Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL Introspection Support #3348

Merged
merged 23 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Ongoing wrk
  • Loading branch information
jamesagnew committed Feb 2, 2022
commit eaccb0e046db2af28657cb1ccc674cdfb94db072
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public FhirContext fhirContextDstu3() {
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider(ISearchParamRegistry theSearchParamRegistry) {
return new GraphQLProviderWithIntrospection(fhirContextDstu3(), validationSupportChain(), graphqlStorageServices(), theSearchParamRegistry);
return new GraphQLProviderWithIntrospection(fhirContextDstu3(), validationSupportChain(), graphqlStorageServices(), theSearchParamRegistry, daoRegistry());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider(ISearchParamRegistry theSearchParamRegistry) {
return new GraphQLProviderWithIntrospection(fhirContextR4(), validationSupportChain(), graphqlStorageServices(), theSearchParamRegistry);
return new GraphQLProviderWithIntrospection(fhirContextR4(), validationSupportChain(), graphqlStorageServices(), theSearchParamRegistry, daoRegistry());
}

@Bean(name = "myResourceCountsCache")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() {
@Bean(name = GRAPHQL_PROVIDER_NAME)
@Lazy
public GraphQLProvider graphQLProvider(ISearchParamRegistry theSearchParamRegistry) {
return new GraphQLProviderWithIntrospection(fhirContextR5(), validationSupportChain(), graphqlStorageServices(), theSearchParamRegistry);
return new GraphQLProviderWithIntrospection(fhirContextR5(), validationSupportChain(), graphqlStorageServices(), theSearchParamRegistry, daoRegistry());
}

@Bean(name = "myResourceCountsCache")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.IDaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.StringUtil;
Expand Down Expand Up @@ -58,9 +60,12 @@
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static ca.uhn.fhir.util.MessageSupplier.msg;

Expand All @@ -71,15 +76,17 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
private final IValidationSupport myValidationSupport;
private final ISearchParamRegistry mySearchParamRegistry;
private final VersionSpecificWorkerContextWrapper myContext;
private final IDaoRegistry myDaoRegistry;

/**
* Constructor
*/
public GraphQLProviderWithIntrospection(FhirContext theFhirContext, IValidationSupport theValidationSupport, IGraphQLStorageServices theIGraphQLStorageServices, ISearchParamRegistry theSearchParamRegistry) {
public GraphQLProviderWithIntrospection(FhirContext theFhirContext, IValidationSupport theValidationSupport, IGraphQLStorageServices theIGraphQLStorageServices, ISearchParamRegistry theSearchParamRegistry, IDaoRegistry theDaoRegistry) {
super(theFhirContext, theValidationSupport, theIGraphQLStorageServices);

myValidationSupport = theValidationSupport;
mySearchParamRegistry = theSearchParamRegistry;
myDaoRegistry = theDaoRegistry;

myContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(theValidationSupport);
myGenerator = new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion());
Expand All @@ -95,27 +102,39 @@ public String processGraphQlPostRequest(ServletRequestDetails theServletRequestD
if (theQueryBody.contains("__schema")) {
EnumSet<GraphQLSchemaGenerator.FHIROperationType> operations;
if (theId != null) {
operations = EnumSet.of(GraphQLSchemaGenerator.FHIROperationType.READ);
throw new InvalidRequestException("GraphQL introspection not supported at instance level. Please try at server- or instance- level.");
}

operations = EnumSet.of(GraphQLSchemaGenerator.FHIROperationType.READ, GraphQLSchemaGenerator.FHIROperationType.SEARCH);

Collection<String> resourceTypes;
if (theRequestDetails.getResourceName() != null) {
resourceTypes = Collections.singleton(theRequestDetails.getResourceName());
} else {
operations = EnumSet.of(GraphQLSchemaGenerator.FHIROperationType.SEARCH);
resourceTypes = new HashSet<>();
for (String next : myContext.getResourceNames()) {
if (myDaoRegistry.isResourceTypeSupported(next)) {
resourceTypes.add(next);
}
}
resourceTypes = resourceTypes
.stream()
.sorted()
.collect(Collectors.toList());
}
return generateSchema(theQueryBody, theRequestDetails.getResourceName(), operations);

return generateSchema(theQueryBody, resourceTypes, operations);
} else {
return super.processGraphQlPostRequest(theServletRequestDetails, theRequestDetails, theId, theQueryBody);
}
}

private String generateSchema(String theQueryBody, String theResourceName, EnumSet<GraphQLSchemaGenerator.FHIROperationType> theOperations) {
private String generateSchema(String theQueryBody, Collection<String> theResourceTypes, EnumSet<GraphQLSchemaGenerator.FHIROperationType> theOperations) {

final StringBuilder schemaBuilder = new StringBuilder();
try (Writer writer = new StringBuilderWriter(schemaBuilder)) {
// Generate FHIR base types schemas
myGenerator.generateTypes(writer);

// Generate schemas for the resource type
StructureDefinition sd = fetchStructureDefinition(theResourceName);
List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry.getActiveSearchParams(theResourceName).values());
myGenerator.generateResource(writer, sd, parameters, theOperations);
myGenerator.generateTypes(writer, theOperations);

// Fix up a few things that are missing from the generated schema
writer
Expand All @@ -127,17 +146,29 @@ private String generateSchema(String theQueryBody, String theResourceName, EnumS
.append("\n id: [token]" + "\n}")
.append("\n");

writer.append("\ntype Query {");
if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.READ)) {
writer
.append("\n ")
.append(theResourceName)
.append("(id: String): ")
.append(theResourceName);
// Generate schemas for the resource types
for (String nextResourceType : theResourceTypes) {
StructureDefinition sd = fetchStructureDefinition(nextResourceType);
List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry.getActiveSearchParams(nextResourceType).values());
myGenerator.generateResource(writer, sd, parameters, theOperations);
}
if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.SEARCH)) {
myGenerator.generateListAccessQuery(writer, parameters, theResourceName);
myGenerator.generateConnectionAccessQuery(writer, parameters, theResourceName);

// Generate queries
writer.append("\ntype Query {");
for (String nextResourceType : theResourceTypes) {
if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.READ)) {
writer
.append("\n ")
.append(nextResourceType)
.append("(id: String): ")
.append(nextResourceType)
.append("\n");
}
if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.SEARCH)) {
List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry.getActiveSearchParams(nextResourceType).values());
myGenerator.generateListAccessQuery(writer, parameters, nextResourceType);
myGenerator.generateConnectionAccessQuery(writer, parameters, nextResourceType);
}
}
writer.append("\n}");

