From eb6ed9187bdcd61a8cbd5081e09c77a0662c5f55 Mon Sep 17 00:00:00 2001 From: chin Date: Wed, 24 May 2023 15:59:13 +0800 Subject: [PATCH] feat: change workitem state API --- .../UPS-RS/change-workItem-state.js | 57 +++++ .../service/change-workItem-state.service.js | 211 ++++++++++++++++++ api/dicom-web/ups-rs.route.js | 36 +++ docs/swagger/openapi.json | 68 ++++++ docs/swagger/parameters/ups.yaml | 7 + error/dicom-web-service.js | 2 + models/DICOM/code.js | 37 +++ models/DICOM/dicom-tags-mapping.js | 3 + 8 files changed, 421 insertions(+) create mode 100644 api/dicom-web/controller/UPS-RS/change-workItem-state.js create mode 100644 api/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js create mode 100644 models/DICOM/code.js diff --git a/api/dicom-web/controller/UPS-RS/change-workItem-state.js b/api/dicom-web/controller/UPS-RS/change-workItem-state.js new file mode 100644 index 00000000..17c14915 --- /dev/null +++ b/api/dicom-web/controller/UPS-RS/change-workItem-state.js @@ -0,0 +1,57 @@ +const { + ChangeWorkItemStateService +} = require("./service/change-workItem-state.service"); +const { ApiLogger } = require("../../../../utils/logs/api-logger"); +const { Controller } = require("../../../controller.class"); +const { DicomWebServiceError } = require("@error/dicom-web-service"); + +class ChangeWorkItemStateController extends Controller { + constructor(req, res) { + super(req, res); + } + + async mainProcess() { + let apiLogger = new ApiLogger(this.request, "UPS-RS"); + + apiLogger.addTokenValue(); + apiLogger.logger.info(`Update workItem, params: ${this.paramsToString()}`); + + try { + let service = new ChangeWorkItemStateService(this.request, this.response); + let workItems = await service.changeWorkItemState(); + return this.response + .set("Content-Type", "application/dicom+json") + .status(200) + .end(); + } catch (e) { + let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e)); + apiLogger.logger.error(errorStr); + + if (e instanceof DicomWebServiceError) { + return this.response.status(e.code).json({ + status: e.status, + message: e.message + }); + } + + this.response.writeHead(500, { + "Content-Type": "application/dicom+json" + }); + this.response.end(JSON.stringify({ + code: 500, + message: "An Server Exception Occurred" + })); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new ChangeWorkItemStateController(req, res); + + await controller.doPipeline(); +}; diff --git a/api/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js b/api/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js new file mode 100644 index 00000000..32f029a7 --- /dev/null +++ b/api/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js @@ -0,0 +1,211 @@ +const _ = require("lodash"); +const moment = require("moment"); +const { DicomJsonModel } = require("@models/DICOM/dicom-json-model"); +const { DicomCode } = require("@models/DICOM/code"); +const workItemModel = require("@models/mongodb/models/workItems"); +const { + DicomWebServiceError, + DicomWebStatusCodes +} = require("@error/dicom-web-service"); + +class ChangeWorkItemStateService { + /** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + constructor(req, res) { + this.request = req; + this.response = res; + this.requestState = /** @type {Object[]} */(this.request.body).pop(); + /** @type {DicomJsonModel} */ + this.requestState = new DicomJsonModel(this.requestState); + this.workItem = null; + this.workItemState = ""; + } + + async changeWorkItemState() { + await this.findOneWorkItem(); + + this.workItemState = this.workItem.getString("00741000"); + let requestState = this.requestState.getString("00741000"); + + if (requestState === "IN PROGRESS") { + this.inProgressChange(); + } else if (requestState === "CANCELED") { + this.cancelChange(); + } else if (requestState === "COMPLETED") { + this.completeChange(); + } + + await workItemModel.findOneAndUpdate({ + upsInstanceUID: this.request.params.workItem + }, { + ...this.requestState.dicomJson + }); + } + + async findOneWorkItem() { + + let workItem = await workItemModel.findOne({ + upsInstanceUID: this.request.params.workItem + }); + + if (!workItem) { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSDoesNotExist, + "The UPS instance not exist", + 404 + ); + } + + this.workItem = new DicomJsonModel(workItem); + + } + + inProgressChange() { + if (this.workItemState === "IN PROGRESS") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSAlreadyInProgress, + "The request is inconsistent with the current state of the Target Workitem", + 409 + ); + } else if (this.workItemState === "COMPLETED" || this.workItemState === "CANCELED") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSMayNoLongerBeUpdated, + "The request is inconsistent with the current state of the Target Workitem", + 409 + ); + } + } + + cancelChange() { + if (this.workItemState === "SCHEDULED") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSNotYetInProgress, + "The request is inconsistent with the current state of the Target Workitem", + 409 + ); + } else if (this.workItemState === "COMPLETED") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSMayNoLongerBeUpdated, + "The request is inconsistent with the current state of the Target Workitem", + 409 + ); + } else if (this.workItemState === "CANCELED") { + this.response.set("Warning", "299 Raccoon: The UPS is already in the requested state of CANCELED."); + } + + let transactionUID = this.requestState.getString("00081195"); + let workItemTransactionUID = this.requestState.getString("00081195"); + if (transactionUID !== workItemTransactionUID) { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSTransactionUIDNotCorrect, + "Refused: The correct Transaction UID was not provided", + 400 + ); + } + this.supplementDiscontinuationReasonCode(); + } + + completeChange() { + if (this.workItemState === "SCHEDULED") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSNotYetInProgress, + "The request is inconsistent with the current state of the Target Workitem (UPS Not Yet In Progress)", + 409 + ); + } else if (this.workItemState === "CANCELED") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSMayNoLongerBeUpdated, + "The request is inconsistent with the current state of the Target Workitem (The CANCELED UPS can not change to COMPLETED)", + 409 + ); + } else if (this.workItemState === "COMPLETED") { + this.response.set("Warning", "299 Raccoon: The UPS is already in the requested state of COMPLETED."); + } + + let transactionUID = this.requestState.getString("00081195"); + let workItemTransactionUID = this.requestState.getString("00081195"); + if (transactionUID !== workItemTransactionUID) { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSTransactionUIDNotCorrect, + "Refused: The correct Transaction UID was not provided", + 400 + ); + } + if (!this.meetFinalStateRequirementsOfCompleted()) { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSNotMetFinalStateRequirements, + "The request is inconsistent with the current state of the Target Workitem (The workitem is not meet final state requirements of completed)", + 409 + ); + } + } + + ensureProgressInformationSequence() { + let progressInformation = _.get(this.workItem.dicomJson, "00741002.Value"); + if (!progressInformation) { + _.set(this.workItem.dicomJson, "00741002", { + vr: "SQ", + Value: [] + }); + } + } + + supplementDiscontinuationReasonCode() { + this.ensureProgressInformationSequence(); + let procedureStepCancellationDateTime = _.get(this.workItem.dicomJson, "00741002.Value.0.00404052"); + if (!procedureStepCancellationDateTime) { + _.set(this.workItem.dicomJson, "00741002.Value.0.00404052", { + vr: "DT", + Value: [ + moment().format("YYYYMMDDhhmmss.SSSSSSZZ") + ] + }); + } + + let reasonCodeMeaning = _.get(this.workItem.dicomJson, "00741002.Value.0.0074100E.Value.0.00080104"); + if (!reasonCodeMeaning) { + _.set(this.workItem.dicomJson, "00741002.Value.0.0074100E.Value.0", { + vr: "SQ", + Value: [ + { + "00081000": { + "vr": "SH", + "Value": ["110513"] + }, + "00080102": { + "vr": "SH", + "Value": ["DCM"] + }, + "00080104": { + "vr": "LO", + "Value": ["Discontinued for unspecified reason"] + } + } + ] + }); + } + } + + meetFinalStateRequirementsOfCompleted() { + let performedProcedure = _.get(this.workItem.dicomJson, "00741216"); + if (performedProcedure && + _.get(performedProcedure, "Value.0.00404050") && + _.get(performedProcedure, "Value.0.00404051")) { + + try { + let stationNameCode = new DicomCode(_.get(performedProcedure, "Value.0.00404028.Value.0")); + let workItemCode = new DicomCode(_.get(performedProcedure, "Value.0.00404019.Value.0")); + + return true; + } catch(e) { + console.log(`Invalid Dicom Code ${e}`); + } + } + return false; + } +} + +module.exports.ChangeWorkItemStateService = ChangeWorkItemStateService; \ No newline at end of file diff --git a/api/dicom-web/ups-rs.route.js b/api/dicom-web/ups-rs.route.js index b7fc2b08..0649552f 100644 --- a/api/dicom-web/ups-rs.route.js +++ b/api/dicom-web/ups-rs.route.js @@ -70,6 +70,8 @@ router.get("/workitems", * description: > * This transaction retrieves a Workitem. It corresponds to the UPS DIMSE N-GET operation. * See [Retrieve Workitem Transaction](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.5) + * parameters: + * - $ref: "#/components/parameters/workitemUID" * responses: * "200": * description: Query successfully @@ -91,6 +93,8 @@ router.get("/workitems/:workItem", * description: > * This transaction modifies Attributes of an existing Workitem. It corresponds to the UPS DIMSE N-SET operation. * See [Update Workitem Transaction](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.6) + * parameters: + * - $ref: "#/components/parameters/workitemUID" * responses: * "200": * description: modify successfully @@ -99,6 +103,38 @@ router.post("/workitems/:workItem", require("./controller/UPS-RS/update-workItem") ); +/** + * @openapi + * /dicom-web/workitems/{workitemUID}/state: + * put: + * tags: + * - UPS-RS + * description: > + * This transaction is used to change the state of a Workitem. It corresponds to the UPS DIMSE N-ACTION operation "Change UPS State".
+ * State changes are used to claim ownership, complete, or cancel a Workitem.
+ * See [Change Workitem State](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.7) + * parameters: + * - $ref: "#/components/parameters/workitemUID" + * responses: + * "200": + * description: The update was successful, and the response payload contains a Status Report document. + */ +router.put("/workitems/:workItem/state", + validateByJoi(Joi.array().items( + Joi.object({ + "00741000": Joi.object({ + vr: Joi.string().valid("CS"), + Value: Joi.array().items(Joi.string().valid("IN PROGRESS", "COMPLETED", "CANCELED")).min(1).max(1) + }), + "00081195": Joi.object({ + vr: Joi.string().valid("UI"), + Value: Joi.array().items(Joi.string()).min(1).max(1) + }) + }) + ).min(1).max(1), "body"), + require("./controller/UPS-RS/change-workItem-state") +); + //#endregion module.exports = router; \ No newline at end of file diff --git a/docs/swagger/openapi.json b/docs/swagger/openapi.json index 40db0164..29e04f87 100644 --- a/docs/swagger/openapi.json +++ b/docs/swagger/openapi.json @@ -516,6 +516,65 @@ } } }, + "/dicom-web/workitems/{workitemUID}": { + "get": { + "tags": [ + "UPS-RS" + ], + "description": "This transaction retrieves a Workitem. It corresponds to the UPS DIMSE N-GET operation. See [Retrieve Workitem Transaction](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.5)\n", + "parameters": [ + { + "$ref": "#/components/parameters/workitemUID" + } + ], + "responses": { + "200": { + "description": "Query successfully", + "content": { + "application/dicom+json": { + "schema": { + "type": "array" + } + } + } + } + } + }, + "post": { + "tags": [ + "UPS-RS" + ], + "description": "This transaction modifies Attributes of an existing Workitem. It corresponds to the UPS DIMSE N-SET operation. See [Update Workitem Transaction](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.6)\n", + "parameters": [ + { + "$ref": "#/components/parameters/workitemUID" + } + ], + "responses": { + "200": { + "description": "modify successfully" + } + } + } + }, + "/dicom-web/workitems/{workitemUID}/state": { + "put": { + "tags": [ + "UPS-RS" + ], + "description": "This transaction is used to change the state of a Workitem. It corresponds to the UPS DIMSE N-ACTION operation \"Change UPS State\".
State changes are used to claim ownership, complete, or cancel a Workitem.
See [Change Workitem State](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.7)\n", + "parameters": [ + { + "$ref": "#/components/parameters/workitemUID" + } + ], + "responses": { + "200": { + "description": "The update was successful, and the response payload contains a Status Report document." + } + } + } + }, "/dicom-web/studies/{studyUID}/bulkdata": { "get": { "tags": [ @@ -1263,6 +1322,15 @@ "type": "string" } }, + "workitemUID": { + "name": "workitemUID", + "description": "workitem instance UID in path", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, "frameNumbers": { "name": "frameNumbers", "description": "comma separated list of one or more non duplicate frame numbers", diff --git a/docs/swagger/parameters/ups.yaml b/docs/swagger/parameters/ups.yaml index 259d99eb..194b01c9 100644 --- a/docs/swagger/parameters/ups.yaml +++ b/docs/swagger/parameters/ups.yaml @@ -4,5 +4,12 @@ components: name: "workitem" description: workitem instance UID in: query + schema: + type: string + "workitemUID": + name: "workitemUID" + description: workitem instance UID in path + in: path + required: true schema: type: string \ No newline at end of file diff --git a/error/dicom-web-service.js b/error/dicom-web-service.js index 783bdb40..f5c16b7a 100644 --- a/error/dicom-web-service.js +++ b/error/dicom-web-service.js @@ -4,6 +4,8 @@ const DicomWebStatusCodes = { "MissingAttribute": "0120", "UPSMayNoLongerBeUpdated": "C300", "UPSTransactionUIDNotCorrect": "C301", + "UPSAlreadyInProgress": "C302", + "UPSNotMetFinalStateRequirements": "C304", "UPSDoesNotExist": "C307", "UPSNotScheduled": "C309", "UPSNotYetInProgress": "C310" diff --git a/models/DICOM/code.js b/models/DICOM/code.js new file mode 100644 index 00000000..807ed1d5 --- /dev/null +++ b/models/DICOM/code.js @@ -0,0 +1,37 @@ +const _ = require("lodash"); + +class DicomCode { + /** + * + * @param {JSON} dicomJson + */ + constructor(dicomJson) { + this.dicomJson = dicomJson; + this.codeValue = ""; + this.codingSchemaDesignator = ""; + this.codeMeaning = ""; + } + + init() { + let codeValue = _.get(this.dicomJson, "00080100.0.Value.0"); + let codingSchemeDesignator = _.get(this.dicomJson, "00080102.0.Value.0"); + let codeMeaning = _.get(this.dicomJson, "00080104.Value.0", ""); + + if (!codeValue) + throw new Error("Missing Code Value"); + + this.codeValue = codeValue; + + if (!codingSchemeDesignator) + throw new Error("Missing Coding Scheme Designator"); + + this.codingSchemaDesignator = codingSchemeDesignator; + + if (!codeMeaning) + throw new Error("Missing Code Meaning"); + + this.codeMeaning = codeMeaning; + } +} + +module.exports.DicomCode = DicomCode; \ No newline at end of file diff --git a/models/DICOM/dicom-tags-mapping.js b/models/DICOM/dicom-tags-mapping.js index 4c7ec7f4..d889d338 100644 --- a/models/DICOM/dicom-tags-mapping.js +++ b/models/DICOM/dicom-tags-mapping.js @@ -435,6 +435,9 @@ module.exports.tagsNeedStore = { "00081084": { "vr": "SQ" }, + "00081195": { + "vr": "UI" + }, "00101010": { "vr": "AS" },