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

allow deleting resources if references are all versioned only #6645

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: add
issue: 6644
jira: SMILE-9604
title: "If references are allowed to contain versioned references,
and a resource is only referenced using versioned references,
DELETE actions will be permitted.
"
TipzCM marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ public class DeleteConflictFinderService {
protected EntityManager myEntityManager;

List<ResourceLink> findConflicts(ResourceTable theEntity, int maxResults) {
TypedQuery<ResourceLink> query = myEntityManager.createQuery(
"SELECT l FROM ResourceLink l WHERE l.myTargetResource.myPid = :target_pid", ResourceLink.class);
String queryStr =
"SELECT l FROM ResourceLink l WHERE l.myTargetResource.myPid = :target_pid AND (l.myTargetResourceVersion IS NULL)";
TypedQuery<ResourceLink> query = myEntityManager.createQuery(queryStr, ResourceLink.class);
query.setParameter("target_pid", theEntity.getId());
query.setMaxResults(maxResults);
return query.getResultList();
List<ResourceLink> resourceLinks = query.getResultList();
TipzCM marked this conversation as resolved.
Show resolved Hide resolved

return resourceLinks;
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
package ca.uhn.fhir.jpa.delete;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;


public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictServiceR4Test.class);


private final DeleteConflictInterceptor myDeleteInterceptor = new DeleteConflictInterceptor();
private int myInterceptorDeleteCount;

Expand Down Expand Up @@ -204,6 +213,139 @@ public void testBadInterceptorNoInfiniteLoop() {
assertEquals(1 + DeleteConflictService.MAX_RETRY_ATTEMPTS, myDeleteInterceptor.myCallCount);
}

private void setupResourceReferenceTests() {
// we don't need this
myInterceptorRegistry.unregisterInterceptor(myDeleteInterceptor);

// we have to allow versioned references; by default we do not
myFhirContext.getParserOptions()
.setStripVersionsFromReferences(false);
}

@ParameterizedTest
@ValueSource(booleans = { true, false })
public void delete_resourceReferencedByVersionedReferenceOnly_succeeds(boolean theUpdateObs) {
// setup
SystemRequestDetails requestDetails = new SystemRequestDetails();
DaoMethodOutcome outcome;

setupResourceReferenceTests();

Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.FINAL);
outcome = myObservationDao.create(observation, requestDetails);
IIdType obsId = outcome.getId();

// create encounter that references Observation by versioned reference only
Encounter encounter = new Encounter();
encounter.setStatus(Encounter.EncounterStatus.FINISHED);
Coding coding = new Coding();
coding.setSystem("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode");
coding.setCode("AMB");
encounter.setClass_(coding);
encounter.addReasonReference()
.setReference(obsId.getValue()); // versioned reference
outcome = myEncounterDao.create(encounter, requestDetails);
assertTrue(outcome.getCreated());

if (theUpdateObs) {
observation.setId(obsId.toUnqualifiedVersionless());
observation.setIssued(new Date());
myObservationDao.update(observation, requestDetails);
}

// test
outcome = myObservationDao.delete(obsId.toUnqualifiedVersionless(), requestDetails);

// validate
assertTrue(outcome.getOperationOutcome() instanceof OperationOutcome);
OperationOutcome oo = (OperationOutcome) outcome.getOperationOutcome();
assertFalse(oo.getIssue().isEmpty());
assertTrue(oo.getIssue().stream().anyMatch(i -> i.getDiagnostics().contains("Successfully deleted 1 resource(s)")));
}

@ParameterizedTest
@ValueSource(booleans = { true, false })
public void delete_resourceReferencedByNonVersionReferenceOnly_fails(boolean theUpdateObservation) {
// setup
SystemRequestDetails requestDetails = new SystemRequestDetails();
DaoMethodOutcome outcome;

setupResourceReferenceTests();

Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.FINAL);
outcome = myObservationDao.create(observation, requestDetails);
IIdType obsId = outcome.getId();

// create encounter that references Observation by versioned reference only
Encounter encounter = new Encounter();
encounter.setStatus(Encounter.EncounterStatus.FINISHED);
Coding coding = new Coding();
coding.setSystem("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode");
coding.setCode("AMB");
encounter.setClass_(coding);
encounter.addReasonReference()
.setReference(obsId.toUnqualifiedVersionless().getValue()); // versionless reference
outcome = myEncounterDao.create(encounter, requestDetails);
assertTrue(outcome.getCreated());

if (theUpdateObservation) {
observation.setId(obsId.toUnqualifiedVersionless());
observation.setIssued(new Date());
myObservationDao.update(observation, requestDetails);
}

// test
try {
myObservationDao.delete(obsId.toUnqualifiedVersionless(), requestDetails);
fail("Deletion of resource referenced by versionless id should fail.");
} catch (ResourceVersionConflictException ex) {
assertTrue(ex.getLocalizedMessage().contains("Unable to delete")
&& ex.getLocalizedMessage().contains("at least one resource has a reference to this resource"),
ex.getLocalizedMessage());
}
}

@Test
public void delete_resourceReferencedByNonVersionedAndVersionedReferences_fails() {
// setup
SystemRequestDetails requestDetails = new SystemRequestDetails();
DaoMethodOutcome outcome;

setupResourceReferenceTests();

Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.FINAL);
outcome = myObservationDao.create(observation, requestDetails);
IIdType obsId = outcome.getId();

// create 2 encounters; one referenced with a versionless id, one referenced with a versioned id
for (String id : new String[] { obsId.getValue(), obsId.toUnqualifiedVersionless().getValue() }) {
Encounter encounter = new Encounter();
encounter.setStatus(Encounter.EncounterStatus.FINISHED);
Coding coding = new Coding();
coding.setSystem("http://terminology.hl7.org/ValueSet/v3-ActEncounterCode");
coding.setCode("AMB");
encounter.setClass_(coding);
encounter.addReasonReference()
.setReference(id);
outcome = myEncounterDao.create(encounter, requestDetails);
assertTrue(outcome.getCreated());
}

// test
try {
// versionless delete
myObservationDao.delete(obsId.toUnqualifiedVersionless(), requestDetails);
fail("Should not be able to delete observations referenced by versionless id because it is referenced by version AND by versionless.");
} catch (ResourceVersionConflictException ex) {
assertTrue(ex.getLocalizedMessage().contains("Unable to delete")
&& ex.getLocalizedMessage().contains("at least one resource has a reference to this resource"),
ex.getLocalizedMessage());
}
}

@Test
public void testNoDuplicateConstraintReferences() {
Patient patient = new Patient();
Expand Down
Loading