Implement GraphQL Connection Approach (#1987)
* implement GraphQLQueryBodyParameter and GraphQLQueryBody annotation * GraphQLQueryBodyParameter to recognize application/graphql * fix 500 error on method without @GraphQLQueryBody * refactor to processGraphQlGetRequest and processGraphQlPostRequest * add testGraphPostContentTypeJson and testGraphPostContentTypeGraphql to GraphQLR4RawTest * fix imports * implement GraphQL Connection * add tests * add cacheControlDirective and requestDetails to registerSearch * fix tests and fix bugs * set error code
This commit is contained in:
parent
5dfdc91682
commit
d20c6a7d25
|
@ -25,7 +25,7 @@ public final class Msg {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IMPORTANT: Please update the following comment after you add a new code
|
* IMPORTANT: Please update the following comment after you add a new code
|
||||||
* Last code value: 2075
|
* Last code value: 2076
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private Msg() {}
|
private Msg() {}
|
||||||
|
|
|
@ -159,6 +159,7 @@ ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.cannotUpdateUrlOrVersionForValueSetReso
|
||||||
ca.uhn.fhir.jpa.patch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1}
|
ca.uhn.fhir.jpa.patch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1}
|
||||||
|
|
||||||
ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
|
ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
|
||||||
|
ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices.invalidGraphqlCursorArgument=GraphQL Cursor "{0}" does not exist and may have expired
|
||||||
|
|
||||||
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.nonDefaultPartitionSelectedForNonPartitionable=Resource type {0} can not be partitioned
|
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.nonDefaultPartitionSelectedForNonPartitionable=Resource type {0} can not be partitioned
|
||||||
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionId=Unknown partition ID: {0}
|
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionId=Unknown partition ID: {0}
|
||||||
|
|
|
@ -6,6 +6,10 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.test.config.TestR4Config;
|
import ca.uhn.fhir.jpa.test.config.TestR4Config;
|
||||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
|
import org.apache.http.NameValuePair;
|
||||||
|
import org.apache.http.client.utils.URLEncodedUtils;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.r4.model.Appointment;
|
import org.hl7.fhir.r4.model.Appointment;
|
||||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||||
|
@ -18,22 +22,21 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.net.URI;
|
||||||
import java.util.Arrays;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Collections;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices.SEARCH_ID_PARAM;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static ca.uhn.fhir.jpa.graphql.DaoRegistryGraphQLStorageServices.SEARCH_OFFSET_PARAM;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ -56,16 +59,16 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
myDaoConfig.setFilterParameterEnabled(true);
|
myDaoConfig.setFilterParameterEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createSomeAppointment() {
|
private void createSomeAppointmentWithType(String id, CodeableConcept type) {
|
||||||
CodeableConcept someCodeableConcept = new CodeableConcept(new Coding("TEST_SYSTEM", "TEST_CODE", "TEST_DISPLAY"));
|
|
||||||
Appointment someAppointment = new Appointment();
|
Appointment someAppointment = new Appointment();
|
||||||
someAppointment.setAppointmentType(someCodeableConcept);
|
someAppointment.setId(id);
|
||||||
return myAppointmentDao.create(someAppointment).getId().getIdPart();
|
someAppointment.setAppointmentType(type);
|
||||||
|
myAppointmentDao.update(someAppointment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListResourcesGraphqlArgumentConversion() {
|
public void testListResourcesGraphqlArgumentConversion() {
|
||||||
String appointmentId = createSomeAppointment();
|
createSomeAppointmentWithType("hapi-1", new CodeableConcept(new Coding("TEST_SYSTEM", "TEST_CODE", "TEST_DISPLAY")));
|
||||||
|
|
||||||
Argument argument = new Argument("appointment_type", new StringValue("TEST_CODE"));
|
Argument argument = new Argument("appointment_type", new StringValue("TEST_CODE"));
|
||||||
|
|
||||||
|
@ -73,12 +76,12 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
||||||
|
|
||||||
assertFalse(result.isEmpty());
|
assertFalse(result.isEmpty());
|
||||||
assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals(appointmentId)));
|
assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals("hapi-1")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListResourceGraphqlFilterArgument() {
|
public void testListResourceGraphqlFilterArgument() {
|
||||||
String appointmentId = createSomeAppointment();
|
createSomeAppointmentWithType("hapi-1", new CodeableConcept(new Coding("TEST_SYSTEM", "TEST_CODE", "TEST_DISPLAY")));
|
||||||
|
|
||||||
Argument argument = new Argument("_filter", new StringValue("appointment-type eq TEST_CODE"));
|
Argument argument = new Argument("_filter", new StringValue("appointment-type eq TEST_CODE"));
|
||||||
|
|
||||||
|
@ -86,12 +89,12 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
||||||
|
|
||||||
assertFalse(result.isEmpty());
|
assertFalse(result.isEmpty());
|
||||||
assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals(appointmentId)));
|
assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals("hapi-1")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testListResourceGraphqlTokenArgumentWithSystem() {
|
public void testListResourceGraphqlTokenArgumentWithSystem() {
|
||||||
String appointmentId = createSomeAppointment();
|
createSomeAppointmentWithType("hapi-1", new CodeableConcept(new Coding("TEST_SYSTEM", "TEST_CODE", "TEST_DISPLAY")));;
|
||||||
|
|
||||||
Argument argument = new Argument("appointment_type", new StringValue("TEST_SYSTEM|TEST_CODE"));
|
Argument argument = new Argument("appointment_type", new StringValue("TEST_SYSTEM|TEST_CODE"));
|
||||||
|
|
||||||
|
@ -99,7 +102,7 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
||||||
|
|
||||||
assertFalse(result.isEmpty());
|
assertFalse(result.isEmpty());
|
||||||
assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals(appointmentId)));
|
assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals("hapi-1")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -109,9 +112,9 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
List<IBaseResource> result = new ArrayList<>();
|
List<IBaseResource> result = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
|
||||||
fail();
|
fail("InvalidRequestException should be thrown.");
|
||||||
} catch (InvalidRequestException e) {
|
} catch (InvalidRequestException e) {
|
||||||
assertEquals(Msg.code(1275) + "Unknown GraphQL argument \"test\". Value GraphQL argument for this type are: [_content, _id, _lastUpdated, _profile, _security, _source, _tag, _text, actor, appointment_type, based_on, date, identifier, location, part_status, patient, practitioner, reason_code, reason_reference, service_category, service_type, slot, specialty, status, supporting_info]", e.getMessage());
|
assertTrue(e.getMessage().contains("Unknown GraphQL argument \"test\"."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +163,7 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
assertFalse(result.isEmpty());
|
assertFalse(result.isEmpty());
|
||||||
assertEquals(5, result.size());
|
assertEquals(5, result.size());
|
||||||
|
|
||||||
List<String> expectedId = Arrays.asList("hapi-1", "hapi-2", "hapi-0", "hapi-3", "hapi-4");
|
List<String> expectedId = Arrays.asList("hapi-1", "hapi-2", "hapi-0", "hapi-3", "hapi-4");
|
||||||
assertTrue(result.stream().allMatch((it) -> expectedId.contains(it.getIdElement().getIdPart())));
|
assertTrue(result.stream().allMatch((it) -> expectedId.contains(it.getIdElement().getIdPart())));
|
||||||
|
|
||||||
//_offset=5
|
//_offset=5
|
||||||
|
@ -173,7 +176,67 @@ public class DaoRegistryGraphQLStorageServicesTest extends BaseJpaR4Test {
|
||||||
assertFalse(result2.isEmpty());
|
assertFalse(result2.isEmpty());
|
||||||
assertEquals(5, result2.size());
|
assertEquals(5, result2.size());
|
||||||
|
|
||||||
List<String> expectedId2 = Arrays.asList("hapi-5", "hapi-6", "hapi-7", "hapi-8", "hapi-9");
|
List<String> expectedId2 = Arrays.asList("hapi-5", "hapi-6", "hapi-7", "hapi-8", "hapi-9");
|
||||||
assertTrue(result2.stream().allMatch((it) -> expectedId2.contains(it.getIdElement().getIdPart())));
|
assertTrue(result2.stream().allMatch((it) -> expectedId2.contains(it.getIdElement().getIdPart())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearch() {
|
||||||
|
createSomePatientWithId("hapi-1");
|
||||||
|
|
||||||
|
List<Argument> arguments = Collections.emptyList();
|
||||||
|
IBaseBundle bundle = mySvc.search(mySrd, "Patient", arguments);
|
||||||
|
|
||||||
|
List<String> result = toUnqualifiedVersionlessIdValues(bundle);
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
assertEquals("Patient/hapi-1", result.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchNextPage() throws URISyntaxException {
|
||||||
|
createSomePatientWithId("hapi-1");
|
||||||
|
createSomePatientWithId("hapi-2");
|
||||||
|
createSomePatientWithId("hapi-3");
|
||||||
|
|
||||||
|
List<Argument> arguments = Collections.singletonList(new Argument("_count", new StringValue("1")));
|
||||||
|
IBaseBundle bundle = mySvc.search(mySrd, "Patient", arguments);
|
||||||
|
|
||||||
|
Optional<String> nextUrl = Optional.ofNullable(BundleUtil.getLinkUrlOfType(myFhirContext, bundle, "next"));
|
||||||
|
assertTrue(nextUrl.isPresent());
|
||||||
|
|
||||||
|
List<NameValuePair> params = URLEncodedUtils.parse(new URI(nextUrl.get()), StandardCharsets.UTF_8);
|
||||||
|
Optional<String> cursorId = params.stream()
|
||||||
|
.filter(it -> SEARCH_ID_PARAM.equals(it.getName()))
|
||||||
|
.map(NameValuePair::getValue)
|
||||||
|
.findAny();
|
||||||
|
Optional<String> cursorOffset = params.stream()
|
||||||
|
.filter(it -> SEARCH_OFFSET_PARAM.equals(it.getName()))
|
||||||
|
.map(NameValuePair::getValue)
|
||||||
|
.findAny();
|
||||||
|
|
||||||
|
assertTrue(cursorId.isPresent());
|
||||||
|
assertTrue(cursorOffset.isPresent());
|
||||||
|
|
||||||
|
List<Argument> nextArguments = Arrays.asList(
|
||||||
|
new Argument(SEARCH_ID_PARAM, new StringValue(cursorId.get())),
|
||||||
|
new Argument(SEARCH_OFFSET_PARAM, new StringValue(cursorOffset.get()))
|
||||||
|
);
|
||||||
|
|
||||||
|
Optional<IBaseBundle> nextBundle = Optional.ofNullable(mySvc.search(mySrd, "Patient", nextArguments));
|
||||||
|
assertTrue(nextBundle.isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchInvalidCursor() {
|
||||||
|
try {
|
||||||
|
List<Argument> arguments = Arrays.asList(
|
||||||
|
new Argument(SEARCH_ID_PARAM, new StringValue("invalid-search-id")),
|
||||||
|
new Argument(SEARCH_OFFSET_PARAM, new StringValue("0"))
|
||||||
|
);
|
||||||
|
mySvc.search(mySrd, "Patient", arguments);
|
||||||
|
fail("InvalidRequestException should be thrown.");
|
||||||
|
} catch (InvalidRequestException e) {
|
||||||
|
assertTrue(e.getMessage().contains("GraphQL Cursor \"invalid-search-id\" does not exist and may have expired"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,18 @@ import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
|
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
|
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
|
||||||
|
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.model.api.IQueryParameterOr;
|
import ca.uhn.fhir.model.api.IQueryParameterOr;
|
||||||
|
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.BundleLinks;
|
||||||
|
import ca.uhn.fhir.rest.api.CacheControlDirective;
|
||||||
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
|
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.param.DateOrListParam;
|
import ca.uhn.fhir.rest.param.DateOrListParam;
|
||||||
|
@ -44,8 +52,9 @@ import ca.uhn.fhir.rest.param.StringOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.StringParam;
|
import ca.uhn.fhir.rest.param.StringParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
import ca.uhn.fhir.rest.param.TokenParam;
|
||||||
|
import ca.uhn.fhir.rest.server.IPagingProvider;
|
||||||
|
import ca.uhn.fhir.rest.server.RestfulServerUtils;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
|
|
||||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||||
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
@ -57,19 +66,27 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.utilities.graphql.Argument;
|
import org.hl7.fhir.utilities.graphql.Argument;
|
||||||
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
|
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
|
||||||
import org.hl7.fhir.utilities.graphql.Value;
|
import org.hl7.fhir.utilities.graphql.Value;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static ca.uhn.fhir.rest.api.Constants.PARAM_COUNT;
|
||||||
import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER;
|
import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER;
|
||||||
|
|
||||||
public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageServices {
|
public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageServices {
|
||||||
|
|
||||||
|
// the constant hasn't already been defined in org.hl7.fhir.core so we define it here
|
||||||
|
static final String SEARCH_ID_PARAM = "search-id";
|
||||||
|
static final String SEARCH_OFFSET_PARAM = "search-offset";
|
||||||
|
|
||||||
private static final int MAX_SEARCH_SIZE = 500;
|
private static final int MAX_SEARCH_SIZE = 500;
|
||||||
@Autowired
|
@Autowired
|
||||||
private FhirContext myContext;
|
private FhirContext myContext;
|
||||||
|
@ -77,6 +94,12 @@ public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageService
|
||||||
private DaoRegistry myDaoRegistry;
|
private DaoRegistry myDaoRegistry;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISearchParamRegistry mySearchParamRegistry;
|
private ISearchParamRegistry mySearchParamRegistry;
|
||||||
|
@Autowired
|
||||||
|
protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
|
||||||
|
@Autowired
|
||||||
|
private IRequestPartitionHelperSvc myPartitionHelperSvc;
|
||||||
|
@Autowired
|
||||||
|
private IPagingProvider myPagingProvider;
|
||||||
|
|
||||||
private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
|
private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
|
||||||
RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(theResourceType);
|
RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(theResourceType);
|
||||||
|
@ -95,19 +118,18 @@ public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageService
|
||||||
return name.replaceAll("-", "_");
|
return name.replaceAll("-", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.NEVER)
|
private SearchParameterMap buildSearchParams(String theType, List<Argument> theSearchParams) {
|
||||||
@Override
|
List<Argument> resourceSearchParam = theSearchParams.stream()
|
||||||
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
|
.filter(it -> !PARAM_COUNT.equals(it.getName()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
FhirContext fhirContext = myContext;
|
FhirContext fhirContext = myContext;
|
||||||
RuntimeResourceDefinition typeDef = fhirContext.getResourceDefinition(theType);
|
RuntimeResourceDefinition typeDef = fhirContext.getResourceDefinition(theType);
|
||||||
IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(typeDef.getImplementingClass());
|
|
||||||
|
|
||||||
SearchParameterMap params = new SearchParameterMap();
|
SearchParameterMap params = new SearchParameterMap();
|
||||||
params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);
|
|
||||||
|
|
||||||
ResourceSearchParams searchParams = mySearchParamRegistry.getActiveSearchParams(typeDef.getName());
|
ResourceSearchParams searchParams = mySearchParamRegistry.getActiveSearchParams(typeDef.getName());
|
||||||
|
|
||||||
for (Argument nextArgument : theSearchParams) {
|
for (Argument nextArgument : resourceSearchParam) {
|
||||||
|
|
||||||
if (nextArgument.getName().equals(PARAM_FILTER)) {
|
if (nextArgument.getName().equals(PARAM_FILTER)) {
|
||||||
String value = nextArgument.getValues().get(0).getValue();
|
String value = nextArgument.getValues().get(0).getValue();
|
||||||
|
@ -189,8 +211,17 @@ public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageService
|
||||||
params.add(searchParamName, queryParam);
|
params.add(searchParamName, queryParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.NEVER)
|
||||||
|
@Override
|
||||||
|
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
|
||||||
|
SearchParameterMap params = buildSearchParams(theType, theSearchParams);
|
||||||
|
params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);
|
||||||
|
|
||||||
RequestDetails requestDetails = (RequestDetails) theAppInfo;
|
RequestDetails requestDetails = (RequestDetails) theAppInfo;
|
||||||
IBundleProvider response = dao.search(params, requestDetails);
|
IBundleProvider response = getDao(theType).search(params, requestDetails);
|
||||||
Integer size = response.size();
|
Integer size = response.size();
|
||||||
//We set size to null in SearchCoordinatorSvcImpl.executeQuery() if matching results exceeds count
|
//We set size to null in SearchCoordinatorSvcImpl.executeQuery() if matching results exceeds count
|
||||||
//so don't throw here
|
//so don't throw here
|
||||||
|
@ -201,7 +232,6 @@ public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageService
|
||||||
|
|
||||||
Validate.notNull(size, "size is null");
|
Validate.notNull(size, "size is null");
|
||||||
theMatches.addAll(response.getResources(0, size));
|
theMatches.addAll(response.getResources(0, size));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRED)
|
@Transactional(propagation = Propagation.REQUIRED)
|
||||||
|
@ -228,10 +258,88 @@ public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageService
|
||||||
return new ReferenceResolution(theContext, outcome);
|
return new ReferenceResolution(theContext, outcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<String> getArgument(List<Argument> params, String name) {
|
||||||
|
return params.stream()
|
||||||
|
.filter(it -> name.equals(it.getName()))
|
||||||
|
.map(it -> it.getValues().get(0).getValue())
|
||||||
|
.findAny();
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.NEVER)
|
@Transactional(propagation = Propagation.NEVER)
|
||||||
@Override
|
@Override
|
||||||
public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
|
public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
|
||||||
throw new NotImplementedOperationException(Msg.code(1277) + "Not yet able to handle this GraphQL request");
|
RequestDetails requestDetails = (RequestDetails) theAppInfo;
|
||||||
|
|
||||||
|
Optional<String> searchIdArgument = getArgument(theSearchParams, SEARCH_ID_PARAM);
|
||||||
|
Optional<String> searchOffsetArgument = getArgument(theSearchParams, SEARCH_OFFSET_PARAM);
|
||||||
|
|
||||||
|
String searchId;
|
||||||
|
int searchOffset;
|
||||||
|
int pageSize;
|
||||||
|
IBundleProvider response;
|
||||||
|
|
||||||
|
if (searchIdArgument.isPresent() && searchOffsetArgument.isPresent()) {
|
||||||
|
searchId = searchIdArgument.get();
|
||||||
|
searchOffset = Integer.parseInt(searchOffsetArgument.get());
|
||||||
|
|
||||||
|
response = Optional.ofNullable(myPagingProvider.retrieveResultList(requestDetails, searchId)).orElseThrow(()->{
|
||||||
|
String msg = myContext.getLocalizer().getMessageSanitized(DaoRegistryGraphQLStorageServices.class, "invalidGraphqlCursorArgument", searchId);
|
||||||
|
return new InvalidRequestException(Msg.code(2076) + msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
pageSize = Optional.ofNullable(response.preferredPageSize())
|
||||||
|
.orElseGet(myPagingProvider::getDefaultPageSize);
|
||||||
|
} else {
|
||||||
|
pageSize = getArgument(theSearchParams, "_count").map(Integer::parseInt)
|
||||||
|
.orElseGet(myPagingProvider::getDefaultPageSize);
|
||||||
|
|
||||||
|
SearchParameterMap params = buildSearchParams(theType, theSearchParams);
|
||||||
|
params.setCount(pageSize);
|
||||||
|
|
||||||
|
CacheControlDirective cacheControlDirective = new CacheControlDirective();
|
||||||
|
cacheControlDirective.parse(requestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL));
|
||||||
|
|
||||||
|
RequestPartitionId requestPartitionId = myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(requestDetails, theType, params, null);
|
||||||
|
response = mySearchCoordinatorSvc.registerSearch(getDao(theType), params, theType, cacheControlDirective, requestDetails, requestPartitionId);
|
||||||
|
|
||||||
|
searchOffset = 0;
|
||||||
|
searchId = myPagingProvider.storeResultList(requestDetails, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// response.size() may return {@literal null}, in that case use pageSize
|
||||||
|
String serverBase = requestDetails.getFhirServerBase();
|
||||||
|
Optional<Integer> numTotalResults = Optional.ofNullable(response.size());
|
||||||
|
int numToReturn = numTotalResults.map(integer -> Math.min(pageSize, integer - searchOffset)).orElse(pageSize);
|
||||||
|
|
||||||
|
BundleLinks links = new BundleLinks(requestDetails.getServerBaseForRequest(), null, RestfulServerUtils.prettyPrintResponse(requestDetails.getServer(), requestDetails), BundleTypeEnum.SEARCHSET);
|
||||||
|
|
||||||
|
// RestfulServerUtils.createLinkSelf not suitable here
|
||||||
|
String linkFormat = "%s/%s?_format=application/json&search-id=%s&search-offset=%d&_count=%d";
|
||||||
|
|
||||||
|
String linkSelf = String.format(linkFormat, serverBase, theType, searchId, searchOffset, pageSize);
|
||||||
|
links.setSelf(linkSelf);
|
||||||
|
|
||||||
|
boolean hasNext = numTotalResults.map(total -> (searchOffset + numToReturn) < total).orElse(true);
|
||||||
|
|
||||||
|
if (hasNext) {
|
||||||
|
String linkNext = String.format(linkFormat, serverBase, theType, searchId, searchOffset+numToReturn, pageSize);
|
||||||
|
links.setNext(linkNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchOffset > 0) {
|
||||||
|
String linkPrev = String.format(linkFormat, serverBase, theType, searchId, Math.max(0, searchOffset-pageSize), pageSize);
|
||||||
|
links.setPrev(linkPrev);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IBaseResource> resourceList = response.getResources(searchOffset, numToReturn + searchOffset);
|
||||||
|
|
||||||
|
IVersionSpecificBundleFactory bundleFactory = myContext.newBundleFactory();
|
||||||
|
bundleFactory.addRootPropertiesToBundle(response.getUuid(), links, response.size(), response.getPublished());
|
||||||
|
bundleFactory.addResourcesToBundle(resourceList, BundleTypeEnum.SEARCHSET, serverBase, null, null);
|
||||||
|
|
||||||
|
IBaseResource result = bundleFactory.getResourceBundle();
|
||||||
|
return (IBaseBundle) result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue