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

Child artifact deletion and retire #111

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ This server currently supports the following CRUD operations:
- Publishable:
- Supports the _Publishable_ minimum write capability _retire_
- Artifact must be in active status and may only change the status to retired and update the date (and other metadata appropriate to indicate retired status)
- Note: While it is not yet mentioned in the specification, _retire_ supports the update of an artifact as well as any resources it is composed of, recursively
- Authoring:
- Supports the additional _Authoring_ capability _revise_
- Artifact must be in (and remain in) draft status
Expand All @@ -121,9 +122,11 @@ This server currently supports the following CRUD operations:
- Publishable:
- Supports the _Publishable_ minimum write capability _archive_
- Artifact must be in retired status
- Note: While it is not yet mentioned in the specification, _archive_ supports the deletion of an artifact in retired status as well as any resources it is composed of, recursively
- Authoring:
- Supports the additional _Authoring_ capability _withdraw_
- Artifact must be in draft status
- Note: While it is not yet mentioned in the specification, _withdraw_ supports the deletion of an artifact in draft status as well as any resources it is composed of, recursively

### Search

Expand Down
40 changes: 27 additions & 13 deletions service/src/db/dbOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,6 @@ export async function updateResource(id: string, data: FhirArtifact, resourceTyp
}
}

/**
* Searches for a document for a resource and deletes it if found
*/
export async function deleteResource(id: string, resourceType: string) {
const collection = Connection.db.collection(resourceType);
logger.debug(`Finding and deleting ${resourceType}/${id} from database`);
const results = await collection.deleteOne({ id });
if (results.deletedCount === 1) {
return { id, deleted: true };
}
return { id, deleted: false };
}

/**
* Inserts a parent artifact and all of its children (if applicable) in a batch
* Error message depends on whether draft or cloned artifacts are being inserted
Expand Down Expand Up @@ -250,6 +237,33 @@ export async function batchUpdate(artifacts: FhirArtifact[], action: string) {
return results;
}

/**
* Deletes a parent artifact and all of its children (if applicable) in a batch
* Error message depends on whether the resource is being archived or withdrawn
*/
export async function batchDelete(artifacts: FhirArtifact[], action: string) {
let error = null;
const results: FhirArtifact[] = [];
const deleteSession = Connection.connection?.startSession();
try {
await deleteSession?.withTransaction(async () => {
for (const artifact of artifacts) {
const collection = await Connection.db.collection(artifact.resourceType);
await collection.deleteOne({ id: artifact.id }, { session: deleteSession });
results.push(artifact);
}
});
console.log(`Batch ${action} transaction committed.`);
} catch (err) {
console.log(`Batch ${action} transaction failed: ` + err);
error = err;
} finally {
await deleteSession?.endSession();
}
if (error) throw error;
return results;
}

