From 55d6924c3450d91d7d0819fd2713b174c0ed052c Mon Sep 17 00:00:00 2001 From: LaurenD Date: Wed, 2 Oct 2024 12:59:55 -0400 Subject: [PATCH] active review workflow and lint/prettier --- app/src/pages/[resourceType].tsx | 6 +- app/src/pages/authoring/index.tsx | 1 - app/src/pages/review/[resourceType]/[id].tsx | 28 +++-- app/src/server/trpc/routers/draft.ts | 107 +++++++++++-------- app/src/server/trpc/routers/service.ts | 25 +++-- app/src/util/versionUtils.ts | 6 +- service/src/requestSchemas.ts | 8 +- 7 files changed, 102 insertions(+), 79 deletions(-) diff --git a/app/src/pages/[resourceType].tsx b/app/src/pages/[resourceType].tsx index ebdb192..fb54573 100644 --- a/app/src/pages/[resourceType].tsx +++ b/app/src/pages/[resourceType].tsx @@ -4,7 +4,6 @@ import { ArtifactResourceType, ResourceInfo, FhirArtifact } from '@/util/types/f import ResourceCards from '@/components/ResourceCards'; import Link from 'next/link'; import { extractResourceInfo } from '@/util/resourceCardUtils'; -import { trpc } from '@/util/trpc'; /** * Component which displays list of all resources of some type as passed in by (serverside) props @@ -54,8 +53,9 @@ export const getServerSideProps: GetServerSideProps<{ const checkedResourceType = resourceType as ArtifactResourceType; // Fetch resource data with the _elements parameter so we only get the elements that we need - // TODO: send this through a procedure instead? - const res = await fetch(`${process.env.MRS_SERVER}/${checkedResourceType}?_elements=id,identifier,name,url,version&status=active`); + const res = await fetch( + `${process.env.MRS_SERVER}/${checkedResourceType}?_elements=id,identifier,name,url,version&status=active` + ); const bundle = (await res.json()) as fhir4.Bundle; if (!bundle.entry) { // Measure Repository should not provide a bundle without an entry diff --git a/app/src/pages/authoring/index.tsx b/app/src/pages/authoring/index.tsx index 2cb9394..4627118 100644 --- a/app/src/pages/authoring/index.tsx +++ b/app/src/pages/authoring/index.tsx @@ -10,7 +10,6 @@ import { createStyles, Loader } from '@mantine/core'; -import { v4 as uuidv4 } from 'uuid'; import { useState } from 'react'; import { trpc } from '../../util/trpc'; import { MeasureSkeleton, LibrarySkeleton } from '@/util/authoringFixtures'; diff --git a/app/src/pages/review/[resourceType]/[id].tsx b/app/src/pages/review/[resourceType]/[id].tsx index 6ca5d26..4a8824f 100644 --- a/app/src/pages/review/[resourceType]/[id].tsx +++ b/app/src/pages/review/[resourceType]/[id].tsx @@ -5,7 +5,6 @@ import { Box, Button, Center, - Checkbox, Divider, Grid, Group, @@ -56,11 +55,9 @@ export default function CommentPage() { } }); - - const utils = trpc.useUtils(); // Currently we can only update draft artifact resources. TODO: should we enable active resource review? const resourceReview = trpc.draft.reviewDraft.useMutation({ - onSuccess: (data) => { + onSuccess: data => { notifications.show({ title: 'Review successfully added!', message: `Review successfully added to ${resourceType}/${resourceID}`, @@ -75,8 +72,11 @@ export default function CommentPage() { color: 'green' }); }); - utils.draft.getDrafts.invalidate(); - ctx.draft.getDraftById.invalidate(); + if (authoring) { + ctx.draft.getDraftById.invalidate(); + } else { + ctx.service.getArtifactById.invalidate(); + } }, onError: e => { notifications.show({ @@ -229,15 +229,13 @@ export default function CommentPage() { } setIsLoading(false); }, 1000); - if (authoring === 'true') { - resourceReview.mutate({ - resourceType: resourceType as ArtifactResourceType, - id: resourceID as string, - type: form.values.type, - summary: form.values.comment, - author: form.values.name - }); - } + resourceReview.mutate({ + resourceType: resourceType as ArtifactResourceType, + id: resourceID as string, + type: form.values.type, + summary: form.values.comment, + author: form.values.name + }); } }} > diff --git a/app/src/server/trpc/routers/draft.ts b/app/src/server/trpc/routers/draft.ts index eb44f68..3bf67ba 100644 --- a/app/src/server/trpc/routers/draft.ts +++ b/app/src/server/trpc/routers/draft.ts @@ -22,16 +22,16 @@ export const draftRouter = router({ } as const; }), - getDrafts: publicProcedure - .input(z.enum(['Measure', 'Library']).optional()) - .query(async ({ input }) => { - if (!input) return null; - const artifactBundle = await fetch( - `${process.env.MRS_SERVER}/${input}?status=draft` - ).then(resArtifacts => resArtifacts.json() as Promise>); - const artifactList = artifactBundle.entry?.filter(entry => entry.resource).map(entry => entry.resource as FhirArtifact); - return artifactList; - }), + getDrafts: publicProcedure.input(z.enum(['Measure', 'Library']).optional()).query(async ({ input }) => { + if (!input) return null; + const artifactBundle = await fetch(`${process.env.MRS_SERVER}/${input}?status=draft`).then( + resArtifacts => resArtifacts.json() as Promise> + ); + const artifactList = artifactBundle.entry + ?.filter(entry => entry.resource) + .map(entry => entry.resource as FhirArtifact); + return artifactList; + }), getDraftById: publicProcedure .input(z.object({ id: z.string().optional(), resourceType: z.enum(['Measure', 'Library']).optional() })) @@ -47,12 +47,12 @@ export const draftRouter = router({ const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}`, { method: 'POST', headers: { - 'Accept': 'application/json+fhir', + Accept: 'application/json+fhir', 'Content-Type': 'application/json+fhir' }, body: JSON.stringify(input.draft) }); - if(res.status === 201){ + if (res.status === 201) { // get resultant id from location header return { draftId: res.headers.get('Location')?.split('/')[2] as string }; } @@ -61,29 +61,27 @@ export const draftRouter = router({ }), updateDraft: publicProcedure - .input( - z.object({ resourceType: z.enum(['Measure', 'Library']), values: z.any(), id: z.string() }) - ) + .input(z.object({ resourceType: z.enum(['Measure', 'Library']), values: z.any(), id: z.string() })) .mutation(async ({ input }) => { const raw = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}`); const resource: FhirArtifact = await raw.json(); resource.url = input.values.url; - resource.identifier = [{system: input.values.identifierSystem, value:input.values.identifierValue}]; + resource.identifier = [{ system: input.values.identifierSystem, value: input.values.identifierValue }]; resource.name = input.values.name; resource.title = input.values.title; resource.description = input.values.description; - if(input.resourceType === 'Measure'){ + if (input.resourceType === 'Measure') { (resource as CRMIShareableMeasure).library = input.values.library; } const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}`, { method: 'PUT', headers: { - 'Accept': 'application/json+fhir', + Accept: 'application/json+fhir', 'Content-Type': 'application/json+fhir' }, body: JSON.stringify(resource) }); - if(res.status === 200){ + if (res.status === 200) { return {}; } const outcome: OperationOutcome = await res.json(); @@ -96,31 +94,30 @@ export const draftRouter = router({ const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}`, { method: 'DELETE', headers: { - 'Accept': 'application/json+fhir', + Accept: 'application/json+fhir', 'Content-Type': 'application/json+fhir' } }); - if(res.status === 204){ + if (res.status === 204) { const resData = { draftId: input.id, resourceType: input.resourceType, children: [] as FhirArtifact[] }; // TODO: update to use server-side batch delete to find child information once it returns a 200/bundle - // const resBundle: Bundle = await res.json(); - // if (!resBundle.entry || resBundle.entry.length === 0) { - // throw new Error(`No deletions found from deleting ${input.resourceType}, id ${input.id}`); - // } - // resBundle.entry.forEach(e => { - // if(e.resource?.extension?.find(ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean)){ - // resData.children.push(e.resource); - // } - // }); + // const resBundle: Bundle = await res.json(); + // if (!resBundle.entry || resBundle.entry.length === 0) { + // throw new Error(`No deletions found from deleting ${input.resourceType}, id ${input.id}`); + // } + // resBundle.entry.forEach(e => { + // if(e.resource?.extension?.find(ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean)){ + // resData.children.push(e.resource); + // } + // }); return resData; } const outcome: OperationOutcome = await res.json(); throw new Error(`Received ${res.status} error on delete: ${outcome.issue[0].details?.text}`); }), - cloneParent: publicProcedure .input(z.object({ id: z.string(), resourceType: z.enum(['Measure', 'Library']) })) .mutation(async ({ input }) => { @@ -128,7 +125,9 @@ export const draftRouter = router({ const resource = (await raw.json()) as FhirArtifact; const version = await calculateVersion(input.resourceType, resource.url, resource.version); // $clone with calculated version - const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$clone?version=${version}&url=${resource.url}`); + const res = await fetch( + `${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$clone?version=${version}&url=${resource.url}` + ); if (res.status !== 200) { const outcome: OperationOutcome = await res.json(); @@ -141,11 +140,15 @@ export const draftRouter = router({ throw new Error(`No clones found from cloning ${input.resourceType}, id ${input.id}`); } - const resData = { cloneId: undefined as string|undefined, children: [] as FhirArtifact[] }; + const resData = { cloneId: undefined as string | undefined, children: [] as FhirArtifact[] }; resBundle.entry.forEach(e => { - if(e.resource?.extension?.find(ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean)){ + if ( + e.resource?.extension?.find( + ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean + ) + ) { resData.children.push(e.resource); - }else{ + } else { resData.cloneId = e.resource?.id; } }); @@ -154,22 +157,30 @@ export const draftRouter = router({ // passes in type, summary, and author from user (set date and target automatically) reviewDraft: publicProcedure - .input(z.object({ id: z.string(), resourceType: z.enum(['Measure', 'Library']), type: z.string(), summary: z.string(), author: z.string() })) + .input( + z.object({ + id: z.string(), + resourceType: z.enum(['Measure', 'Library']), + type: z.string(), + summary: z.string(), + author: z.string() + }) + ) .mutation(async ({ input }) => { const raw = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}`); const resource = (await raw.json()) as FhirArtifact; const date = new Date().toISOString(); const canonical = `${resource.url}|${resource.version}`; - + const params = new URLSearchParams({ - 'reviewDate': date, - 'artifactAssessmentType': input.type, - 'artifactAssessmentSummary': input.summary, - 'artifactAssessmentTarget': canonical, - 'artifactAssessmentAuthor': input.author + reviewDate: date, + artifactAssessmentType: input.type, + artifactAssessmentSummary: input.summary, + artifactAssessmentTarget: canonical, + artifactAssessmentAuthor: input.author }); const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$review?${params}`); - + if (res.status !== 200) { const outcome: OperationOutcome = await res.json(); throw new Error(`Received ${res.status} error on $review: ${outcome.issue[0].details?.text}`); @@ -181,11 +192,15 @@ export const draftRouter = router({ throw new Error(`No updated resources found from reviewing ${input.resourceType}, id ${input.id}`); } - const resData = { reviewId: undefined as string|undefined, children: [] as FhirArtifact[] }; + const resData = { reviewId: undefined as string | undefined, children: [] as FhirArtifact[] }; resBundle.entry.forEach(e => { - if(e.resource?.extension?.find(ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean)){ + if ( + e.resource?.extension?.find( + ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean + ) + ) { resData.children.push(e.resource); - }else{ + } else { resData.reviewId = e.resource?.id; } }); diff --git a/app/src/server/trpc/routers/service.ts b/app/src/server/trpc/routers/service.ts index 214bbf8..069f060 100644 --- a/app/src/server/trpc/routers/service.ts +++ b/app/src/server/trpc/routers/service.ts @@ -89,11 +89,15 @@ export const serviceRouter = router({ throw new Error(`No drafts found from drafting ${input.resourceType}, id ${input.id}`); } - const resData = { draftId: undefined as string|undefined, children: [] as FhirArtifact[] }; + const resData = { draftId: undefined as string | undefined, children: [] as FhirArtifact[] }; resBundle.entry.forEach(e => { - if(e.resource?.extension?.find(ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean)){ + if ( + e.resource?.extension?.find( + ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean + ) + ) { resData.children.push(e.resource); - }else{ + } else { resData.draftId = e.resource?.id; } }); @@ -103,8 +107,9 @@ export const serviceRouter = router({ releaseParent: publicProcedure .input(z.object({ resourceType: z.enum(['Measure', 'Library']), id: z.string(), version: z.string() })) .mutation(async ({ input }) => { - - const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$release?releaseVersion=${input.version}&versionBehavior=force`); + const res = await fetch( + `${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$release?releaseVersion=${input.version}&versionBehavior=force` + ); if (res.status !== 200) { const outcome: OperationOutcome = await res.json(); @@ -122,11 +127,15 @@ export const serviceRouter = router({ id: string; }[] = [{ resourceType: input.resourceType, id: input.id }]; //start with parent and add children resBundle.entry.forEach(e => { - if(e.resource?.extension?.find(ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean)){ - released.push({resourceType: e.resource.resourceType, id: e.resource.id}); + if ( + e.resource?.extension?.find( + ext => ext.url === 'http://hl7.org/fhir/StructureDefinition/artifact-isOwned' && ext.valueBoolean + ) + ) { + released.push({ resourceType: e.resource.resourceType, id: e.resource.id }); } }); - + return { location: `/${input.resourceType}/${input.id}`, released: released, status: res.status, error: null }; }) }); diff --git a/app/src/util/versionUtils.ts b/app/src/util/versionUtils.ts index 49b0f8f..045753c 100644 --- a/app/src/util/versionUtils.ts +++ b/app/src/util/versionUtils.ts @@ -7,8 +7,7 @@ import { Bundle } from 'fhir/r4'; * It increments the version if the artifact has one or sets it to * 0.0.1 if it does not */ -export async function calculateVersion(resourceType: 'Library'|'Measure', url: string, version: string) { - +export async function calculateVersion(resourceType: 'Library' | 'Measure', url: string, version: string) { let newVersion = '0.0.1'; // initial version coercion and increment @@ -94,8 +93,7 @@ function checkVersionFormat(version: string): boolean { // in order to decide whether to increment the version further async function getResourceByUrl(url: string, version: string, resourceType: string) { const res = await fetch(`${process.env.MRS_SERVER}/${resourceType}?url=${url}&version=${version}`); - const bundle:Bundle = await res.json(); + const bundle: Bundle = await res.json(); // return first entry found in bundle return bundle.entry && bundle.entry.length > 0 ? bundle.entry[0].resource : null; } - diff --git a/service/src/requestSchemas.ts b/service/src/requestSchemas.ts index e75dbc3..1526c63 100644 --- a/service/src/requestSchemas.ts +++ b/service/src/requestSchemas.ts @@ -205,7 +205,9 @@ export const ApproveArgs = z artifactAssessmentSummary: z.string().optional(), artifactAssessmentTarget: checkUri.optional(), artifactAssessmentRelatedArtifact: checkUri.optional(), - artifactAssessmentAuthor: z.union([z.object({ reference: z.string()}).transform((val) => val.reference), z.string()]).optional() //object from POST or string from GET + artifactAssessmentAuthor: z + .union([z.object({ reference: z.string() }).transform(val => val.reference), z.string()]) + .optional() //object from POST or string from GET }) .strict() .superRefine(catchInvalidParams([catchMissingId, catchMissingTypeAndSummary])); @@ -220,7 +222,9 @@ export const ReviewArgs = z artifactAssessmentSummary: z.string().optional(), artifactAssessmentTarget: checkUri.optional(), artifactAssessmentRelatedArtifact: checkUri.optional(), - artifactAssessmentAuthor: z.union([z.object({ reference: z.string()}).transform((val) => val.reference), z.string()]).optional() //object from POST or string from GET + artifactAssessmentAuthor: z + .union([z.object({ reference: z.string() }).transform(val => val.reference), z.string()]) + .optional() //object from POST or string from GET }) .strict() .superRefine(catchInvalidParams([catchMissingId, catchMissingTypeAndSummary]));