Expand All @@ -148,6 +179,7 @@ private String generateSchema(String theQueryBody, String theResourceName, EnumS

String schema = schemaBuilder.toString().replace("\r", "");

// Set these to INFO if you're testing, then set back before committing
ourLog.debug("Schema generated: {} chars", schema.length());
ourLog.debug("Schema generated: {}", msg(() -> StringUtil.prependLineNumbers(schema)));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.provider.r4;

import ca.uhn.fhir.util.FileUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
Expand All @@ -10,7 +11,9 @@
import org.apache.http.entity.StringEntity;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -24,16 +27,39 @@
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;

@TestMethodOrder(MethodOrderer.MethodName.class)
public class GraphQLR4Test extends BaseResourceProviderR4Test {
public static final String INTROSPECTION_QUERY = "{\"query\":\"\\n query IntrospectionQuery {\\n __schema {\\n queryType { name }\\n mutationType { name }\\n subscriptionType { name }\\n types {\\n ...FullType\\n }\\n directives {\\n name\\n description\\n locations\\n args {\\n ...InputValue\\n }\\n }\\n }\\n }\\n\\n fragment FullType on __Type {\\n kind\\n name\\n description\\n fields(includeDeprecated: true) {\\n name\\n description\\n args {\\n ...InputValue\\n }\\n type {\\n ...TypeRef\\n }\\n isDeprecated\\n deprecationReason\\n }\\n inputFields {\\n ...InputValue\\n }\\n interfaces {\\n ...TypeRef\\n }\\n enumValues(includeDeprecated: true) {\\n name\\n description\\n isDeprecated\\n deprecationReason\\n }\\n possibleTypes {\\n ...TypeRef\\n }\\n }\\n\\n fragment InputValue on __InputValue {\\n name\\n description\\n type { ...TypeRef }\\n defaultValue\\n }\\n\\n fragment TypeRef on __Type {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n ofType {\\n kind\\n name\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n \",\"operationName\":\"IntrospectionQuery\"}";
private Logger ourLog = LoggerFactory.getLogger(GraphQLR4Test.class);
private IIdType myPatientId0;

@Test
public void testIntrospectRead_Patient() throws IOException {
public void testInstance_Read_Patient() throws IOException {
initTestPatients();

String uri = ourServerBase + "/Patient/1/$graphql";
String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));

try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" +
" \"name\":[{\n" +
" \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
"}" + DATA_SUFFIX), TestUtil.stripWhitespace(resp));
}

}

@Test
public void testType_Introspect_Patient() throws IOException {
initTestPatients();

String uri = ourServerBase + "/Patient/$graphql";
HttpPost httpGet = new HttpPost(uri);
httpGet.setEntity(new StringEntity(INTROSPECTION_QUERY, ContentType.APPLICATION_JSON));

Expand All @@ -44,17 +70,22 @@ public void testIntrospectRead_Patient() throws IOException {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"Patient\""));
assertThat(resp, not(containsString("\"PatientList\"")));
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\","));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\",")));
assertThat(resp, not(containsString("\"name\":\"Observation\",\"args\":[{\"name\":\"id\"")));
assertThat(resp, not(containsString("\"name\":\"ObservationList\",\"args\":[{\"name\":\"_filter\"")));
assertThat(resp, not(containsString("\"name\":\"ObservationConnection\",\"fields\":[{\"name\":\"count\"")));
assertThat(resp, containsString("\"name\":\"Patient\",\"args\":[{\"name\":\"id\""));
assertThat(resp, containsString("\"name\":\"PatientList\",\"args\":[{\"name\":\"_filter\""));
}
}
}

