Skip to content

Commit 6eb220f

Browse files
lukedegruchyclaude
andauthored
Add transaction support to IgRepository that writes to and reads from disk for SDE SearchParameter testing (#966)
* Add in-memory transaction support to IgRepository for SDE SearchParameter testing IgRepository inherited the default IRepository.transaction() which throws NotImplementedOperationException. This meant ensureSupplementalDataElementSearchParameter() silently failed when using IgRepository, making it impossible to verify SDE SearchParameter creation in integration tests. Add a simple transaction() implementation that supports POST entries stored in an in-memory overlay (no filesystem writes), with read() and search() modified to include these resources. Add hasSupplementalDataSearchParameter() assertions to the Measure and MultiMeasure test fluent APIs, and wire them into existing tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Expand IgRepository transaction() to support PUT and DELETE operations Previously transaction() only supported POST entries. This adds PUT (store/overwrite) and DELETE (remove from overlay) to match InMemoryFhirRepository's transaction capabilities. All operations remain in-memory only and are not written to the filesystem. Adds IgRepositoryTransactionTest with parameterized tests covering POST, PUT, DELETE, mixed operations, and error cases for both Library and SearchParameter resource types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Write transaction resources to disk instead of in-memory overlay Remove the transactionResources ConcurrentHashMap and delegate transaction() to the existing create/update/delete methods, which write to the filesystem. This removes the in-memory overlay lookups from read() and search(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 400f9d8 commit 6eb220f

8 files changed

Lines changed: 414 additions & 6 deletions

File tree

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.opencds.cqf.fhir.cr.measure.r4;
22

3+
import static org.junit.jupiter.api.Assertions.assertFalse;
34
import static org.opencds.cqf.fhir.test.Resources.getResourcePath;
45

56
import ca.uhn.fhir.context.FhirContext;
@@ -19,6 +20,7 @@
1920
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus;
2021
import org.hl7.fhir.r4.model.Parameters;
2122
import org.hl7.fhir.r4.model.Resource;
23+
import org.hl7.fhir.r4.model.SearchParameter;
2224
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE;
2325
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE;
2426
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE;
@@ -32,6 +34,7 @@
3234
import org.opencds.cqf.fhir.cr.measure.r4.selected.report.SelectedMeasureReportReference;
3335
import org.opencds.cqf.fhir.utility.monad.Eithers;
3436
import org.opencds.cqf.fhir.utility.repository.ig.IgRepository;
37+
import org.opencds.cqf.fhir.utility.search.Searches.SearchBuilder;
3538

3639
// consider rolling this entire thing into MultiMeasure with "single measure" assertions
3740
@SuppressWarnings({"squid:S2699", "squid:S5960", "squid:S1135"})
@@ -459,5 +462,16 @@ public SelectedMeasureReportExtension extensionByValueReference(String resourceR
459462
public SelectedMeasureReportExtension extension(String supplementalDataId) {
460463
return report().extension(supplementalDataId);
461464
}
465+
466+
public Then hasSupplementalDataSearchParameter() {
467+
var result = repository.search(
468+
Bundle.class,
469+
SearchParameter.class,
470+
new SearchBuilder()
471+
.withTokenParam("code", "supplemental-data")
472+
.build());
473+
assertFalse(result.getEntry().isEmpty(), "Expected supplemental-data SearchParameter in repository");
474+
return this;
475+
}
462476
}
463477
}

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeProportionTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ void proportionBooleanPopulation() {
2626
.measureId("ProportionBooleanAllPopulations")
2727
.evaluate()
2828
.then()
29+
.hasSupplementalDataSearchParameter()
2930
// MeasureDef assertions (pre-scoring) - verify internal state after processing
3031
.def()
3132
.hasNoErrors()

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ class MinimalMeasureTest {
1616
void evaluateSucceedsWithMinimalMeasure() {
1717
var when = GIVEN_MINIMAL_MEASURE_REPO.when().measureId("Minimal").evaluate();
1818

19-
var report = when.then().report();
19+
var then = when.then();
20+
then.hasSupplementalDataSearchParameter();
21+
var report = then.report();
2022
assertNotNull(report);
2123
assertEquals(0, report.getGroup().size());
2224
}

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.opencds.cqf.fhir.cr.measure.r4;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
45
import static org.junit.jupiter.api.Assertions.assertNotNull;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
67
import static org.junit.jupiter.api.Assertions.fail;
@@ -40,6 +41,7 @@
4041
import org.hl7.fhir.r4.model.Reference;
4142
import org.hl7.fhir.r4.model.Resource;
4243
import org.hl7.fhir.r4.model.ResourceType;
44+
import org.hl7.fhir.r4.model.SearchParameter;
4345
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE;
4446
import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE;
4547
import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE;
@@ -49,6 +51,7 @@
4951
import org.opencds.cqf.fhir.cr.measure.r4.selected.def.SelectedMeasureDefCollection;
5052
import org.opencds.cqf.fhir.utility.BundleHelper;
5153
import org.opencds.cqf.fhir.utility.repository.ig.IgRepository;
54+
import org.opencds.cqf.fhir.utility.search.Searches.SearchBuilder;
5255

5356
@SuppressWarnings("squid:S1135")
5457
class MultiMeasure {
@@ -324,6 +327,17 @@ public MultiMeasure.SelectedMeasureReport measureReport(String measureUrl, Strin
324327
public MultiMeasure.SelectedMeasureReport getFirstMeasureReport() {
325328
return report().getFirstMeasureReport();
326329
}
330+
331+
public MultiMeasure.Then hasSupplementalDataSearchParameter() {
332+
var result = repository.search(
333+
Bundle.class,
334+
SearchParameter.class,
335+
new SearchBuilder()
336+
.withTokenParam("code", "supplemental-data")
337+
.build());
338+
assertFalse(result.getEntry().isEmpty(), "Expected supplemental-data SearchParameter in repository");
339+
return this;
340+
}
327341
}
328342

329343
public static class SelectedReport extends MultiMeasure.Selected<Parameters, Void> {

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasureServiceTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void MultiMeasure_AllSubjects_MeasureIdentifier() {
2828
.evaluate();
2929

3030
when.then()
31+
.hasSupplementalDataSearchParameter()
3132
// This is a population/summary report so we should have a single bundle containing
3233
// all MeasureReports
3334
.hasBundleCount(1)

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureServiceEnsureSearchParameterTest.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import static org.junit.jupiter.api.Assertions.assertTrue;
55

66
import ca.uhn.fhir.context.FhirContext;
7-
import org.hl7.fhir.instance.model.api.IBaseBundle;
87
import org.hl7.fhir.r4.model.Bundle;
98
import org.hl7.fhir.r4.model.SearchParameter;
109
import org.junit.jupiter.api.Test;
@@ -16,10 +15,6 @@
1615
/**
1716
* Tests the {@code ensureSearchParameters} flag on {@link MeasureEvaluationOptions}
1817
* to verify that the SDE SearchParameter is created (or not) based on the flag value.
19-
* <p/>
20-
* Note that {@link org.opencds.cqf.fhir.utility.repository.ig.IgRepository} does not support
21-
* {@link org.opencds.cqf.fhir.utility.repository.ig.IgRepository#transaction(IBaseBundle)}, which
22-
* means we can't assert ensure SDE at the Measure/MultiMeasure integration test level.
2318
*/
2419
class R4MultiMeasureServiceEnsureSearchParameterTest {
2520

cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ca.uhn.fhir.rest.api.EncodingEnum;
1111
import ca.uhn.fhir.rest.api.MethodOutcome;
1212
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
13+
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
1314
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
1415
import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException;
1516
import ca.uhn.fhir.util.BundleBuilder;
@@ -31,6 +32,8 @@
3132
import org.hl7.fhir.instance.model.api.IBaseParameters;
3233
import org.hl7.fhir.instance.model.api.IBaseResource;
3334
import org.hl7.fhir.instance.model.api.IIdType;
35+
import org.opencds.cqf.fhir.utility.BundleHelper;
36+
import org.opencds.cqf.fhir.utility.Canonicals;
3437
import org.opencds.cqf.fhir.utility.matcher.ResourceMatcher;
3538
import org.opencds.cqf.fhir.utility.repository.Repositories;
3639
import org.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding;
@@ -694,6 +697,61 @@ public <R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R
694697
return invokeOperation(id, id.getResourceType(), name, parameters);
695698
}
696699

700+
/**
701+
* Processes a transaction bundle, supporting PUT, POST, and DELETE entries.
702+
* Resources are written to the filesystem via the standard {@link #create}, {@link #update},
703+
* and {@link #delete} methods.
704+
*
705+
* @param <B> the bundle type
706+
* @param transaction the transaction bundle to process
707+
* @param headers request headers
708+
* @return a response bundle with entry responses for each processed entry
709+
* @throws NotImplementedOperationException if the transaction contains unsupported entry types
710+
*/
711+
@Override
712+
@SuppressWarnings("unchecked")
713+
public <B extends IBaseBundle> B transaction(B transaction, Map<String, String> headers) {
714+
var version = transaction.getStructureFhirVersionEnum();
715+
var returnBundle = (B) BundleHelper.newBundle(version, "transaction-response");
716+
BundleHelper.getEntry(transaction).forEach(e -> {
717+
if (BundleHelper.isEntryRequestPut(version, e)) {
718+
var resource = BundleHelper.getEntryResource(version, e);
719+
this.update(resource, headers);
720+
var location = resource.getIdElement().getValue();
721+
BundleHelper.addEntry(
722+
returnBundle,
723+
BundleHelper.newEntryWithResponse(
724+
version, BundleHelper.newResponseWithLocation(version, location)));
725+
} else if (BundleHelper.isEntryRequestPost(version, e)) {
726+
var resource = BundleHelper.getEntryResource(version, e);
727+
if (!resource.getIdElement().hasIdPart()) {
728+
resource.setId(java.util.UUID.randomUUID().toString());
729+
}
730+
this.create(resource, headers);
731+
var location = resource.getIdElement().getValue();
732+
BundleHelper.addEntry(
733+
returnBundle,
734+
BundleHelper.newEntryWithResponse(
735+
version, BundleHelper.newResponseWithLocation(version, location)));
736+
} else if (BundleHelper.isEntryRequestDelete(version, e)) {
737+
var requestId = BundleHelper.getEntryRequestId(version, e)
738+
.orElseThrow(() -> new ResourceNotFoundException("Trying to delete an entry without id"));
739+
var requestUrl = BundleHelper.getEntryRequestUrl(version, e);
740+
var resourceType = Canonicals.getResourceType(requestUrl);
741+
var resourceClass =
742+
fhirContext.getResourceDefinition(resourceType).getImplementingClass();
743+
this.delete(resourceClass, requestId.withResourceType(resourceType), headers);
744+
BundleHelper.addEntry(
745+
returnBundle,
746+
BundleHelper.newEntryWithResponse(
747+
version, BundleHelper.newResponseWithLocation(version, requestUrl)));
748+
} else {
749+
throw new NotImplementedOperationException("Transaction only supports PUT, POST, or DELETE");
750+
}
751+
});
752+
return returnBundle;
753+
}
754+
697755
protected <R extends IBaseResource> R invokeOperation(
698756
IIdType id, String resourceType, String operationName, IBaseParameters parameters) {
699757
if (operationProvider == null) {

0 commit comments

Comments
 (0)