function handlePossibleDuplicateKeyError(e: any, resourceType?: string) {
if (e instanceof MongoServerError && e.code === 11000) {
let errorString = 'Resource with primary identifiers already in repository.';
Expand Down
50 changes: 43 additions & 7 deletions service/src/services/LibraryService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { loggers, RequestArgs, RequestCtx } from '@projecttacoma/node-fhir-server-core';
import {
batchDelete,
batchInsert,
batchUpdate,
createResource,
deleteResource,
findDataRequirementsWithQuery,
findResourceById,
findResourceCountWithQuery,
Expand Down Expand Up @@ -161,6 +161,9 @@ export class LibraryService implements Service<CRMIShareableLibrary> {
}

/**
* retire: only updates a library with status 'active' to have status 'retired'
* and any resources it is composed of
*
* result of sending a PUT request to {BASE_URL}/4_0_1/Library/{id}
* updates the library with the passed in id using the passed in data
* or creates a library with passed in id if it does not exist in the database
Expand All @@ -175,27 +178,60 @@ export class LibraryService implements Service<CRMIShareableLibrary> {
if (resource.id !== args.id) {
throw new BadRequestError('Argument id must match request body id for PUT request');
}
// note: the distance between this database call and the update resource call, could cause a race condition
const oldResource = (await findResourceById(resource.id, resource.resourceType)) as CRMIShareableLibrary | null;
// note: the distance between this database call and the update resource call, could cause a race condition
if (oldResource) {
checkFieldsForUpdate(resource, oldResource);

if (resource.status === 'retired') {
// because we are changing the status/date of artifact, we want to also do so for
// any resources it is composed of
const children = oldResource.relatedArtifact ? await getChildren(oldResource.relatedArtifact) : [];
children.forEach(child => {
child.status = resource.status;
child.date = resource.date;
});

// now we want to batch update the retired parent Library and any of its children
await batchUpdate([resource, ...(await Promise.all(children))], 'retire');

return { id: args.id, created: false };
}
} else {
checkFieldsForCreate(resource);
}

return updateResource(args.id, resource, 'Library');
}

/**
* archive: deletes a library and any resources it is composed of with 'retried' status
* withdraw: deletes a library and any resources it is composed of with 'draft' status
* result of sending a DELETE request to {BASE_URL}/4_0_1/Library/{id}
* deletes the library with the passed in id if it exists in the database
* as well as all resources it is composed of
* requires id parameter
*/
async remove(args: RequestArgs) {
const resource = (await findResourceById(args.id, 'Library')) as CRMIShareableLibrary | null;
if (!resource) {
throw new ResourceNotFoundError(`Existing resource not found with id ${args.id}`);
const library = await findResourceById<CRMIShareableLibrary>(args.id, 'Library');
if (!library) {
throw new ResourceNotFoundError(`No resource found in collection: Library, with id: ${args.id}`);
}
checkFieldsForDelete(resource);
return deleteResource(args.id, 'Library');
checkFieldsForDelete(library);
const archiveOrWithdraw = library.status === 'retired' ? 'archive' : 'withdraw';
checkIsOwned(
library,
`Child artifacts cannot be directly ${archiveOrWithdraw === 'archive' ? 'archived' : 'withdrawn'}`
);

// recursively get any child artifacts from the artifact if they exist
const children = library.relatedArtifact ? await getChildren(library.relatedArtifact) : [];

// now we want to batch delete (archive/withdraw) the Library artifact and any of its children
const newDeletes = await batchDelete([library, ...children], archiveOrWithdraw);

// we want to return a Bundle containing the deleted artifacts
return createBatchResponseBundle(newDeletes);
}

/**
Expand Down
50 changes: 42 additions & 8 deletions service/src/services/MeasureService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { loggers, RequestArgs, RequestCtx } from '@projecttacoma/node-fhir-server-core';
import {
batchDelete,
batchInsert,
batchUpdate,
createResource,
deleteResource,
findDataRequirementsWithQuery,
findResourceById,
findResourceCountWithQuery,
Expand Down Expand Up @@ -166,8 +166,10 @@ export class MeasureService implements Service<CRMIShareableMeasure> {
}

/**
* result of sending a PUT request to {BASE_URL}/4_0_1/Measure/{id}
* updates the measure with the passed in id using the passed in data
* retire: only updates a measure with status 'active' to have status 'retired'
* and any resource it is composed of
*
* Otherwise, updates the measure with the passed in id using the passed in data
* or creates a measure with passed in id if it does not exist in the database
*/
async update(args: RequestArgs, { req }: RequestCtx) {
Expand All @@ -184,6 +186,21 @@ export class MeasureService implements Service<CRMIShareableMeasure> {
// note: the distance between this database call and the update resource call, could cause a race condition
if (oldResource) {
checkFieldsForUpdate(resource, oldResource);

if (resource.status === 'retired') {
// because we are changing the status/date of artifact, we want to also do so for
// any resources it is composed of
const children = oldResource.relatedArtifact ? await getChildren(oldResource.relatedArtifact) : [];
children.forEach(child => {
child.status = resource.status;
child.date = resource.date;
});

// now we want to batch update the retired parent Measure and any of its children
await batchUpdate([resource, ...(await Promise.all(children))], 'retire');

return { id: args.id, created: false };
}
} else {
checkFieldsForCreate(resource);
}
Expand All @@ -192,16 +209,33 @@ export class MeasureService implements Service<CRMIShareableMeasure> {
}

/**
* archive: deletes a measure and any resources it is composed of with 'retired' status
* withdraw: deletes a measure and any resources it is composed of with 'draft' status
* result of sending a DELETE request to {BASE_URL}/4_0_1/measure/{id}
* deletes the measure with the passed in id if it exists in the database
* as well as all resources it is composed of
* requires id parameter
*/
async remove(args: RequestArgs) {
const resource = (await findResourceById(args.id, 'Measure')) as CRMIShareableMeasure | null;
if (!resource) {
throw new ResourceNotFoundError(`Existing resource not found with id ${args.id}`);
const measure = await findResourceById<CRMIShareableMeasure>(args.id, 'Measure');
if (!measure) {
throw new ResourceNotFoundError(`No resource found in collection: Measure, with id: ${args.id}`);
}
checkFieldsForDelete(resource);
return deleteResource(args.id, 'Measure');
checkFieldsForDelete(measure);
const archiveOrWithdraw = measure.status === 'retired' ? 'archive' : 'withdraw';
checkIsOwned(
measure,
`Child artifacts cannot be directly ${archiveOrWithdraw === 'archive' ? 'archived' : 'withdrawn'}`
);

// recursively get any child artifacts from the artifact if they exist
const children = measure.relatedArtifact ? await getChildren(measure.relatedArtifact) : [];

// now we want to batch delete (archive/withdraw) the Measure artifact and any of its children
const newDeletes = await batchDelete([measure, ...children], archiveOrWithdraw);

// we want to return a Bundle containing the deleted artifacts
return createBatchResponseBundle(newDeletes);
}

/**
Expand Down
Loading