@Test
public void testIntrospectSearch_Patient() throws IOException {
public void testType_Introspect_Observation() throws IOException {
initTestPatients();

String uri = ourServerBase + "/Patient/$graphql";
String uri = ourServerBase + "/Observation/$graphql";
HttpPost httpGet = new HttpPost(uri);
httpGet.setEntity(new StringEntity(INTROSPECTION_QUERY, ContentType.APPLICATION_JSON));

Expand All @@ -65,20 +96,23 @@ public void testIntrospectSearch_Patient() throws IOException {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\","));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\",")));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"Patient\"")));
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"PatientList\""));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"ObservationList\"")));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\",")));
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\","));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"PatientList\"")));
assertThat(resp, containsString("\"name\":\"Observation\",\"args\":[{\"name\":\"id\""));
assertThat(resp, containsString("\"name\":\"ObservationList\",\"args\":[{\"name\":\"_filter\""));
assertThat(resp, containsString("\"name\":\"ObservationConnection\",\"fields\":[{\"name\":\"count\""));
assertThat(resp, not(containsString("\"name\":\"Patient\",\"args\":[{\"name\":\"id\"")));
assertThat(resp, not(containsString("\"name\":\"PatientList\",\"args\":[{\"name\":\"_filter\"")));
}
}
}

@Test
public void testIntrospectSearch_Observation() throws IOException {
public void testRoot_Introspect() throws IOException {
initTestPatients();

String uri = ourServerBase + "/Observation/$graphql";
String uri = ourServerBase + "/$graphql";
HttpPost httpGet = new HttpPost(uri);
httpGet.setEntity(new StringEntity(INTROSPECTION_QUERY, ContentType.APPLICATION_JSON));

Expand All @@ -87,40 +121,49 @@ public void testIntrospectSearch_Observation() throws IOException {
for (int i = 0; i < 3; i++) {
try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
ourLog.info("Response has size: {}", FileUtil.formatFileSize(resp.length()));
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\",")));
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Patient\","));
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Observation\","));
assertThat(resp, not(containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"PatientList\"")));
assertThat(resp, containsString("{\"kind\":\"OBJECT\",\"name\":\"Query\",\"fields\":[{\"name\":\"ObservationList\""));
assertThat(resp, containsString("\"name\":\"Observation\",\"args\":[{\"name\":\"id\""));
assertThat(resp, containsString("\"name\":\"ObservationList\",\"args\":[{\"name\":\"_filter\""));
assertThat(resp, containsString("\"name\":\"ObservationConnection\",\"fields\":[{\"name\":\"count\""));
assertThat(resp, containsString("\"name\":\"Patient\",\"args\":[{\"name\":\"id\""));
assertThat(resp, containsString("\"name\":\"PatientList\",\"args\":[{\"name\":\"_filter\""));
}
}
}

@Test
public void testInstanceSimpleRead() throws IOException {
public void testRoot_Read_Patient() throws IOException {
initTestPatients();

String query = "{name{family,given}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/Patient/" + myPatientId0.getIdPart() + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));
String query = "{Patient(id:\"" + myPatientId0.getIdPart() + "\"){name{family,given}}}";
HttpGet httpGet = new HttpGet(ourServerBase + "/$graphql?query=" + UrlUtil.escapeUrlParam(query));

try (CloseableHttpResponse response = ourHttpClient.execute(httpGet)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(TestUtil.stripWhitespace(DATA_PREFIX + "{\n" +
" \"name\":[{\n" +
" \"family\":\"FAM\",\n" +
" \"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
" },{\n" +
" \"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
" }]\n" +
"}" + DATA_SUFFIX), TestUtil.stripWhitespace(resp));
assertEquals(TestUtil.stripWhitespace(DATA_PREFIX +
"{\n" +
"\"Patient\":{\n" +
"\"name\":[{\n" +
"\"family\":\"FAM\",\n" +
"\"given\":[\"GIVEN1\",\"GIVEN2\"]\n" +
"},{\n" +
"\"given\":[\"GivenOnly1\",\"GivenOnly2\"]\n" +
"}]\n" +
"}\n" +
"}" +
DATA_SUFFIX), TestUtil.stripWhitespace(resp));
}

}



@Test
public void testSearch_Patient() throws IOException {
public void testRoot_Search_Patient() throws IOException {
initTestPatients();

String query = "{PatientList(given:\"given\"){name{family,given}}}";
Expand All @@ -146,8 +189,9 @@ public void testSearch_Patient() throws IOException {
}

}

@Test
public void testSearch_Observation() throws IOException {
public void testRoot_Search_Observation() throws IOException {
initTestPatients();

String query = "{ObservationList(date: \"2022\") {id}}";
Expand Down
Loading