diff --git a/api/dicom-web/controller/UPS-RS/create-workItems.js b/api/dicom-web/controller/UPS-RS/create-workItems.js new file mode 100644 index 00000000..41091426 --- /dev/null +++ b/api/dicom-web/controller/UPS-RS/create-workItems.js @@ -0,0 +1,56 @@ +const _ = require("lodash"); +const { + CreateWorkItemService +} = require("./service/create-workItem.service"); +const { ApiLogger } = require("../../../../utils/logs/api-logger"); +const { Controller } = require("../../../controller.class"); +const { DicomWebServiceError } = require("@error/dicom-web-service"); + +class CreateWorkItemController extends Controller { + constructor(req, res) { + super(req, res); + } + + async mainProcess() { + let apiLogger = new ApiLogger(this.request, "UPS-RS"); + + apiLogger.addTokenValue(); + apiLogger.logger.info("Create workItem"); + + try { + let workItemService = new CreateWorkItemService(this.request, this.response); + let workItem = await workItemService.createUps(); + apiLogger.logger.info(`Create workItem ${workItem.upsInstanceUID} successful`); + return this.response.status(201).send(); + } catch (e) { + let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e)); + apiLogger.logger.error(errorStr); + + if (e instanceof DicomWebServiceError) { + return this.response.status(e.code).send({ + 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 CreateWorkItemController(req, res); + + await controller.doPipeline(); +}; diff --git a/api/dicom-web/controller/UPS-RS/get-workItem.js b/api/dicom-web/controller/UPS-RS/get-workItem.js new file mode 100644 index 00000000..4cc0eb09 --- /dev/null +++ b/api/dicom-web/controller/UPS-RS/get-workItem.js @@ -0,0 +1,46 @@ +const { + GetWorkItemService +} = require("./service/get-workItem.service"); +const { ApiLogger } = require("../../../../utils/logs/api-logger"); +const { Controller } = require("../../../controller.class"); + +class GetWorkItemController extends Controller { + constructor(req, res) { + super(req, res); + } + + async mainProcess() { + let apiLogger = new ApiLogger(this.request, "UPS-RS"); + + apiLogger.addTokenValue(); + apiLogger.logger.info(`Get workItem, query: ${this.queryToString()}`); + + try { + let getWorkItemService = new GetWorkItemService(this.request, this.response); + let workItems = await getWorkItemService.getUps(); + return this.response.set("Content-Type", "application/dicom+json").status(200).json(workItems); + } catch (e) { + let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e)); + apiLogger.logger.error(errorStr); + + this.response.writeHead(500, { + "Content-Type": "application/dicom+json" + }); + this.response.end(JSON.stringify({ + code: 500, + message: errorStr + })); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new GetWorkItemController(req, res); + + await controller.doPipeline(); +}; diff --git a/api/dicom-web/controller/UPS-RS/service/create-workItem.service.js b/api/dicom-web/controller/UPS-RS/service/create-workItem.service.js new file mode 100644 index 00000000..2dba3719 --- /dev/null +++ b/api/dicom-web/controller/UPS-RS/service/create-workItem.service.js @@ -0,0 +1,92 @@ +const _ = require("lodash"); +const workItemModel = require("@models/mongodb/models/workItems"); +const patientModel = require("@models/mongodb/models/patient"); +const { UIDUtils } = require("@dcm4che/util/UIDUtils"); +const { + DicomWebServiceError, + DicomWebStatusCodes +} = require("@error/dicom-web-service"); +const { DicomJsonModel } = require("@models/DICOM/dicom-json-model"); + +class CreateWorkItemService { + constructor(req, res) { + this.request = req; + this.response = res; + this.requestWorkItem = /** @type {Object[]} */(this.request.body).pop(); + /** @type {DicomJsonModel} */ + this.requestWorkItem = new DicomJsonModel(this.requestWorkItem); + } + + async createUps() { + let uid = _.get(this.request, "query.workitem", + await UIDUtils.createUID() + ); + _.set(this.requestWorkItem.dicomJson, "upsInstanceUID", uid); + _.set(this.requestWorkItem.dicomJson, "00080018", { + vr: "UI", + Value: [ + uid + ] + }); + let workListLabel = this.requestWorkItem.getString("00741202"); + if (!workListLabel) { + _.set(this.requestWorkItem, "00741202", { + vr: "LO", + Value: [ + "RACCOON" + ] + }); + } + + if (this.requestWorkItem.getString("00741000") !== "SCHEDULED") { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSNotScheduled, + `Refused: The provided value of UPS State was not "SCHEDULED"`, + 400 + ); + } + + let patient = await this.findOneOrCreatePatient(); + + let workItem = new workItemModel(this.requestWorkItem.dicomJson); + + if (await this.isUpsExist(uid)) { + throw new DicomWebServiceError( + DicomWebStatusCodes.DuplicateSOPinstance, + `SOP Instance UID that was already allocated to another SOP Instance`, + 400 + ); + } + await workItem.save(); + + + //TODO: subscription + return workItem; + } + + async findOneOrCreatePatient() { + let patientId = this.requestWorkItem.getString("00100020"); + _.set(this.requestWorkItem.dicomJson, "patientID", patientId); + + /** @type {patientModel | null} */ + let patient = await patientModel.findOne({ + "00100020.Value": patientId + }); + + if (!patient) { + /** @type {patientModel} */ + let patientObj = new patientModel(this.requestWorkItem.dicomJson); + patient = await patientObj.save(); + } + + return patient; + } + + async isUpsExist(uid) { + return await workItemModel.findOne({ + upsInstanceUID: uid + }); + } +} + +module.exports.CreateWorkItemService = CreateWorkItemService; \ No newline at end of file diff --git a/api/dicom-web/controller/UPS-RS/service/get-workItem.service.js b/api/dicom-web/controller/UPS-RS/service/get-workItem.service.js new file mode 100644 index 00000000..3260cba2 --- /dev/null +++ b/api/dicom-web/controller/UPS-RS/service/get-workItem.service.js @@ -0,0 +1,58 @@ +const _ = require("lodash"); +const workItemsModel = require("@models/mongodb/models/workItems"); +const { + convertAllQueryToDICOMTag, + convertRequestQueryToMongoQuery +} = require("../../QIDO-RS/service/QIDO-RS.service"); + +class GetWorkItemService { + constructor(req, res) { + this.request = req; + this.response = res; + this.query = {}; + + /** + * @private + */ + this.limit_ = parseInt(this.request.query.limit) || 100; + delete this.request.query["limit"]; + + /** + * @private + */ + this.skip_ = parseInt(this.request.query.offset) || 0; + delete this.request.query["offset"]; + + + this.initQuery_(); + } + + async getUps() { + let mongoQuery = (await convertRequestQueryToMongoQuery(this.query)).$match; + + let queryOptions = { + query: mongoQuery, + skip: this.skip_, + limit: this.limit_, + requestParams: this.request.params + }; + + let docs = await workItemsModel.getDicomJson(queryOptions); + + return docs; + } + + initQuery_() { + let query = _.cloneDeep(this.request.query); + let queryKeys = Object.keys(query).sort(); + for (let i = 0; i < queryKeys.length; i++) { + let queryKey = queryKeys[i]; + if (!query[queryKey]) delete query[queryKey]; + } + + this.query = convertAllQueryToDICOMTag(query); + } + +} + +module.exports.GetWorkItemService = GetWorkItemService; \ No newline at end of file diff --git a/api/dicom-web/ups-rs.route.js b/api/dicom-web/ups-rs.route.js new file mode 100644 index 00000000..88cad24f --- /dev/null +++ b/api/dicom-web/ups-rs.route.js @@ -0,0 +1,66 @@ +/** + * https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11 + * https://dicom.nema.org/medical/dicom/2019a/output/chtml/part18/sect_6.9.html + * @author Chin-Lin, Lee + */ +const express = require("express"); +const Joi = require("joi"); +const { validateParams, intArrayJoi, validateByJoi } = require("../validator"); +const router = express(); +const GLOBAL_SUBSCRIPTION_UIDS = [ + "1.2.840.10008.5.1.4.34.5", + "1.2.840.10008.5.1.4.34.5.1" +]; + +//#region UPS-RS + +/** + * @openapi + * /dicom-web/workitems: + * post: + * tags: + * - UPS-RS + * description: > + * This transaction creates a Workitem on the target Worklist. It corresponds to the UPS DIMSE N-CREATE operation. + * See [Create Workitem Transaction](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.4) + * parameters: + * - $ref: "#/components/parameters/workitem" + * responses: + * "201": + * description: The workitem create successfully + */ +router.post("/workitems", + validateParams({ + workitem: Joi.string().invalid(...GLOBAL_SUBSCRIPTION_UIDS).optional() + }, "query", { + allowUnknown: false + }), + validateByJoi(Joi.array().items(Joi.object()).min(1).max(1), "body"), + require("./controller/UPS-RS/create-workItems") +); + + +/** + * @openapi + * /dicom-web/workitems: + * 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) + * responses: + * "200": + * description: Query successfully + * content: + * "application/dicom+json": + * schema: + * type: array + */ +router.get("/workitems", + require("./controller/UPS-RS/get-workItem") +); + +//#endregion + +module.exports = router; \ No newline at end of file diff --git a/docs/swagger/openapi.json b/docs/swagger/openapi.json index 455e7cfd..40db0164 100644 --- a/docs/swagger/openapi.json +++ b/docs/swagger/openapi.json @@ -480,6 +480,42 @@ } } }, + "/dicom-web/workitems": { + "post": { + "tags": [ + "UPS-RS" + ], + "description": "This transaction creates a Workitem on the target Worklist. It corresponds to the UPS DIMSE N-CREATE operation. See [Create Workitem Transaction](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_11.4)\n", + "parameters": [ + { + "$ref": "#/components/parameters/workitem" + } + ], + "responses": { + "201": { + "description": "The workitem create successfully" + } + } + }, + "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", + "responses": { + "200": { + "description": "Query successfully", + "content": { + "application/dicom+json": { + "schema": { + "type": "array" + } + } + } + } + } + } + }, "/dicom-web/studies/{studyUID}/bulkdata": { "get": { "tags": [ @@ -1219,6 +1255,14 @@ "type": "string" } }, + "workitem": { + "name": "workitem", + "description": "workitem instance UID", + "in": "query", + "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 new file mode 100644 index 00000000..259d99eb --- /dev/null +++ b/docs/swagger/parameters/ups.yaml @@ -0,0 +1,8 @@ +components: + parameters: + "workitem": + name: "workitem" + description: workitem instance UID + in: query + schema: + type: string \ No newline at end of file diff --git a/error/dicom-web-service.js b/error/dicom-web-service.js new file mode 100644 index 00000000..319116ce --- /dev/null +++ b/error/dicom-web-service.js @@ -0,0 +1,18 @@ +const DicomWebStatusCodes = { + "DuplicateSOPinstance": "0111", + "UPSNotScheduled": "C309" +}; + +class DicomWebServiceError extends Error { + constructor(status, message, code=500) { + super(message); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.status = status; + this.code = code; + } +} + +module.exports.DicomWebStatusCodes = DicomWebStatusCodes; +module.exports.DicomWebServiceError = DicomWebServiceError; \ No newline at end of file diff --git a/models/DICOM/dicom-json-model.js b/models/DICOM/dicom-json-model.js index a0c031c6..40454c10 100644 --- a/models/DICOM/dicom-json-model.js +++ b/models/DICOM/dicom-json-model.js @@ -205,6 +205,14 @@ class DicomJsonModel { } } + /** + * + * @param {string} tag + */ + getString(tag) { + return String(_.get(this.dicomJson, `${tag}.Value.0`, "")); + } + getMediaStorageInfo() { return { "00880130": { diff --git a/models/DICOM/dicom-tags-mapping.js b/models/DICOM/dicom-tags-mapping.js index 9968f0d2..a8a01f48 100644 --- a/models/DICOM/dicom-tags-mapping.js +++ b/models/DICOM/dicom-tags-mapping.js @@ -380,5 +380,222 @@ module.exports.tagsNeedStore = { "00120072": { "vr": "LO" } + }, + /** + * @description + * tag from dcm4che: {@link https://github.com/dcm4che/dcm4chee-arc-light/blob/master/dcm4chee-arc-conf-test/src/test/java/org/dcm4chee/arc/conf/ArchiveDeviceFactory.java | ArchiveDeviceFactory} + * ,{@link https://dicom.nema.org/medical/Dicom/2016e/output/chtml/part03/sect_C.30.2.html | Unified Procedure Step Scheduled Procedure Information Module Attributes} + */ + UPS: { + "00080005": { + "vr": "CS" + }, + "00080016": { + "vr": "UI" + }, + "00080018": { + "vr": "UI" + }, + "00080080": { + "vr": "LO" + }, + "00080081": { + "vr": "ST" + }, + "00080082": { + "vr": "SQ" + }, + "00080090": { + "vr": "PN" + }, + "00080092": { + "vr": "ST" + }, + "00080094": { + "vr": "SH" + }, + "00080096": { + "vr": "SQ" + }, + "0008009C": { + "vr": "PN" + }, + "0008009D": { + "vr": "SQ" + }, + "00081040": { + "vr": "LO" + }, + "00081041": { + "vr": "SQ" + }, + "00081080": { + "vr": "LO" + }, + "00081084": { + "vr": "SQ" + }, + "00101010": { + "vr": "AS" + }, + "00101020": { + "vr": "DS" + }, + "00101021": { + "vr": "SQ" + }, + "00101023": { + "vr": "DS" + }, + "00101024": { + "vr": "DS" + }, + "00101030": { + "vr": "DS" + }, + "00102180": { + "vr": "SH" + }, + "001021B0": { + "vr": "LT" + }, + "00102203": { + "vr": "CS" + }, + "00102000": { + "vr": "LO" + }, + "00102110": { + "vr": "LO" + }, + "001021A0": { + "vr": "CS" + }, + "001021C0": { + "vr": "US" + }, + "001021D0": { + "vr": "DA" + }, + "0020000D": { + "vr": "UI" + }, + "00321066": { + "vr": "UT" + }, + "00321067": { + "vr": "SQ" + }, + "00380010": { + "vr": "LO" + }, + "00380014": { + "vr": "SQ" + }, + "00380050": { + "vr": "LO" + }, + "00380100": { + "vr": "SQ" + }, + "00380101": { + "vr": "SQ" + }, + "00380500": { + "vr": "LO" + }, + "00380502": { + "vr": "SQ" + }, + "00380008": { + "vr": "CS" + }, + "00380016": { + "vr": "LO" + }, + "00380020": { + "vr": "DA" + }, + "00380021": { + "vr": "TM" + }, + "00380060": { + "vr": "LO" + }, + "00380062": { + "vr": "LO" + }, + "00380064": { + "vr": "SQ" + }, + "00380300": { + "vr": "LO" + }, + "00380400": { + "vr": "LO" + }, + "00384000": { + "vr": "LT" + }, + "00404005": { + "vr": "DT" + }, + "00404008": { + "vr": "DT" + }, + "00404011": { + "vr": "DT" + }, + "00404018": { + "vr": "SQ" + }, + "00404021": { + "vr": "SQ" + }, + "00404025": { + "vr": "SQ" + }, + "00404026": { + "vr": "SQ" + }, + "00404027": { + "vr": "SQ" + }, + "00404034": { + "vr": "SQ" + }, + "00404041": { + "vr": "CS" + }, + "00404070": { + "vr": "SQ" + }, + "0040A370": { + "vr": "SQ" + }, + "00741000": { + "vr": "CS" + }, + "00741002": { + "vr": "SQ" + }, + "00741200": { + "vr": "CS" + }, + "00741202": { + "vr": "LO" + }, + "00741204": { + "vr": "LO" + }, + "00741210": { + "vr": "SQ" + }, + "00741216": { + "vr": "SQ" + }, + "00741224": { + "vr": "SQ" + } } }; diff --git a/models/mongodb/models/workItems.js b/models/mongodb/models/workItems.js new file mode 100644 index 00000000..fcde4df3 --- /dev/null +++ b/models/mongodb/models/workItems.js @@ -0,0 +1,98 @@ +const path = require("path"); +const mongoose = require("mongoose"); +const _ = require("lodash"); +const { tagsNeedStore } = require("../../DICOM/dicom-tags-mapping"); +const { getVRSchema } = require("../schema/dicomJsonAttribute"); + +let workItemSchema = new mongoose.Schema( + { + upsInstanceUID: { + type: String, + default: void 0, + index: true, + required: true + }, + patientID: { + type: String, + default: void 0, + index: true, + required: true + }, + transactionUID: { + type: String, + default: void 0, + index: true + } + }, + { + strict: false, + versionKey: false, + toObject: { + getters: true + } + } +); + +for (let tag in tagsNeedStore.UPS) { + let vr = tagsNeedStore.UPS[tag].vr; + let tagSchema = getVRSchema(vr); + workItemSchema.add({ + [tag]: tagSchema + }); +} + +/** + * + * @param {import("../../../utils/typeDef/dicom").DicomJsonMongoQueryOptions} queryOptions + * @returns + */ +workItemSchema.statics.getDicomJson = async function (queryOptions) { + let workItemFields = getWorkItemFields(); + + try { + let docs = await mongoose.model("workItems").find(queryOptions.query, workItemFields) + .limit(queryOptions.limit) + .skip(queryOptions.skip) + .setOptions({ + strictQuery: false + }) + .exec(); + + + let workItemDicomJson = docs.map((v) => { + let obj = v.toObject(); + delete obj._id; + delete obj.id; + return obj; + }); + + return workItemDicomJson; + + } catch (e) { + throw e; + } +}; + +function getWorkItemFields() { + return { + upsInstanceUID: 0, + patientID: 0, + transactionUID: 0 + }; +} + +/** + * @typedef { mongoose.Model & { + * getDicomJson: function(import("../../../utils/typeDef/dicom").DicomJsonMongoQueryOptions): Promise + * }} WorkItemsModel +*/ + +/** @type {WorkItemsModel} */ +let workItemModel = mongoose.model( + "workItems", + workItemSchema, + "workItems" +); + +/** @type { WorkItemsModel } */ +module.exports = workItemModel; diff --git a/models/mongodb/service.js b/models/mongodb/service.js index ee2687c0..fe7ab819 100644 --- a/models/mongodb/service.js +++ b/models/mongodb/service.js @@ -36,7 +36,7 @@ function timeQuery(iQuery, colName) { if (dashIndex === 0) { // -HHMMSS let timeStr = value.substring(1); let time = getTimeFloatFromString(timeStr); - + return getLessThanOrEqualTimeQuery(time); } else if (dashIndex === value.length - 1) { // HHMMSS- let timeStr = value.substring(0, dashIndex); @@ -63,7 +63,7 @@ function timeQuery(iQuery, colName) { */ function getEqualTimeQuery(time) { return { - $eq : time + $eq: time }; } @@ -83,7 +83,7 @@ function getGreaterThanOrEqualTimeQuery(time) { */ function getLessThanOrEqualTimeQuery(time) { return { - $lte : time + $lte: time }; } diff --git a/routes.js b/routes.js index 480fe3b6..c9604b81 100644 --- a/routes.js +++ b/routes.js @@ -27,6 +27,7 @@ module.exports = function (app) { app.use("/dicom-web", require("./api/dicom-web/wado-rs-bulkdata.route")); app.use("/dicom-web", require("./api/dicom-web/wado-rs-thumbnail.route")); app.use("/dicom-web", require("./api/dicom-web/delete.route")); + app.use("/dicom-web", require("./api/dicom-web/ups-rs.route")); app.use("/wado", require("./api/WADO-URI")); };