diff --git a/app/src/components/ResourceInfoCard.tsx b/app/src/components/ResourceInfoCard.tsx index 173407f..59c5dbd 100644 --- a/app/src/components/ResourceInfoCard.tsx +++ b/app/src/components/ResourceInfoCard.tsx @@ -39,24 +39,47 @@ export default function ResourceInfoCard({ resourceInfo, authoring }: ResourceIn const ctx = trpc.useContext(); const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false); - const deleteMutation = trpc.draft.deleteDraft.useMutation({ - onSuccess: () => { - notifications.show({ - title: `Draft ${resourceInfo.resourceType} Deleted!`, - message: `Draft ${resourceInfo.resourceType}/${resourceInfo.id} successfully deleted`, - icon: , - color: 'green' + const successNotification = (resourceType: string, childArtifact: boolean, idOrUrl?: string) => { + let message; + if (childArtifact) { + message = `Draft of child ${resourceType} artifact of url ${idOrUrl} successfully deleted`; + } else { + message = `Draft of ${resourceType}/${idOrUrl} successfully deleted`; + } + notifications.show({ + title: `${resourceType} Deleted!`, + message: message, + icon: , + color: 'green' + }); + ctx.draft.getDraftCounts.invalidate(); + }; + + const errorNotification = (resourceType: string, errorMessage: string, childArtifact: boolean, idOrUrl?: string) => { + let message; + if (childArtifact) { + message = `Attempt to delete draft of child ${resourceType} artifact of url ${idOrUrl} failed with message: ${errorMessage}`; + } else { + message = `Attempt to delete draft of ${resourceType}/${idOrUrl} failed with message: ${errorMessage}`; + } + notifications.show({ + title: `${resourceType} Deletion Failed!`, + message: message, + icon: , + color: 'red' + }); + }; + + const deleteMutation = trpc.draft.deleteParent.useMutation({ + onSuccess: (data, variables) => { + successNotification(variables.resourceType, false, variables.id); + data.children.forEach(c => { + successNotification(c.resourceType, true, c.url); }); - ctx.draft.getDraftCounts.invalidate(); ctx.draft.getDrafts.invalidate(); }, - onError: e => { - notifications.show({ - title: `Draft ${resourceInfo.resourceType} Deletion Failed!`, - message: `Attempt to delete draft of ${resourceInfo.resourceType}/${resourceInfo.id} failed with message: ${e.message}`, - icon: , - color: 'red' - }); + onError: (e, variables) => { + errorNotification(variables.resourceType, e.message, false, variables.id); } }); @@ -67,7 +90,7 @@ export default function ResourceInfoCard({ resourceInfo, authoring }: ResourceIn onClose={() => setIsConfirmationModalOpen(false)} modalText={`This will delete draft ${resourceInfo.resourceType} "${ resourceInfo.name ? resourceInfo.name : `${resourceInfo.resourceType}/${resourceInfo.id}` - }" permanently.`} + }" and any child artifacts permanently.`} onConfirm={() => { deleteMutation.mutate({ resourceType: resourceInfo.resourceType, @@ -130,19 +153,28 @@ export default function ResourceInfoCard({ resourceInfo, authoring }: ResourceIn - {authoring && ( - - setIsConfirmationModalOpen(true)} - > - - - - )} + {authoring && + (resourceInfo.isChild ? ( + + + + + + + + ) : ( + + setIsConfirmationModalOpen(true)} + > + + + + ))} diff --git a/app/src/server/db/dbOperations.ts b/app/src/server/db/dbOperations.ts index ced70a1..41ad233 100644 --- a/app/src/server/db/dbOperations.ts +++ b/app/src/server/db/dbOperations.ts @@ -49,17 +49,15 @@ export async function batchCreateDraft(drafts: FhirArtifact[]) { const client = await clientPromise; const session = client.startSession(); try { - session.startTransaction(); - const inserts = drafts.map(draft => { - const collection = client.db().collection(draft.resourceType); - return collection.insertOne(draft as any, { session }); + await session.withTransaction(async () => { + for (const draft of drafts) { + const collection = await client.db().collection(draft.resourceType); + await collection.insertOne(draft as any, { session }); + } }); - await Promise.all(inserts); - await session.commitTransaction(); console.log('Batch drafts transaction committed.'); } catch (err) { console.error('Batch drafts transaction failed: ' + err); - await session.abortTransaction(); error = err; } finally { await session.endSession(); @@ -93,3 +91,27 @@ export async function deleteDraft(resourceType: ArtifactResourceType, id: string const collection = client.db().collection(resourceType); return collection.deleteOne({ id }); } + +/** + * Deletes a parent artifact and all of its children (if applicable) in a batch + */ +export async function batchDeleteDraft(drafts: FhirArtifact[]) { + let error = null; + const client = await clientPromise; + const deleteSession = client.startSession(); + try { + await deleteSession.withTransaction(async () => { + for (const draft of drafts) { + const collection = await client.db().collection(draft.resourceType); + await collection.deleteOne({ id: draft.id }, { session: deleteSession }); + } + }); + console.log('Batch delete transaction committed.'); + } catch (err) { + console.error('Batch delete transaction failed: ' + err); + error = err; + } finally { + await deleteSession.endSession(); + } + if (error) throw error; +} diff --git a/app/src/server/trpc/routers/draft.ts b/app/src/server/trpc/routers/draft.ts index bf3ad08..a0bc75e 100644 --- a/app/src/server/trpc/routers/draft.ts +++ b/app/src/server/trpc/routers/draft.ts @@ -7,9 +7,11 @@ import { getDraftCount, updateDraft, deleteDraft, - getDraftByUrl + getDraftByUrl, + batchDeleteDraft } from '../../db/dbOperations'; import { publicProcedure, router } from '../trpc'; +import { getDraftChildren } from '@/util/serviceUtils'; /** one big router with resource types passed in */ export const draftRouter = router({ @@ -67,5 +69,33 @@ export const draftRouter = router({ .mutation(async ({ input }) => { const res = await deleteDraft(input.resourceType, input.id); return { draftId: input.id, resourceType: input.resourceType, ...res }; + }), + + deleteParent: publicProcedure + .input(z.object({ id: z.string(), resourceType: z.enum(['Measure', 'Library']) })) + .mutation(async ({ input }) => { + // get the parent draft artifact by id + const draftRes = await getDraftById(input.id, input.resourceType); + + if (!draftRes) { + throw new Error(`No draft artifact found for resourceType ${input.resourceType}, id ${input.id}`); + } + + // recursively get any child artifacts from the artifact if they exist + const children = draftRes?.relatedArtifact ? await getDraftChildren(draftRes.relatedArtifact) : []; + + const childDrafts = children.map(async child => { + const draft = await getDraftByUrl(child.url, child.version, child.resourceType); + if (!draft) { + throw new Error('No artifacts found in search'); + } + return draft; + }); + + const draftArtifacts = [draftRes].concat(await Promise.all(childDrafts)); + + await batchDeleteDraft(draftArtifacts); + + return { draftId: draftRes.id, children: children }; }) }); diff --git a/app/src/util/modifyResourceFields.ts b/app/src/util/modifyResourceFields.ts index dd6db7c..4828757 100644 --- a/app/src/util/modifyResourceFields.ts +++ b/app/src/util/modifyResourceFields.ts @@ -11,6 +11,7 @@ import { getDraftByUrl } from '@/server/db/dbOperations'; export async function modifyResourceToDraft(artifact: FhirArtifact) { artifact.id = uuidv4(); artifact.status = 'draft'; + let count = 0; // initial version coercion and increment // we can only increment artifacts whose versions are either semantic, can be coerced @@ -35,7 +36,6 @@ export async function modifyResourceToDraft(artifact: FhirArtifact) { // check for existing draft with proposed version let existingDraft = await getDraftByUrl(artifact.url, artifact.version, artifact.resourceType); // only increment a limited number of times - let count = 0; while (existingDraft && count < 10) { // increment artifact version const incVersion = inc(artifact.version, 'patch'); @@ -56,7 +56,11 @@ export async function modifyResourceToDraft(artifact: FhirArtifact) { ) ) { const url = ra.resource.split('|')[0]; - const version = ra.resource.split('|')[1]; + let version = ra.resource.split('|')[1]; + while (count !== 0) { + version = incrementArtifactVersion(version); + count--; + } ra.resource = url + '|' + incrementArtifactVersion(version); } }); diff --git a/app/src/util/resourceCardUtils.ts b/app/src/util/resourceCardUtils.ts index 2c3f4d3..b4c867c 100644 --- a/app/src/util/resourceCardUtils.ts +++ b/app/src/util/resourceCardUtils.ts @@ -19,7 +19,10 @@ export function extractResourceInfo(resource: FhirArtifact) { name: resource.name ?? null, url: resource.url ?? null, version: resource.version ?? null, - status: resource.status ?? null + status: resource.status ?? null, + isChild: !!resource.extension?.find( + ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean === true + ) }; return resourceInfo; } diff --git a/app/src/util/types/fhir.ts b/app/src/util/types/fhir.ts index 1919692..abdd7f1 100644 --- a/app/src/util/types/fhir.ts +++ b/app/src/util/types/fhir.ts @@ -24,4 +24,5 @@ export interface ResourceInfo { url: string | null; version: string; status: string | null; + isChild: boolean; }