diff --git a/package-lock.json b/package-lock.json index 1a9e65c..9ea1f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "service" ], "devDependencies": { + "@types/lodash": "^4.17.4", "concurrently": "^7.6.0" } }, @@ -4128,6 +4129,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", + "dev": true + }, "node_modules/@types/luxon": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.7.tgz", diff --git a/package.json b/package.json index 78cf109..ab61346 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:service": "npm run test --workspace=service" }, "devDependencies": { + "@types/lodash": "^4.17.4", "concurrently": "^7.6.0" } } diff --git a/service/.env.example b/service/.env.example index 0fa8154..691e4c9 100644 --- a/service/.env.example +++ b/service/.env.example @@ -3,4 +3,5 @@ PORT=3000 HOST='localhost' DATABASE_URL='mongodb://localhost:27017/measure-repository?replicaSet=rs0' -VSAC_API_KEY="" # Add if you plan on using the `include-terminology` query param \ No newline at end of file +VSAC_API_KEY="" # Add if you plan on using the `include-terminology` query param +AUTHORING=true # Make false if this is running as a publishable repository instead \ No newline at end of file diff --git a/service/README.md b/service/README.md index a5f1284..0a1df3d 100644 --- a/service/README.md +++ b/service/README.md @@ -99,8 +99,28 @@ This server currently supports the following CRUD operations: - Read by ID with `GET` to endpoint: `4_0_1//` - Create resource (Library or Measure) with `POST` to endpoint: `4_0_1/` + - Publishable: + - Supports the [CRMI Publishable Artifact Repository](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/artifact-repository-service.html#publishable-artifact-repository) minimum write capability _publish_ + - Artifact must be in active status and conform to appropriate shareable and publishable profiles + - Authoring (In Progress): + - Supports the [CRMI Authoring Artifact Repository](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/artifact-repository-service.html#authoring-artifact-repository) additional authoring capability _submit_ + - Artifact must be in draft status + - Update resource (Library or Measure) with `PUT` to endpoint: `4_0_1//` - _More functionality coming soon!_ + - Publishable: + - Supports the [CRMI Publishable Artifact Repository](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/artifact-repository-service.html#publishable-artifact-repository) minimum write capability _retire_ + - Artifact must be in active status and may only change the status to retired and update the date (and other metadata appropriate to indicate retired status) + - Authoring (In Progress): + - Supports the [CRMI Authoring Artifact Repository](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/artifact-repository-service.html#authoring-artifact-repository) additional authoring capability _revise_ + - Artifact must be in (and remain in) draft status + +- Delete resource (Library or Measure) with `DELETE` to endpoint: `4_0_1//` + - Publishable: + - Supports the [CRMI Publishable Artifact Repository](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/artifact-repository-service.html#publishable-artifact-repository) minimum write capability _archive_ + - Artifact must be in retired status + - Authoring (In Progress): + - Supports the [CRMI Authoring Artifact Repository](https://hl7.org/fhir/uv/crmi/1.0.0-snapshot/artifact-repository-service.html#authoring-artifact-repository) additional authoring capability _withdraw_ + - Artifact must be in draft status ### Search diff --git a/service/src/db/dbOperations.ts b/service/src/db/dbOperations.ts index 6c82745..af184f0 100644 --- a/service/src/db/dbOperations.ts +++ b/service/src/db/dbOperations.ts @@ -99,3 +99,16 @@ export async function updateResource(id: string, data: fhir4.FhirResource, resou return { id, created: false }; } + +/** + * Searches for a document for a resource and deletes it if found + */ +export async function deleteResource(id: string, resourceType: string) { + const collection = Connection.db.collection(resourceType); + logger.debug(`Finding and deleting ${resourceType}/${id} from database`); + const results = await collection.deleteOne({ id }); + if (results.deletedCount === 1) { + return { id, deleted: true }; + } + return { id, deleted: false }; +} diff --git a/service/src/services/LibraryService.ts b/service/src/services/LibraryService.ts index 47baa6e..fb88492 100644 --- a/service/src/services/LibraryService.ts +++ b/service/src/services/LibraryService.ts @@ -1,6 +1,7 @@ import { loggers, RequestArgs, RequestCtx } from '@projecttacoma/node-fhir-server-core'; import { createResource, + deleteResource, findDataRequirementsWithQuery, findResourceById, findResourceCountWithQuery, @@ -18,7 +19,10 @@ import { gatherParams, validateParamIdSource, checkContentTypeHeader, - checkExpectedResourceType + checkExpectedResourceType, + updateFields, + checkFieldsforUpdate, + checkFieldsForDelete } from '../util/inputUtils'; import { v4 as uuidv4 } from 'uuid'; import { Calculator } from 'fqm-execution'; @@ -97,18 +101,13 @@ export class LibraryService implements Service { checkContentTypeHeader(contentType); const resource = req.body; checkExpectedResourceType(resource.resourceType, 'Library'); - resource['id'] = uuidv4(); - if (resource.status != 'active') { - resource.status = 'active'; - logger.warn(`Resource ${resource.id} has been coerced to active`); - } + updateFields(resource); return createResource(resource, 'Library'); } /** * result of sending a PUT request to {BASE_URL}/4_0_1/Library/{id} * updates the library with the passed in id using the passed in data - * or creates a library with passed in id if it does not exist in the database */ async update(args: RequestArgs, { req }: RequestCtx) { logger.info(`PUT /Library/${args.id}`); @@ -120,13 +119,24 @@ export class LibraryService implements Service { if (resource.id !== args.id) { throw new BadRequestError('Argument id must match request body id for PUT request'); } - if (resource.status != 'active') { - resource.status = 'active'; - logger.warn(`Resource ${resource.id} has been coerced to active`); - } + const oldResource = (await findResourceById(resource.id, resource.resourceType)) as fhir4.Library | null; + checkFieldsforUpdate(resource, oldResource); return updateResource(args.id, resource, 'Library'); } + /** + * result of sending a DELETE request to {BASE_URL}/4_0_1/Library/{id} + * deletes the library with the passed in id if it exists in the database + */ + async delete(args: RequestArgs, { req }: RequestCtx) { + const resource = (await findResourceById(args.id, 'Library')) as fhir4.Library | null; + if (!resource) { + throw new ResourceNotFoundError(`Existing resource not found with id ${args.id}`); + } + checkFieldsForDelete(resource); + return deleteResource(args.id, 'Library'); + } + /** * 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 diff --git a/service/src/services/MeasureService.ts b/service/src/services/MeasureService.ts index ceafdd5..9b37b0b 100644 --- a/service/src/services/MeasureService.ts +++ b/service/src/services/MeasureService.ts @@ -1,6 +1,7 @@ import { loggers, RequestArgs, RequestCtx } from '@projecttacoma/node-fhir-server-core'; import { createResource, + deleteResource, findDataRequirementsWithQuery, findResourceById, findResourceCountWithQuery, @@ -17,7 +18,10 @@ import { gatherParams, validateParamIdSource, checkContentTypeHeader, - checkExpectedResourceType + checkExpectedResourceType, + updateFields, + checkFieldsforUpdate, + checkFieldsForDelete } from '../util/inputUtils'; import { Calculator } from 'fqm-execution'; import { MeasureSearchArgs, MeasureDataRequirementsArgs, PackageArgs, parseRequestSchema } from '../requestSchemas'; @@ -98,11 +102,7 @@ export class MeasureService implements Service { checkContentTypeHeader(contentType); const resource = req.body; checkExpectedResourceType(resource.resourceType, 'Measure'); - resource['id'] = uuidv4(); - if (resource.status != 'active') { - resource.status = 'active'; - logger.warn(`Resource ${resource.id} has been coerced to active`); - } + updateFields(resource); return createResource(resource, 'Measure'); } @@ -121,13 +121,24 @@ export class MeasureService implements Service { if (resource.id !== args.id) { throw new BadRequestError('Argument id must match request body id for PUT request'); } - if (resource.status != 'active') { - resource.status = 'active'; - logger.warn(`Resource ${resource.id} has been coerced to active`); - } + const oldResource = (await findResourceById(resource.id, resource.resourceType)) as fhir4.Measure | null; + checkFieldsforUpdate(resource, oldResource); return updateResource(args.id, resource, 'Measure'); } + /** + * result of sending a DELETE request to {BASE_URL}/4_0_1/Library/{id} + * deletes the library with the passed in id if it exists in the database + */ + async delete(args: RequestArgs, { req }: RequestCtx) { + const resource = (await findResourceById(args.id, 'Measure')) as fhir4.Measure | null; + if (!resource) { + throw new ResourceNotFoundError(`Existing resource not found with id ${args.id}`); + } + checkFieldsForDelete(resource); + return deleteResource(args.id, 'Measure'); + } + /** * 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 diff --git a/service/src/util/inputUtils.ts b/service/src/util/inputUtils.ts index 10703f8..9329088 100644 --- a/service/src/util/inputUtils.ts +++ b/service/src/util/inputUtils.ts @@ -1,6 +1,10 @@ import { RequestArgs, RequestQuery, FhirResourceType } from '@projecttacoma/node-fhir-server-core'; import { Filter } from 'mongodb'; -import { BadRequestError } from './errorUtils'; +import { BadRequestError, ResourceNotFoundError } from './errorUtils'; +import { v4 as uuidv4 } from 'uuid'; +import _ from 'lodash'; +import { loggers } from '@projecttacoma/node-fhir-server-core'; +const logger = loggers.get('default'); /* * Gathers parameters from both the query and the FHIR parameter request body resource @@ -67,3 +71,55 @@ export function checkExpectedResourceType(resourceType: string, expectedResource throw new BadRequestError(`Expected resourceType '${expectedResourceType}' in body. Received '${resourceType}'.`); } } + +export function updateFields(resource: fhir4.Measure | fhir4.Library) { + resource['id'] = uuidv4(); + if (process.env.AUTHORING) { + // authoring requires active or draft, TODO: return error instead + if (resource.status !== 'active' && resource.status !== 'draft') { + resource.status = 'active'; + logger.warn(`Resource ${resource.id} has been coerced to active`); + } + } else { + // publishable requires active, TODO: return error instead + if (resource.status !== 'active') { + resource.status = 'active'; + logger.warn(`Resource ${resource.id} has been coerced to active`); + } + } +} + +export function checkFieldsforUpdate( + resource: fhir4.Measure | fhir4.Library, + oldResource: fhir4.Measure | fhir4.Library | null +) { + if (!oldResource) { + throw new ResourceNotFoundError(`Existing resource not found with id ${resource.id}`); + } + if (!process.env.AUTHORING || oldResource.status === 'active') { + // publishable or active status requires retire functionality + // TODO: is there any other metadata we should allow to update for the retire functionality? + if (!process.env.AUTHORING && oldResource.status !== 'active') { + throw new BadRequestError( + `Resource status is currently ${oldResource.status}. Publishable repository service updates may only be made to active status resources.` + ); + } + const { status: statusOld, date: dateOld, ...limitedOld } = oldResource; + const { status: statusNew, date: dateNew, ...limitedNew } = resource; + + if (statusNew !== 'retired') { + throw new BadRequestError('Updating active status resources requires changing the resource status to retired.'); + } + + if (!_.isEqual(limitedOld, limitedNew)) { + throw new BadRequestError('Updating active status resources may only change the status and date.'); + } + } else if (oldResource.status === 'draft') { + // authoring and draft status requires revise functionality + if (resource.status != 'draft') { + throw new BadRequestError('Existing draft resources must stay in draft while revising.'); + } + } else { + throw new BadRequestError(`Cannot update existing resource with status ${oldResource.status}`); + } +}