Skip to content

Commit

Permalink
release implementation (#109)
Browse files Browse the repository at this point in the history
* release implementation

* Add duplicate key release collision tests
  • Loading branch information
lmd59 authored Sep 3, 2024
1 parent f103013 commit 03319f5
Show file tree
Hide file tree
Showing 10 changed files with 668 additions and 6 deletions.
8 changes: 8 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ And takes the following optional parameters:
- artifactAssessmentRelatedArtifact
- artifactAssessmentAuthor

### Release

The Measure Repository Service Authoring Repository Service server supports the `Measure` and `Library` `$release` operations as defined by the [Canonical Resource Management Infrastructure IG](https://hl7.org/fhir/uv/crmi/OperationDefinition-crmi-release.html). It requires the following parameters:

- id
- version
- [versionBehavior](https://hl7.org/fhir/uv/crmi/ValueSet-crmi-release-version-behavior.html)

## License

Copyright 2022-2023 The MITRE Corporation
Expand Down
4 changes: 2 additions & 2 deletions service/scripts/dbSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ function modifyEntriesForUpload(entries: fhir4.BundleEntry<fhir4.FhirResource>[]
if (!updatedEntry.resource.id) {
updatedEntry.resource.id = uuidv4();
}
if (updatedEntry.resource.status != 'active') {
if (updatedEntry.resource.status != 'active' && process.env.AUTHORING === 'false') {
updatedEntry.resource.status = 'active';
console.warn(
`Resource ${updatedEntry.resource.resourceType}/${updatedEntry.resource.id} status has been coerced to 'active'.`
`Resource ${updatedEntry.resource.resourceType}/${updatedEntry.resource.id} status has been coerced to 'active' for Publishable environment.`
);
}
}
Expand Down
20 changes: 20 additions & 0 deletions service/src/config/capabilityStatementResources.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@
],
"name": "review",
"definition": "http://hl7.org/fhir/uv/crmi/OperationDefinition/crmi-review"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation",
"valueCode": "SHALL"
}
],
"name": "release",
"definition": "http://hl7.org/fhir/uv/crmi/OperationDefinition/crmi-release"
}
]
},
Expand Down Expand Up @@ -400,6 +410,16 @@
],
"name": "review",
"definition": "http://hl7.org/fhir/uv/crmi/OperationDefinition/crmi-review"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/capabilitystatement-expectation",
"valueCode": "SHALL"
}
],
"name": "release",
"definition": "http://hl7.org/fhir/uv/crmi/OperationDefinition/crmi-release"
}
]
}
Expand Down
48 changes: 48 additions & 0 deletions service/src/config/serverConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,30 @@ export const serverConfig: ServerConfig = {
route: '/:id/$review',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-review.html'
},
{
name: 'release',
route: '/$release',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
},
{
name: 'release',
route: '/$release',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
},
{
name: 'release',
route: '/:id/$release',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
},
{
name: 'release',
route: '/:id/$release',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
}
]
},
Expand Down Expand Up @@ -331,6 +355,30 @@ export const serverConfig: ServerConfig = {
route: '/:id/$review',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-review.html'
},
{
name: 'release',
route: '/$release',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
},
{
name: 'release',
route: '/$release',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
},
{
name: 'release',
route: '/:id/$release',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
},
{
name: 'release',
route: '/:id/$release',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-release.html'
}
]
}
Expand Down
8 changes: 8 additions & 0 deletions service/src/requestSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ export const ReviewArgs = z
.strict()
.superRefine(catchInvalidParams([catchMissingId, catchMissingTypeAndSummary]));

export const ReleaseArgs = z
.object({
id: z.string(),
releaseVersion: z.string(),
versionBehavior: z.union([z.literal('default'), z.literal('check'), z.literal('force')])
})
.strict();

