Skip to content

Commit

Permalink
Cleanup and version calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
lmd59 committed Sep 21, 2024
1 parent cc01fd3 commit 1987313
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 168 deletions.
13 changes: 3 additions & 10 deletions app/src/components/ReleaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,17 @@ export default function ReleaseModal({ open = true, onClose, id, resourceType }:
color: 'red'
});
} else {
data.deletable?.forEach(d => {
data.released?.forEach(r => {
notifications.show({
title: `Draft ${d.resourceType} released!`,
message: `Draft ${d.resourceType}/${d.id} successfully released to the Publishable Measure Repository!`,
title: `Draft ${r.resourceType} released!`,
message: `Draft ${r.resourceType}/${r.id} successfully released!`,
icon: <CircleCheck />,
color: 'green'
});
});
ctx.draft.getDraftCounts.invalidate();
ctx.draft.getDrafts.invalidate();
router.push(data.location);

data.deletable?.forEach(d => {
deleteMutation.mutate({
resourceType: d.resourceType,
id: d.id
});
});
}
onClose();
}
Expand Down
4 changes: 3 additions & 1 deletion app/src/pages/[resourceType].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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
Expand Down Expand Up @@ -53,7 +54,8 @@ 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
const res = await fetch(`${process.env.MRS_SERVER}/${checkedResourceType}?_elements=id,identifier,name,url,version`);
// 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 bundle = (await res.json()) as fhir4.Bundle<FhirArtifact>;
if (!bundle.entry) {
// Measure Repository should not provide a bundle without an entry
Expand Down
35 changes: 4 additions & 31 deletions app/src/server/trpc/routers/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CRMIShareableMeasure, FhirArtifact } from '@/util/types/fhir';
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
import { Bundle, OperationOutcome } from 'fhir/r4';
import { calculateVersion } from '@/util/versionUtils';

/**
* Endpoints dealing with outgoing calls to the central measure repository service to handle draft measures
Expand Down Expand Up @@ -51,34 +52,6 @@ export const draftRouter = router({
return resource as FhirArtifact;
}),

// getDraftByUrl: publicProcedure
// .input(
// z.object({
// url: z.string().optional(),
// version: z.string().optional(),
// resourceType: z.enum(['Measure', 'Library']).optional()
// })
// )
// .query(async ({ input }) => {
// input.url && input.resourceType && input.version
// ? getDraftByUrl<FhirArtifact>(input.url, input.version, input.resourceType)
// : null;
// }),
// TODO: double check this isn't needed
// getDraftByUrl: publicProcedure
// .input(z.object({ resourceType: z.enum(['Measure', 'Library']), url: z.string(), version: z.string() }))
// .query(async ({ input }) => {
// const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}?url=${input.url}&version=${input.version}`);
// const bundle:Bundle<FhirArtifact> = await res.json();
// // return first entry found in bundle
// return bundle.entry && bundle.entry.length > 0 ? bundle.entry[0].resource : null;
// }),




// TODO: fix below CRUD

createDraft: publicProcedure
.input(z.object({ resourceType: z.enum(['Measure', 'Library']), draft: z.any() }))
.mutation(async ({ input }) => {
Expand Down Expand Up @@ -163,9 +136,9 @@ export const draftRouter = router({
cloneParent: publicProcedure
.input(z.object({ id: z.string(), resourceType: z.enum(['Measure', 'Library']) }))
.mutation(async ({ input }) => {

// TODO: use modifyResource logic to determine a reasonable version
const version = 'placeholder';
const raw = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}`);
const resource = (await raw.json()) as FhirArtifact;
const version = calculateVersion(resource);
// $clone with calculated version
const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$clone?version=${version}`);

Expand Down
18 changes: 9 additions & 9 deletions app/src/server/trpc/routers/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CRMIShareableLibrary, FhirArtifact } from '@/util/types/fhir';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { Bundle, OperationOutcome } from 'fhir/r4';
import { calculateVersion } from '@/util/versionUtils';

/**
* Endpoints dealing with outgoing calls to the central measure repository service to handle active measures
Expand Down Expand Up @@ -71,9 +72,9 @@ export const serviceRouter = router({
draftParent: publicProcedure
.input(z.object({ resourceType: z.enum(['Measure', 'Library']), id: z.string() }))
.mutation(async ({ input }) => {

// TODO: use modifyResource logic to determine a reasonable version
const version = 'placeholder';
const raw = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}`);
const resource = (await raw.json()) as FhirArtifact;
const version = calculateVersion(resource);
// $draft with calculated version
const res = await fetch(`${process.env.MRS_SERVER}/${input.resourceType}/${input.id}/$draft?version=${version}`);

Expand Down Expand Up @@ -108,7 +109,6 @@ export const serviceRouter = router({
if (res.status !== 200) {
const outcome: OperationOutcome = await res.json();
return { location: null, deletable: null, status: res.status, error: outcome.issue[0].details?.text };
// throw new Error(`Received ${res.status} error on $release: ${outcome.issue[0].details?.text}`);
}

const resBundle: Bundle<FhirArtifact> = await res.json();
Expand All @@ -117,17 +117,17 @@ export const serviceRouter = router({
throw new Error(`No released artifacts found from releasing ${input.resourceType}, id ${input.id}`);
}

const toDelete: {
const released: {
resourceType: 'Measure' | 'Library';
id: string;
}[] = [{ resourceType: input.resourceType, id: input.id }]; //start with parent and add children to be deleted upon success
}[] = [{ 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)){
toDelete.push({resourceType: e.resource.resourceType, id: e.resource.id});
released.push({resourceType: e.resource.resourceType, id: e.resource.id});
}
});

// TODO: construct location for router.push from input.id (parent id)
return { location: 'placeholder', deletable: toDelete, status: res.status, error: null };
// TODO: construct location for router.push from input.id (parent id) -> does a new location even makes sense??
return { location: `/${input.resourceType}/${input.id}`, released: released, status: res.status, error: null };
})
});
117 changes: 0 additions & 117 deletions app/src/util/modifyResourceFields.ts

This file was deleted.

101 changes: 101 additions & 0 deletions app/src/util/versionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { coerce, inc } from 'semver';
import { FhirArtifact } from './types/fhir';
import { Bundle } from 'fhir/r4';

/**
* Helper function that returns the new version for an artifact.
* It increments the version if the artifact has one or sets it to
* 0.0.1 if it does not
*/
export async function calculateVersion(artifact: FhirArtifact) {

let newVersion = '0.0.1';

// initial version coercion and increment
// we can only increment artifacts whose versions are either semantic, can be coerced
// to semantic, or are in x.x.xxx/x.xx.xxx format. Every other kind of version will become 0.0.1
if (artifact.version) {
const coerced = coerce(artifact.version);
const incVersion = coerced !== null ? inc(coerced, 'patch') : null;
if (checkVersionFormat(artifact.version)) {
// check that it is x.x.xxx/x.xx.xxx, format and increment manually
newVersion = incrementArtifactVersion(artifact.version);
} else if (incVersion !== null) {
// if possible, coerce the version to semver, and increment
newVersion = incVersion;
}
}

// subsequent version increments
if (artifact.url) {
let count = 0;
// check for existing draft with proposed version
let existingDraft = await getResourceByUrl(artifact.url, newVersion, artifact.resourceType);
// only increment a limited number of times
while (existingDraft && count < 10) {
// increment artifact version
const incVersion = inc(artifact.version, 'patch');
newVersion = incVersion ?? incrementArtifactVersion(newVersion);

existingDraft = await getResourceByUrl(artifact.url, newVersion, artifact.resourceType);
count++;
}
}
return newVersion;
}

/**
* Increments an artifact version that is in x.x.xxx/x.xx.xxx format
*/
function incrementArtifactVersion(version: string): string {
const stringParts = version.split('.');
const padMinor = stringParts[1].length === 2; //pad minor version if it's x.xx.xxx format
const versionParts = stringParts.map(Number);
// increment the patch version
versionParts[2]++;

// if the patch version reaches 1000, reset it to 0 and increment the minor version
if (versionParts[2] >= 1000) {
versionParts[2] = 0;
versionParts[1]++;
}

// if the minor version reaches 10/100 (depending on minor pad), reset it to 0 and increment the major version
const minorLimit = padMinor ? 100 : 10;
if (versionParts[1] >= minorLimit) {
versionParts[1] = 0;
versionParts[0]++;
}

let formattedPatch = versionParts[2].toString();
if (versionParts[2] < 100) {
formattedPatch = versionParts[2].toString().padStart(3, '0');
}

let formattedMinor = versionParts[1].toString();
if (padMinor && versionParts[1] < 10) {
formattedMinor = versionParts[1].toString().padStart(2, '0');
}

return `${versionParts[0]}.${formattedMinor}.${formattedPatch}`;
}

/**
* Takes in an artifact version and returns true if it is in x.x.xxx
* format and returns false if it is not
*/
function checkVersionFormat(version: string): boolean {
const format = /^\d\.\d{1,2}\.\d{3}$/;

return format.test(version);
}

// a function to check if the given url/version/resourceType exists on the server
// 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<FhirArtifact> = await res.json();
// return first entry found in bundle
return bundle.entry && bundle.entry.length > 0 ? bundle.entry[0].resource : null;
}

0 comments on commit 1987313

Please sign in to comment.