Skip to content

Commit

Permalink
Expose draft operation (#103)
Browse files Browse the repository at this point in the history
* Expose  operation

* Update service/src/requestSchemas.ts

Co-authored-by: hossenlopp <[email protected]>

---------

Co-authored-by: hossenlopp <[email protected]>
  • Loading branch information
elsaperelli and hossenlopp authored Jul 8, 2024
1 parent 299f778 commit f876096
Show file tree
Hide file tree
Showing 15 changed files with 702 additions and 41 deletions.
3 changes: 2 additions & 1 deletion service/.env.test
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
VSAC_API_KEY="example-api-key"
AUTHORING=true
AUTHORING=true
DATABASE_URL='mongodb://localhost:27017/measure-repository?replicaSet=rs0'
12 changes: 7 additions & 5 deletions service/jest-mongodb-config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
module.exports = {
mongodbMemoryServerOptions: {
binary: {
verison: '6.0.1',
version: '6.0.1',
skipMD5: true
},
autoStart: false,
instance: {}
},

useSharedDBForAllJestWorkers: false
instance: {},
replSet: {
count: 3,
storageEngine: 'wiredTiger'
}
}
};
4 changes: 2 additions & 2 deletions service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dev:watch": "nodemon ./src/index.ts",
"dev:clean": "npm run db:reset && npm run dev",
"start": "node ./dist/index.js",
"test": "jest",
"test": "jest --runInBand --silent",
"test:watch": "jest --watch"
},
"devDependencies": {
Expand Down Expand Up @@ -57,4 +57,4 @@
"uuid": "^9.0.0",
"zod": "^3.20.2"
}
}
}
48 changes: 48 additions & 0 deletions service/src/config/serverConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,30 @@ export const serverConfig: ServerConfig = {
route: '/:id/$data-requirements',
method: 'POST',
reference: 'http://hl7.org/fhir/us/cqfmeasures/OperationDefinition/Measure-data-requirements'
},
{
name: 'draft',
route: '/$draft',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
},
{
name: 'draft',
route: '/$draft',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
},
{
name: 'draft',
route: '/:id/$draft',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
},
{
name: 'draft',
route: '/:id/$draft',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
}
]
},
Expand Down Expand Up @@ -139,6 +163,30 @@ export const serverConfig: ServerConfig = {
route: '/:id/$data-requirements',
method: 'POST',
reference: 'http://hl7.org/fhir/us/cqfmeasures/OperationDefinition/Library-data-requirements'
},
{
name: 'draft',
route: '/$draft',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
},
{
name: 'draft',
route: '/$draft',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
},
{
name: 'draft',
route: '/:id/$draft',
method: 'GET',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
},
{
name: 'draft',
route: '/:id/$draft',
method: 'POST',
reference: 'https://hl7.org/fhir/uv/crmi/STU1/OperationDefinition-crmi-draft.html'
}
]
}
Expand Down
39 changes: 39 additions & 0 deletions service/src/db/dbOperations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FhirResourceType, loggers } from '@projecttacoma/node-fhir-server-core';
import { Filter } from 'mongodb';
import { Connection } from './Connection';
import { ArtifactResourceType, FhirArtifact } from '../types/service-types';

const logger = loggers.get('default');

Expand All @@ -12,6 +13,18 @@ export async function findResourceById<T extends fhir4.FhirResource>(id: string,
return collection.findOne<T>({ id: id }, { projection: { _id: 0, _dataRequirements: 0 } });
}

/**
* Retrieves the artifact of the given type with the given url and version
*/
export async function findArtifactByUrlAndVersion<T extends FhirArtifact>(
url: string,
version: string,
resourceType: ArtifactResourceType
) {
const collection = Connection.db.collection(resourceType);
return collection.findOne<T>({ url, version }, { projection: { _id: 0 } });
}

/**
* searches the database and returns an array of all resources of the given type that match the given query
*/
Expand Down Expand Up @@ -112,3 +125,29 @@ export async function deleteResource(id: string, resourceType: string) {
}
return { id, deleted: false };
}

/**
* Drafts an active parent artifact and all of its children (if applicable) in a batch
*/
export async function batchDraft(drafts: FhirArtifact[]) {
let error = null;
const results: FhirArtifact[] = [];
const draftSession = Connection.connection?.startSession();
try {
await draftSession?.withTransaction(async () => {
for (const draft of drafts) {
const collection = await Connection.db.collection(draft.resourceType);
await collection.insertOne(draft as any, { session: draftSession });
results.push(draft);
}
});
console.log('Batch draft transaction committed.');
} catch (err) {
console.log('Batch draft transaction failed: ' + err);
error = err;
} finally {
await draftSession?.endSession();
}
if (error) throw error;
return results;
}
23 changes: 23 additions & 0 deletions service/src/requestSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod';
import { BadRequestError, NotImplementedError } from './util/errorUtils';