export const IdentifyingParameters = z
.object({
id: z.string(),
Expand Down
83 changes: 82 additions & 1 deletion service/src/services/LibraryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
DraftArgs,
CloneArgs,
ApproveArgs,
ReviewArgs
ReviewArgs,
ReleaseArgs
} from '../requestSchemas';
import { Service } from '../types/service';
import {
Expand Down Expand Up @@ -413,6 +414,86 @@ export class LibraryService implements Service<CRMIShareableLibrary> {
return createBatchResponseBundle(reviewedArtifacts);
}

/**
* result of sending a POST or GET request to:
* {BASE_URL}/4_0_1/Library/$release or {BASE_URL}/4_0_1/Library/[id]/$release
* releases an artifact, updating the status of an existing draft artifact to active and
* setting the date element of the resource. Also sets the version and recursively releases
* child artifacts according to the versionBehavior parameter.
*/
async release(args: RequestArgs, { req }: RequestCtx) {
logger.info(`${req.method} ${req.path}`);

// checks that the authoring environment variable is true
checkAuthoring();

if (req.method === 'POST') {
const contentType: string | undefined = req.headers['content-type'];
checkContentTypeHeader(contentType);
}

const params = gatherParams(req.query, args.resource);
validateParamIdSource(req.params.id, params.id);
const query = extractIdentificationForQuery(args, params);
const parsedParams = parseRequestSchema({ ...params, ...query }, ReleaseArgs);

const library = await findResourceById<CRMIShareableLibrary>(parsedParams.id, 'Library');
if (!library) {
throw new ResourceNotFoundError(`No resource found in collection: Library, with id: ${parsedParams.id}`);
}
if (library.status !== 'draft') {
throw new BadRequestError(
`Library with id: ${library.id} has status ${library.status}. $release may only be used on draft artifacts.`
);
}
checkIsOwned(library, 'Child artifacts cannot be directly released.');

library.status = 'active';
library.date = new Date().toISOString();

// Version behavior source: https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/CodeSystem-crmi-release-version-behavior-codes.html
if (parsedParams.versionBehavior === 'check') {
logger.info('Applying check version behavior');
// check: if the root artifact has a specified version different from the version passed in, an error will be returned
// Developer note: this is assumed to be the behavior for child artifacts as well
if (parsedParams.releaseVersion !== library.version) {
throw new BadRequestError(
`Library with id: ${library.id} has version ${library.version}, which does not match the passed release version ${parsedParams.releaseVersion}`
);
}
} else if (parsedParams.versionBehavior === 'force') {
logger.info('Applying force version behavior');
// force: version provided will be applied to the root and all children, regardless of whether a version was already specified
library.version = parsedParams.releaseVersion;
} else {
logger.info('Applying default version behavior');
// default: version provided will be applied to the root artifact and all children if a version is not specified.
// Developer note: this is currently a null operation because version is a required field
}

// recursively get any child artifacts from the artifact if they exist
const children = library.relatedArtifact ? await getChildren(library.relatedArtifact) : [];
children.forEach(child => {
child.status = 'active';
child.date = new Date().toISOString();
if (parsedParams.versionBehavior === 'check') {
if (parsedParams.releaseVersion !== child.version) {
throw new BadRequestError(
`Child artifact with id: ${child.id} has version ${child.version}, which does not match the passed release version ${parsedParams.releaseVersion}`
);
}
} else if (parsedParams.versionBehavior === 'force') {
child.version = parsedParams.releaseVersion;
}
});

// batch update the released parent Library and any of its children
const releasedArtifacts = await batchUpdate([library, ...(await Promise.all(children))], 'release');

// return a Bundle containing the updated artifacts
return createBatchResponseBundle(releasedArtifacts);
}

/**
* result of sending a POST or GET request to:
* {BASE_URL}/4_0_1/Library/$cqfm.package or {BASE_URL}/4_0_1/Library/:id/$cqfm.package
Expand Down
83 changes: 82 additions & 1 deletion service/src/services/MeasureService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
DraftArgs,
CloneArgs,
ApproveArgs,
ReviewArgs
ReviewArgs,
ReleaseArgs
} from '../requestSchemas';
import { v4 as uuidv4 } from 'uuid';
import { Filter } from 'mongodb';
Expand Down Expand Up @@ -415,6 +416,86 @@ export class MeasureService implements Service<CRMIShareableMeasure> {
return createBatchResponseBundle(reviewedArtifacts);
}

/**
* result of sending a POST or GET request to:
* {BASE_URL}/4_0_1/Measure/$release or {BASE_URL}/4_0_1/Measure/[id]/$release
* releases an artifact, updating the status of an existing draft artifact to active and
* setting the date element of the resource. Also sets the version and recursively releases
* child artifacts according to the versionBehavior parameter.
*/
async release(args: RequestArgs, { req }: RequestCtx) {
logger.info(`${req.method} ${req.path}`);

// checks that the authoring environment variable is true
checkAuthoring();

if (req.method === 'POST') {
const contentType: string | undefined = req.headers['content-type'];
checkContentTypeHeader(contentType);
}

const params = gatherParams(req.query, args.resource);
validateParamIdSource(req.params.id, params.id);
const query = extractIdentificationForQuery(args, params);
const parsedParams = parseRequestSchema({ ...params, ...query }, ReleaseArgs);

const measure = await findResourceById<CRMIShareableMeasure>(parsedParams.id, 'Measure');
if (!measure) {
throw new ResourceNotFoundError(`No resource found in collection: Measure, with id: ${parsedParams.id}`);
}
if (measure.status !== 'draft') {
throw new BadRequestError(
`Measure with id: ${measure.id} has status ${measure.status}. $release may only be used on draft artifacts.`
);
}
checkIsOwned(measure, 'Child artifacts cannot be directly released.');

measure.status = 'active';
measure.date = new Date().toISOString();

// Version behavior source: https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/CodeSystem-crmi-release-version-behavior-codes.html
if (parsedParams.versionBehavior === 'check') {
logger.info('Applying check version behavior');
// check: if the root artifact has a specified version different from the version passed in, an error will be returned
// Developer note: this is assumed to be the behavior for child artifacts as well
if (parsedParams.releaseVersion !== measure.version) {
throw new BadRequestError(
`Measure with id: ${measure.id} has version ${measure.version}, which does not match the passed release version ${parsedParams.releaseVersion}`
);
}
} else if (parsedParams.versionBehavior === 'force') {
logger.info('Applying force version behavior');
// force: version provided will be applied to the root and all children, regardless of whether a version was already specified
measure.version = parsedParams.releaseVersion;
} else {
logger.info('Applying default version behavior');
// default: version provided will be applied to the root artifact and all children if a version is not specified.
// Developer note: this is currently a null operation because version is a required field
}

// recursively get any child artifacts from the artifact if they exist
const children = measure.relatedArtifact ? await getChildren(measure.relatedArtifact) : [];
children.forEach(child => {
child.status = 'active';
child.date = new Date().toISOString();
if (parsedParams.versionBehavior === 'check') {
if (parsedParams.releaseVersion !== child.version) {
throw new BadRequestError(
`Child artifact with id: ${child.id} has version ${child.version}, which does not match the passed release version ${parsedParams.releaseVersion}`
);
}
} else if (parsedParams.versionBehavior === 'force') {
child.version = parsedParams.releaseVersion;
}
});

// batch update the released parent Measure and any of its children
const releasedArtifacts = await batchUpdate([measure, ...(await Promise.all(children))], 'release');

// return a Bundle containing the updated artifacts
return createBatchResponseBundle(releasedArtifacts);
}

/**
* result of sending a POST or GET request to:
* {BASE_URL}/4_0_1/Measure/$cqfm.package or {BASE_URL}/4_0_1/Measure/:id/$cqfm.package
Expand Down
Loading

0 comments on commit 03319f5

Please sign in to comment.