From 09bbc3831cf7a04bef5e004ac91549bb50ec442d Mon Sep 17 00:00:00 2001 From: chin Date: Mon, 31 Jul 2023 22:19:03 +0800 Subject: [PATCH] refactor: move SQL out of plugins and integration --- .../STOW-RS/service/dicom-jpeg-generator.js | 74 +++++++++++++++++++ .../service/request-multipart-parser.js | 47 ++++++++++++ .../STOW-RS}/service/stow-rs.service.js | 32 +++++++- .../controller/STOW-RS/storeInstance.js | 68 +++++++++++++++++ api-sql/dicom-web/stow-rs.route.js | 35 +++++++++ app.js | 2 +- .../models => models/sql}/dicom-json-model.js | 5 +- {plugins => models}/sql/init.js | 2 +- {plugins => models}/sql/instance.js | 0 .../sql/models/patient.model.js | 5 +- .../sql/models/personName.model.js | 2 +- models/sql/po/patient.po.js | 68 +++++++++++++++++ {plugins => models}/sql/vrTypeMapping.js | 0 plugins/store-sql/index.js | 55 -------------- routes.js | 2 +- server.js | 2 +- 16 files changed, 332 insertions(+), 67 deletions(-) create mode 100644 api-sql/dicom-web/controller/STOW-RS/service/dicom-jpeg-generator.js create mode 100644 api-sql/dicom-web/controller/STOW-RS/service/request-multipart-parser.js rename {plugins/store-sql => api-sql/dicom-web/controller/STOW-RS}/service/stow-rs.service.js (67%) create mode 100644 api-sql/dicom-web/controller/STOW-RS/storeInstance.js create mode 100644 api-sql/dicom-web/stow-rs.route.js rename {plugins/sql/models => models/sql}/dicom-json-model.js (85%) rename {plugins => models}/sql/init.js (90%) rename {plugins => models}/sql/instance.js (100%) rename {plugins => models}/sql/models/patient.model.js (88%) rename {plugins => models}/sql/models/personName.model.js (90%) create mode 100644 models/sql/po/patient.po.js rename {plugins => models}/sql/vrTypeMapping.js (100%) delete mode 100644 plugins/store-sql/index.js diff --git a/api-sql/dicom-web/controller/STOW-RS/service/dicom-jpeg-generator.js b/api-sql/dicom-web/controller/STOW-RS/service/dicom-jpeg-generator.js new file mode 100644 index 00000000..2b5b4b3f --- /dev/null +++ b/api-sql/dicom-web/controller/STOW-RS/service/dicom-jpeg-generator.js @@ -0,0 +1,74 @@ +const fs = require("fs"); +const { Dcm2JpgExecutor$Dcm2JpgOptions } = require("../../../../../models/DICOM/dcm4che/wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor$Dcm2JpgOptions"); +const colorette = require("colorette"); +const { DicomJpegGenerator } = require("@root/api/dicom-web/controller/STOW-RS/service/dicom-jpeg-generator"); +/** + * @typedef JsDcm2JpegTask + * @property {Dcm2JpgExecutor$Dcm2JpgOptions} jsDcm2Jpeg + * @property {string} jpegFilename + */ + +class SqlDicomJpegGenerator extends DicomJpegGenerator { + /** + * + * @param {import("../../../../../models/DICOM/dicom-json-model").DicomJsonModel} dicomJsonModel + * @param {string} dicomInstanceFilename + */ + constructor(dicomJsonModel, dicomInstanceFilename) { + super(dicomJsonModel, dicomInstanceFilename); + } + + + + /** + * @private + */ + async insertStartTask_() { + let startTaskObj = { + studyUID: this.dicomJsonModel.uidObj.studyUID, + seriesUID: this.dicomJsonModel.uidObj.seriesUID, + instanceUID: this.dicomJsonModel.uidObj.sopInstanceUID, + status: false, + message: "processing", + taskTime: new Date(), + finishedTime: null, + fileSize: `${(fs.statSync(this.dicomInstanceFilename).size / 1024 / 1024).toFixed(3)}MB` + }; + + + } + + /** + * @private + */ + async insertEndTask_() { + let endTaskObj = { + studyUID: this.dicomJsonModel.uidObj.studyUID, + seriesUID: this.dicomJsonModel.uidObj.seriesUID, + instanceUID: this.dicomJsonModel.uidObj.sopInstanceUID, + status: true, + message: "generated", + finishedTime: new Date() + }; + + } + + /** + * @private + * @param {string} message + */ + async insertErrorTask_(message) { + let errorTaskObj = { + studyUID: this.dicomJsonModel.uidObj.studyUID, + seriesUID: this.dicomJsonModel.uidObj.seriesUID, + instanceUID: this.dicomJsonModel.uidObj.sopInstanceUID, + status: false, + message: message, + finishedTime: new Date() + }; + + } + +} + +module.exports.SqlDicomJpegGenerator = SqlDicomJpegGenerator; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/STOW-RS/service/request-multipart-parser.js b/api-sql/dicom-web/controller/STOW-RS/service/request-multipart-parser.js new file mode 100644 index 00000000..c10c8af3 --- /dev/null +++ b/api-sql/dicom-web/controller/STOW-RS/service/request-multipart-parser.js @@ -0,0 +1,47 @@ +const formidable = require("formidable"); +const path = require("path"); +const _ = require("lodash"); + +class StowRsRequestMultipartParser { + /** + * @param {import('express').Request} req + */ + constructor(req) { + this.request = req; + } + + /** + * + * @return {Promise} + */ + async parse() { + return new Promise((resolve, reject) => { + new formidable.IncomingForm({ + uploadDir: path.join(process.cwd(), "/tempUploadFiles"), + maxFileSize: 100 * 1024 * 1024 * 1024, + multiples: true, + isGetBoundaryInData: true + }).parse(this.request, async (err, fields, files) => { + + if (err) { + return reject(err); + } + + let fileField = Object.keys(files).pop(); + let uploadFiles = files[fileField]; + if (!_.isArray(uploadFiles)) uploadFiles = [uploadFiles]; + + return resolve({ + status: true, + multipart: { + fields: fields, + files: uploadFiles + } + }); + + }); + }); + } +} + +module.exports.StowRsRequestMultipartParser = StowRsRequestMultipartParser; \ No newline at end of file diff --git a/plugins/store-sql/service/stow-rs.service.js b/api-sql/dicom-web/controller/STOW-RS/service/stow-rs.service.js similarity index 67% rename from plugins/store-sql/service/stow-rs.service.js rename to api-sql/dicom-web/controller/STOW-RS/service/stow-rs.service.js index 1541d4f4..1ad91a31 100644 --- a/plugins/store-sql/service/stow-rs.service.js +++ b/api-sql/dicom-web/controller/STOW-RS/service/stow-rs.service.js @@ -3,7 +3,8 @@ const _ = require("lodash"); const { DicomJsonParser } = require("@models/DICOM/dicom-json-parser"); const { StowRsService } = require("@root/api/dicom-web/controller/STOW-RS/service/stow-rs.service"); const { DicomFileSaver } = require("@root/api/dicom-web/controller/STOW-RS/service/dicom-file-saver"); -const { SqlDicomJsonModel: DicomJsonModel } = require("../../sql/models/dicom-json-model"); +const { SqlDicomJsonModel: DicomJsonModel } = require("@models/sql/dicom-json-model"); +const { SqlDicomJpegGenerator: DicomJpegGenerator } = require("./dicom-jpeg-generator"); class SqlStowRsService extends StowRsService { /** @@ -14,6 +15,35 @@ class SqlStowRsService extends StowRsService { super(req, uploadFiles); } + async storeInstances() { + for (let i = 0; i < this.uploadFiles.length; i++) { + + let currentFile = this.uploadFiles[i]; + + let { + dicomJsonModel, + dicomFileSaveInfo + } = await this.storeInstance(currentFile); + + + //sync DICOM to FHIR + // if (isSyncToFhir) { + // let dicomFhirService = new DicomFhirService(this.request, dicomJsonModel); + // await dicomFhirService.initDicomFhirConverter(); + // await dicomFhirService.postDicomToFhirServerAndStoreLog(); + // } + + //generate JPEG + // let dicomJpegGenerator = new DicomJpegGenerator(dicomJsonModel, dicomFileSaveInfo.instancePath); + // dicomJpegGenerator.generateAllFrames(); + } + + return { + code: this.responseCode, + responseMessage: this.responseMessage + }; + } + /** * * @param {import("formidable").File} file diff --git a/api-sql/dicom-web/controller/STOW-RS/storeInstance.js b/api-sql/dicom-web/controller/STOW-RS/storeInstance.js new file mode 100644 index 00000000..cec91a7c --- /dev/null +++ b/api-sql/dicom-web/controller/STOW-RS/storeInstance.js @@ -0,0 +1,68 @@ +const { performance } = require("node:perf_hooks"); +const errorResponseMessage = require("@root/utils/errorResponse/errorResponseMessage"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { Controller } = require("@root/api/controller.class"); +const { StowRsRequestMultipartParser } = require("./service/request-multipart-parser"); +const { SqlStowRsService: StowRsService } = require("./service/stow-rs.service"); + +class StoreInstanceController extends Controller { + constructor(req, res) { + super(req, res); + } + + async mainProcess() { + let startSTOWTime = performance.now(); + let retCode; + let storeMessage; + let apiLogger = new ApiLogger(this.request, "STOW-RS"); + apiLogger.addTokenValue(); + + try { + let requestMultipartParser = new StowRsRequestMultipartParser(this.request); + let multipartParseResult = await requestMultipartParser.parse(); + + if (multipartParseResult.status) { + let stowRsService = new StowRsService(this.request, multipartParseResult.multipart.files); + let storeInstancesResult = await stowRsService.storeInstances(); + + retCode = storeInstancesResult.code; + storeMessage = storeInstancesResult.responseMessage; + } + let endSTOWTime = performance.now(); + let elapsedTime = (endSTOWTime - startSTOWTime).toFixed(3); + apiLogger.logger.info(`Finished STOW-RS, elapsed time: ${elapsedTime} ms`); + + this.response.writeHead(retCode, { + "Content-Type": "application/dicom" + }); + + return this.response.end(JSON.stringify(storeMessage)); + } catch (e) { + let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e)); + apiLogger.logger.error(errorStr); + + let errorMessage = + errorResponseMessage.getInternalServerErrorMessage(errorStr); + this.response.writeHead(500, { + "Content-Type": "application/dicom+json" + }); + return this.response.end(JSON.stringify(errorMessage)); + } + } +} + + +/** + * To store DICOM instance + * 1. we parse multipart request to get file info that user upload + * 2. parse DICOM to JSON and store DICOM file from step 1 + * 3. parse DICOM json model to FHIR (Patient, Endpoint, ImagingStudy) + * 4. upload FHIR to FHIR server + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new StoreInstanceController(req, res); + + await controller.doPipeline(); +}; diff --git a/api-sql/dicom-web/stow-rs.route.js b/api-sql/dicom-web/stow-rs.route.js new file mode 100644 index 00000000..73b80eee --- /dev/null +++ b/api-sql/dicom-web/stow-rs.route.js @@ -0,0 +1,35 @@ +const express = require("express"); +const Joi = require("joi"); +const { validateParams, intArrayJoi } = require("@root/api/validator"); +const router = express(); + +//#region STOW-RS + +/** + * @openapi + * /dicom-web/studies: + * post: + * tags: + * - STOW-RS + * description: store DICOM instance + * requestBody: + * content: + * multipart/related: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * encoding: + * file: + * contentType: application/dicom; + * responses: + * "200": + * description: The DICOM instance store successfully + */ +router.post("/studies", require("./controller/STOW-RS/storeInstance")); + +//#endregion + +module.exports = router; \ No newline at end of file diff --git a/app.js b/app.js index 5833d74c..e1674a52 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,5 @@ // const mongodb = require("./models/mongodb/index"); -require("./plugins/sql/init"); +require("./models/sql/init"); const express = require("express"); const { createServer } = require("http"); const app = express(); diff --git a/plugins/sql/models/dicom-json-model.js b/models/sql/dicom-json-model.js similarity index 85% rename from plugins/sql/models/dicom-json-model.js rename to models/sql/dicom-json-model.js index 84218706..5ad6e706 100644 --- a/plugins/sql/models/dicom-json-model.js +++ b/models/sql/dicom-json-model.js @@ -1,6 +1,7 @@ const _ = require("lodash"); const { DicomJsonModel } = require("@models/DICOM/dicom-json-model"); +const { PatientPersistentObject } = require("./po/patient.po"); class SqlDicomJsonModel extends DicomJsonModel { @@ -32,8 +33,8 @@ class SqlDicomJsonModel extends DicomJsonModel { } async storePatientCollection(dicomJson) { - console.log(dicomJson); - console.log("TODO: Store Patient"); + let patientPo = new PatientPersistentObject(dicomJson); + let patient = await patientPo.createPatient(); } } diff --git a/plugins/sql/init.js b/models/sql/init.js similarity index 90% rename from plugins/sql/init.js rename to models/sql/init.js index 0c0e3f8b..b673d28b 100644 --- a/plugins/sql/init.js +++ b/models/sql/init.js @@ -10,7 +10,7 @@ async function init() { foreignKey: "x00100010" }); - await sequelizeInstance.sync({}); + await sequelizeInstance.sync({force: true}); } catch (e) { console.error('Unable to connect to the database:', e); process.exit(1); diff --git a/plugins/sql/instance.js b/models/sql/instance.js similarity index 100% rename from plugins/sql/instance.js rename to models/sql/instance.js diff --git a/plugins/sql/models/patient.model.js b/models/sql/models/patient.model.js similarity index 88% rename from plugins/sql/models/patient.model.js rename to models/sql/models/patient.model.js index 1cf9d10b..1737f7bc 100644 --- a/plugins/sql/models/patient.model.js +++ b/models/sql/models/patient.model.js @@ -1,5 +1,5 @@ const { Sequelize, DataTypes, Model } = require("sequelize"); -const sequelizeInstance = require("@root/plugins/sql/instance"); +const sequelizeInstance = require("@models/sql/instance"); const { vrTypeMapping } = require("../vrTypeMapping"); class PatientModel extends Model {}; @@ -23,9 +23,6 @@ PatientModel.init({ "x00100040": { type: vrTypeMapping.CS }, - "x00101001": { - type: vrTypeMapping.PN - }, "x00102160": { type: vrTypeMapping.SH }, diff --git a/plugins/sql/models/personName.model.js b/models/sql/models/personName.model.js similarity index 90% rename from plugins/sql/models/personName.model.js rename to models/sql/models/personName.model.js index 2bdf9bc9..2ed7e9af 100644 --- a/plugins/sql/models/personName.model.js +++ b/models/sql/models/personName.model.js @@ -1,5 +1,5 @@ const { Sequelize, DataTypes, Model } = require("sequelize"); -const sequelizeInstance = require("@root/plugins/sql/instance"); +const sequelizeInstance = require("@models/sql/instance"); class PersonNameModel extends Model {} diff --git a/models/sql/po/patient.po.js b/models/sql/po/patient.po.js new file mode 100644 index 00000000..dfcd6da2 --- /dev/null +++ b/models/sql/po/patient.po.js @@ -0,0 +1,68 @@ +const moment = require("moment"); +const _ = require("lodash"); +const { PersonNameModel } = require("../models/personName.model"); +const { PatientModel } = require("../models/patient.model"); +const { tagsNeedStore } = require("@models/DICOM/dicom-tags-mapping"); + + +class PatientPersistentObject { + constructor(dicomJson) { + this.json = {}; + Object.keys(tagsNeedStore.Patient).forEach(key => { + let value = _.get(dicomJson, key); + value ? _.set(this.json, key, value) : undefined; + }); + this.x00100010 = _.get(dicomJson, "00100010.Value.0", ""); + this.x00100020 = _.get(dicomJson, "00100020.Value.0", ""); + this.x00100021 = _.get(dicomJson, "00100021.Value.0", ""); + this.x00100030 = _.get(dicomJson, "00100030.Value.0", ""); + this.x00100032 = _.get(dicomJson, "00100032.Value.0", ""); + this.x00100040 = _.get(dicomJson, "00100040.Value.0", ""); + this.x00102160 = _.get(dicomJson, "00102160.Value.0", ""); + this.x00104000 = _.get(dicomJson, "00104000.Value.0", ""); + this.x00880130 = _.get(dicomJson, "00880130.Value.0", ""); + this.x00880140 = _.get(dicomJson, "00880140.Value.0", ""); + } + + async createPersonName() { + if (this.x00100010) { + return await PersonNameModel.create({ + alphabetic: _.get(this.x00100010, "Alphabetic", undefined), + ideographic: _.get(this.x00100010, "Ideographic", undefined), + phonetic: _.get(this.x00100010, "Phonetic", undefined) + }); + } + return undefined; + } + + async createPatient() { + let [patient, created] = await PatientModel.findOrCreate({ + where: { + x00100020: this.x00100020 + }, + defaults: { + json: this.json, + x00100020: this.x00100020, + x00100021: this.x00100021, + x00100030: this.x00100030 ? this.x00100030 : undefined, + x00100032: this.x00100032 ? Number(this.x00100032) : undefined, + x00100040: this.x00100040, + x00102160: this.x00102160, + x00104000: this.x00104000, + x00880130: this.x00880130, + x00880140: this.x00880140 + } + }); + + if (created) { + let personName = await this.createPersonName(); + patient.x00100010 = personName ? personName.id : undefined; + await patient.save(); + } + + return patient; + } + +} + +module.exports.PatientPersistentObject = PatientPersistentObject; \ No newline at end of file diff --git a/plugins/sql/vrTypeMapping.js b/models/sql/vrTypeMapping.js similarity index 100% rename from plugins/sql/vrTypeMapping.js rename to models/sql/vrTypeMapping.js diff --git a/plugins/store-sql/index.js b/plugins/store-sql/index.js deleted file mode 100644 index 53df6585..00000000 --- a/plugins/store-sql/index.js +++ /dev/null @@ -1,55 +0,0 @@ -const { performance } = require("node:perf_hooks"); -const errorResponseMessage = require("@root/utils/errorResponse/errorResponseMessage"); -const { ApiLogger } = require("@root/utils/logs/api-logger"); -const { StowRsRequestMultipartParser } = require("@root/api/dicom-web/controller/STOW-RS/service/request-multipart-parser"); -const { SqlStowRsService } = require("./service/stow-rs.service"); - - -/** - * To store DICOM instance - * 1. we parse multipart request to get file info that user upload - * 2. parse DICOM to JSON and store DICOM file from step 1 - * 3. parse DICOM json model to FHIR (Patient, Endpoint, ImagingStudy) - * 4. upload FHIR to FHIR server - * @param {import('express').Request} req - * @param {import('express').Response} res - */ -module.exports = async function (req, res) { - let startSTOWTime = performance.now(); - let retCode; - let storeMessage; - let apiLogger = new ApiLogger(req, "STOW-RS"); - apiLogger.addTokenValue(); - - try { - let requestMultipartParser = new StowRsRequestMultipartParser(req); - let multipartParseResult = await requestMultipartParser.parse(); - - if (multipartParseResult.status) { - let stowRsService = new SqlStowRsService(req, multipartParseResult.multipart.files); - let storeInstancesResult = await stowRsService.storeInstances(); - - retCode = storeInstancesResult.code; - storeMessage = storeInstancesResult.responseMessage; - } - let endSTOWTime = performance.now(); - let elapsedTime = (endSTOWTime - startSTOWTime).toFixed(3); - apiLogger.logger.info(`Finished STOW-RS, elapsed time: ${elapsedTime} ms`); - - res.writeHead(retCode, { - "Content-Type": "application/dicom" - }); - - return res.end(JSON.stringify(storeMessage)); - } catch (e) { - let errorStr = JSON.stringify(e, Object.getOwnPropertyNames(e)); - apiLogger.logger.error(errorStr); - - let errorMessage = - errorResponseMessage.getInternalServerErrorMessage(errorStr); - res.writeHead(500, { - "Content-Type": "application/dicom+json" - }); - return res.end(JSON.stringify(errorMessage)); - } -}; diff --git a/routes.js b/routes.js index a994920c..70fa56ea 100644 --- a/routes.js +++ b/routes.js @@ -19,7 +19,7 @@ module.exports = function (app) { loadAllPlugin(); - app.use("/dicom-web", require("./api/dicom-web/stow-rs.route")); + app.use("/dicom-web", require("./api-sql/dicom-web/stow-rs.route")); app.use("/dicom-web", require("./api/dicom-web/qido-rs.route")); app.use("/dicom-web", require("./api/dicom-web/wado-rs-instance.route")); app.use("/dicom-web", require("./api/dicom-web/wado-rs-metadata.route")); diff --git a/server.js b/server.js index 4d5118d9..fd685a37 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,7 @@ const compress = require("compression"); const cors = require("cors"); const os = require("os"); const SequelizeStore = require("connect-session-sequelize")(session.Store); -const sequelizeInstance = require("./plugins/sql/instance"); +const sequelizeInstance = require("./models/sql/instance"); const passport = require("passport"); const { raccoonConfig } = require("./config-class");