const DATE_REGEX = /([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)/;
const VERSION_REGEX = /^\d+\.\d+\.\d+(\.\d+|)$/;

// Operation Definition: http://hl7.org/fhir/us/cqfmeasures/STU4/OperationDefinition-cqfm-package.html
const UNSUPPORTED_PACKAGE_ARGS = [
Expand Down Expand Up @@ -52,6 +53,8 @@ const UNSUPPORTED_LIBRARY_SEARCH_ARGS = [...UNSUPPORTED_CORE_SEARCH_ARGS, 'conte

const hasIdentifyingInfo = (args: Record<string, any>) => args.id || args.url || args.identifier;

const idAndVersion = (args: Record<string, any>) => args.id && args.version;

/**
* Returns a function that checks if any unsupported params are present, then runs the
* other passed in functions in sequence. Each catchFunction is expected to check for
Expand Down Expand Up @@ -90,6 +93,20 @@ export function catchMissingIdentifyingInfo(val: Record<string, any>, ctx: z.Ref
}
}

/**
* Checks that id and version are included. Adds an issue to the ctx
* that triggers a BadRequest to be thrown if not.
*/
export function catchMissingIdAndVersion(val: Record<string, any>, ctx: z.RefinementCtx) {
if (!idAndVersion(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
params: { serverErrorCode: constants.ISSUE.CODE.REQUIRED },
message: 'Must provide id and version'
});
}
}

/**
* For searches, checks that the version only appears in combination with a url. Adds an
* issue to the ctx that triggers a BadRequest to be thrown if url is not specified when version
Expand All @@ -110,6 +127,12 @@ const stringToBool = z
.transform(x => (typeof x === 'boolean' ? x : x === 'true'));
const stringToNumber = z.coerce.number();
const checkDate = z.string().regex(DATE_REGEX, 'Invalid FHIR date');
const checkVersion = z.string().regex(VERSION_REGEX, 'Invalid Semantic Version');

export const DraftArgs = z
.object({ id: z.string(), version: checkVersion })
.strict()
.superRefine(catchInvalidParams([hasIdentifyingInfo, catchMissingIdAndVersion]));

export const IdentifyingParameters = z
.object({
Expand Down
72 changes: 68 additions & 4 deletions service/src/services/LibraryService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { loggers, RequestArgs, RequestCtx } from '@projecttacoma/node-fhir-server-core';
import {
batchDraft,
createResource,
deleteResource,
findDataRequirementsWithQuery,
Expand All @@ -9,9 +10,20 @@ import {
findResourcesWithQuery,
updateResource
} from '../db/dbOperations';
import { LibrarySearchArgs, LibraryDataRequirementsArgs, PackageArgs, parseRequestSchema } from '../requestSchemas';
import {
LibrarySearchArgs,
LibraryDataRequirementsArgs,
PackageArgs,
parseRequestSchema,
DraftArgs
} from '../requestSchemas';
import { Service } from '../types/service';
import { createLibraryPackageBundle, createSearchsetBundle, createSummarySearchsetBundle } from '../util/bundleUtils';
import {
createBatchResponseBundle,
createLibraryPackageBundle,
createSearchsetBundle,
createSummarySearchsetBundle
} from '../util/bundleUtils';
import { BadRequestError, ResourceNotFoundError } from '../util/errorUtils';
import { getMongoQueryFromRequest } from '../util/queryUtils';
import {
Expand All @@ -22,13 +34,17 @@ import {
checkExpectedResourceType,
checkFieldsForCreate,
checkFieldsForUpdate,
checkFieldsForDelete
checkFieldsForDelete,
checkExistingArtifact,
checkIsOwned,
checkDraft
} from '../util/inputUtils';
import { v4 as uuidv4 } from 'uuid';
import { Calculator } from 'fqm-execution';
const logger = loggers.get('default');
import { Filter } from 'mongodb';
import { FhirLibraryWithDR } from '../types/service-types';
import { CRMILibrary, FhirLibraryWithDR } from '../types/service-types';
import { getChildren, modifyResourcesForDraft } from '../util/serviceUtils';

/*
* Implementation of a service for the `Library` resource
Expand Down Expand Up @@ -144,6 +160,54 @@ export class LibraryService implements Service<fhir4.Library> {
return deleteResource(args.id, 'Library');
}

/**
* result of sending a POST or GET request to:
* {BASE_URL}/4_0_1/Library/$draft or {BASE_URL}/4_0_1/Library/[id]/$draft
* drafts a new version of an existing Library artifact in active status,
* as well as for all resource it is composed of
* requires id and version parameters
*/
async draft(args: RequestArgs, { req }: RequestCtx) {
logger.info(`${req.method} ${req.path}`);

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

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 }, DraftArgs);

const activeLibrary = await findResourceById<CRMILibrary>(parsedParams.id, 'Library');
if (!activeLibrary) {
throw new ResourceNotFoundError(`No resource found in collection: Library, with id: ${args.id}`);
}
checkIsOwned(activeLibrary, 'Child artifacts cannot be directly drafted');

await checkExistingArtifact(activeLibrary.url, parsedParams.version, 'Library');

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

const draftArtifacts = await modifyResourcesForDraft(
[activeLibrary, ...(await Promise.all(children))],
params.version
);

// now we want to batch insert the parent Library artifact and any of its children
const newDrafts = await batchDraft(draftArtifacts);

// we want to return a Bundle containing the created artifacts
return createBatchResponseBundle(newDrafts);
}

/**
* 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
Loading

0 comments on commit f876096

Please sign in to comment.