diff --git a/.env.template b/.env.template index 93d40865..2019778c 100644 --- a/.env.template +++ b/.env.template @@ -1,12 +1,12 @@ -# MongoDB -MONGODB_NAME="raccoon-dicom" -MONGODB_HOSTS=["mongodb"] -MONGODB_PORTS=[27017] -MONGODB_USER="root" -MONGODB_PASSWORD="root" -MONGODB_AUTH_SOURCE="admin" -MONGODB_OPTIONS="" -MONGODB_IS_SHARDING_MODE=false +# SQL +SQL_HOST="127.0.0.1" +SQL_PORT="5432" +SQL_DB="raccoon" +SQL_TYPE="postgres" +SQL_USERNAME="postgres" +SQL_PASSWORD="postgres" +SQL_LOGGING=false +SQL_FORCE_SYNC=false # Server SERVER_PORT=8081 diff --git a/.gitignore b/.gitignore index 069815de..9d34e050 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ config/ae-prod.properties # ignore jscpd output report -/report \ No newline at end of file +/report + +/jsconfig.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..01d5a6fd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,233 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [1.2.0](https://github.com/Chinlinlee/raccoon-dicom/compare/v1.1.0...v1.2.0) (2023-06-17) + + +### Features + +* [#11](https://github.com/Chinlinlee/raccoon-dicom/issues/11) ([7d2d9ed](https://github.com/Chinlinlee/raccoon-dicom/commit/7d2d9ed000d32bbc87aade7d0a16a1eef9d66a20)) + + +### Bug Fixes + +* wrong config filename of plugin of checking ([4b6de1a](https://github.com/Chinlinlee/raccoon-dicom/commit/4b6de1aa70d02e2e71ff5d86c7da0b57d134f247)) + +## [1.1.0](https://github.com/Chinlinlee/raccoon-dicom/compare/v1.0.1...v1.1.0) (2023-06-15) + + +### Features + +* [#10](https://github.com/Chinlinlee/raccoon-dicom/issues/10) ([f617273](https://github.com/Chinlinlee/raccoon-dicom/commit/f617273cecdb7c764f45e48d76135590d6a7ca9f)) +* add `update workitem` API ([6d921bb](https://github.com/Chinlinlee/raccoon-dicom/commit/6d921bbdcd0983ea190985d47b91e47cf7e5f15b)) +* add base dcm2dcm ([d71d895](https://github.com/Chinlinlee/raccoon-dicom/commit/d71d895f9b221850a621b2d8e8267c771b2efbdf)) +* add dicom error status ([eb69a59](https://github.com/Chinlinlee/raccoon-dicom/commit/eb69a59e18a2ad3df8627008de8a9b2ca2074b03)) +* add MONGODB_OPTIONS to .env ([bbfdd11](https://github.com/Chinlinlee/raccoon-dicom/commit/bbfdd1154f0e5bbcb007bff413faa972c860c36f)) +* add request query to string in controller ([2695ccd](https://github.com/Chinlinlee/raccoon-dicom/commit/2695ccdc82a1bedd892e61c1a47d1bb735eee258)) +* add un subscription API ([28e35f4](https://github.com/Chinlinlee/raccoon-dicom/commit/28e35f41dd81d6daa66901e469677ec155a01607)) +* add update event for subscription ([35347fe](https://github.com/Chinlinlee/raccoon-dicom/commit/35347fe2ab0573f2e01d7f62f8510f6121f739c6)) +* add ups cancel request API ([1d2c07a](https://github.com/Chinlinlee/raccoon-dicom/commit/1d2c07a5b2b4b85ed373960eaa0a93bbfcb511f9)) +* change state event ([08ae3f2](https://github.com/Chinlinlee/raccoon-dicom/commit/08ae3f289207c946934dadd9896c44eae9130bb2)) +* change workitem state API ([eb6ed91](https://github.com/Chinlinlee/raccoon-dicom/commit/eb6ed9187bdcd61a8cbd5081e09c77a0662c5f55)) +* check DIMSE is setup or not ([bb30a34](https://github.com/Chinlinlee/raccoon-dicom/commit/bb30a34b9ad2618d5a4af1b7ce922216821fbb23)) +* global subscription, event for creating ([c985d94](https://github.com/Chinlinlee/raccoon-dicom/commit/c985d948adc48496f693c8487d4d13cd9cac8d81)) +* **qido-rs:** support time query ([9d7ec94](https://github.com/Chinlinlee/raccoon-dicom/commit/9d7ec9468031da3d9ca938dd5d21694c1633b610)) +* retrieve UPS by instance UID ([2712733](https://github.com/Chinlinlee/raccoon-dicom/commit/2712733aecf9ab6ba17db979ab456ed870eaaa3d)) +* subscription API ([98b3f34](https://github.com/Chinlinlee/raccoon-dicom/commit/98b3f345003fcefb0df11339322596be06370b8f)) +* suspend subscription and add API docs ([7952464](https://github.com/Chinlinlee/raccoon-dicom/commit/7952464b4e64ebf2322410fc24d439cd481db5ec)) +* throw error when query key invalid (+1 squashed commits) ([017c18e](https://github.com/Chinlinlee/raccoon-dicom/commit/017c18e674e4dff604fdea8acd9579b214e99d92)) +* **UPS-RS:** add create and get workitem APIs ([4d55216](https://github.com/Chinlinlee/raccoon-dicom/commit/4d552160d591d8f79af3315ea8765816a265006a)) +* use cors only in dev NODE_ENV ([c19d8b0](https://github.com/Chinlinlee/raccoon-dicom/commit/c19d8b0b1183ce691ddcf8abddfbf12d71a7ab1b)) + + +### Bug Fixes + +* [#6](https://github.com/Chinlinlee/raccoon-dicom/issues/6) ([9f1948a](https://github.com/Chinlinlee/raccoon-dicom/commit/9f1948a07a3109363c8c89abdadd0b11292709cb)) +* cannot cmove study just with UID ([1d8f7bb](https://github.com/Chinlinlee/raccoon-dicom/commit/1d8f7bb33b36523841163fc1663b167b98db42e2)) +* incorrect tag for transaction uid ([8137620](https://github.com/Chinlinlee/raccoon-dicom/commit/81376208de0a764efa9703d1f202ef4fa38dc58c)) +* invalid `subscribed` field in query matching ([e904b7c](https://github.com/Chinlinlee/raccoon-dicom/commit/e904b7cd6f5e9c2dd03e88d03fbfa6339f128f86)) +* joi validator return wrong value ([8dd66f3](https://github.com/Chinlinlee/raccoon-dicom/commit/8dd66f3e75eb9baaa2fac74baf0b5dc29dcb981b)) +* missing patient attribute in workitem ([f778760](https://github.com/Chinlinlee/raccoon-dicom/commit/f7787608085a34ff1c286ab7767c6588f9154b19)) +* not fire init events when create global sub ([f9fa5bd](https://github.com/Chinlinlee/raccoon-dicom/commit/f9fa5bd81791f7a1ce9ca6cfb670cbab33dc7f50)) +* not parse application/dicom+json to json ([8838616](https://github.com/Chinlinlee/raccoon-dicom/commit/88386168e7135e39da409cf714d6d2a07c011e22)) +* response wrong status when empty result ([20fc53c](https://github.com/Chinlinlee/raccoon-dicom/commit/20fc53c1757ec0c65b3a2bd239b1e812dea3d4ad)) +* should not present transaction UID in query ([3481b62](https://github.com/Chinlinlee/raccoon-dicom/commit/3481b628f8968639b121a9286411c68557205c16)) +* wrong way to do time query ([e270a6c](https://github.com/Chinlinlee/raccoon-dicom/commit/e270a6ce0c46c5659e1b587e1f4c142480314bed)) + + +### Build + +* default enable DIMSE in .env.template ([636e64f](https://github.com/Chinlinlee/raccoon-dicom/commit/636e64ff3529105585cffef1b7f0463ce33cfd3e)) +* **dimse:** update example config ([61a1904](https://github.com/Chinlinlee/raccoon-dicom/commit/61a190430b998eca91835b49c018113cc1ee4f03)) +* **docker:** install imagemagick ([c5443f4](https://github.com/Chinlinlee/raccoon-dicom/commit/c5443f498cd8a5366c7d4875cf22c13e3a659aa4)) +* **log4js:** default support pm2 ([0155745](https://github.com/Chinlinlee/raccoon-dicom/commit/01557457ce150d716e74125c50bee303ca42cea4)) +* update dcm4che bridge ts classes ([5858126](https://github.com/Chinlinlee/raccoon-dicom/commit/5858126853919c8c130e87991c355226c36f2b9c)) + +### [1.0.1](https://github.com/Chinlinlee/raccoon-dicom/compare/v1.0.0...v1.0.1) (2023-05-04) + + +### Bug Fixes + +* naming incorrect about `stow` ([aa8b074](https://github.com/Chinlinlee/raccoon-dicom/commit/aa8b074233908005994382acef03a90b8bdb854c)) + +## 1.0.0 (2023-05-04) + + +### Features + +* [#2](https://github.com/Chinlinlee/raccoon-dicom/issues/2) ([fa924a9](https://github.com/Chinlinlee/raccoon-dicom/commit/fa924a9253e63345cf641f585d889d5eabbc3977)) +* `Controller` for supporting plugin pre/post ([6248724](https://github.com/Chinlinlee/raccoon-dicom/commit/62487248700aea3aba48ba725023a49c67733d03)) +* add `DicomWebService` to handle common fns ([b636f48](https://github.com/Chinlinlee/raccoon-dicom/commit/b636f4885a9e3f836354d84d3d64e36c537994ab)) +* add `QIDO-RS` of study level ([99a6283](https://github.com/Chinlinlee/raccoon-dicom/commit/99a62839f9ae09077064df5551f3481384808fa9)) +* add `TM` tag's schema ([0ac51f1](https://github.com/Chinlinlee/raccoon-dicom/commit/0ac51f1dbb886ca584a84666b6aa7ee2f1e535e1)) +* add API log for rendered instances frames ([4a76246](https://github.com/Chinlinlee/raccoon-dicom/commit/4a76246592fb9694221539556064f3d9d01c53fb)) +* add API of query for series in a study ([a2eb251](https://github.com/Chinlinlee/raccoon-dicom/commit/a2eb251f2469a7470769d7c92796d13427cc6791)) +* add config to determine whether sync FHIR ([052d8c4](https://github.com/Chinlinlee/raccoon-dicom/commit/052d8c4fac1abcae276b92581f132c94c875c1cf)) +* add content-location header when retrieving ([6179073](https://github.com/Chinlinlee/raccoon-dicom/commit/6179073805b4066ca77268564aff8e78dc601ba6)) +* add custom error ([fd8c654](https://github.com/Chinlinlee/raccoon-dicom/commit/fd8c6549c933e3031faf6108914d2db6074af2d7)) +* add dcm4che tool (uft8 converter, dcm2jpg) ([730e0c0](https://github.com/Chinlinlee/raccoon-dicom/commit/730e0c057c151c7b9e995a6e54d76f8ba309c7a8)) +* add delete API of DICOM hierarchy ([66865bc](https://github.com/Chinlinlee/raccoon-dicom/commit/66865bc472d29369a40dc67ff46d7043012ce282)) +* add general API info log ([3dcd205](https://github.com/Chinlinlee/raccoon-dicom/commit/3dcd2052e88de6a2cb23892424ed3788028b3978)) +* add index for UIDs ([b801f86](https://github.com/Chinlinlee/raccoon-dicom/commit/b801f86d61dfb637371bb557c6007a2ea254cd13)) +* add instance level in a series of QIDO-RS ([baa5d4c](https://github.com/Chinlinlee/raccoon-dicom/commit/baa5d4c62d8bd9b14dcc8520058e49fe743ca6f0)) +* add IS type schema & refactor getVRSchema ([6ad47db](https://github.com/Chinlinlee/raccoon-dicom/commit/6ad47db2734d5ca03f160a8febe05a23f001772b)) +* add local uploader ([387620b](https://github.com/Chinlinlee/raccoon-dicom/commit/387620b34ba9a0c7ee33cfea87a7474d09de7ee4)) +* add log4js `api` logger config ([3ed65f5](https://github.com/Chinlinlee/raccoon-dicom/commit/3ed65f587fbd8111f83a2a88d109b79527fec72d)) +* add mediaStorageUID, ID ([7b77892](https://github.com/Chinlinlee/raccoon-dicom/commit/7b77892c42c25dfa509bd02723f6c9fa7ba1bde8)) +* add method to write buffer to multipart ([87c10fb](https://github.com/Chinlinlee/raccoon-dicom/commit/87c10fb4afd1e0c20cae43338c93f5d7420ba6ee)) +* add new error message of response ([30d9586](https://github.com/Chinlinlee/raccoon-dicom/commit/30d95861633c6c1e6f6ebe44a78eba30a246b336)) +* add new QIDO-RS transations ([70efb15](https://github.com/Chinlinlee/raccoon-dicom/commit/70efb156bf9e65e5c200f0479f25fc6b44eb9804)) +* add Patient schema, and store patient ([939e8a2](https://github.com/Chinlinlee/raccoon-dicom/commit/939e8a2a3ae7c589359b982181d23f96874f42b8)) +* add plugin mechanism ([7e57425](https://github.com/Chinlinlee/raccoon-dicom/commit/7e57425434895ae2f4562a09ae77178ebfc05835)) +* add python logger ([a0fd614](https://github.com/Chinlinlee/raccoon-dicom/commit/a0fd6146ca9bef039155370c5a54378cbabe05b3)) +* add Rendered Instance ([0f40a33](https://github.com/Chinlinlee/raccoon-dicom/commit/0f40a337c7a9a67e578435c1a0b8f7dac7410f01)) +* add Rendered Series ([5d4d09f](https://github.com/Chinlinlee/raccoon-dicom/commit/5d4d09ffa4e793582ef6f7d360eda2b815af0993)) +* add retrieve instance's metadata ([b0f9b09](https://github.com/Chinlinlee/raccoon-dicom/commit/b0f9b09309dc6cc55816ce904f43c62252d07528)) +* add retrieve rendered frames ([ca1d81c](https://github.com/Chinlinlee/raccoon-dicom/commit/ca1d81c768099d4d5cd31c1c9a8479fc14a8f378)) +* add retrieve series's instances metadata ([77aec48](https://github.com/Chinlinlee/raccoon-dicom/commit/77aec48ed25563c1a11a55484b23a785f0c19709)) +* add retrieve study's instances metadata ([5a01d9d](https://github.com/Chinlinlee/raccoon-dicom/commit/5a01d9db8dd3357a9b0abbb57b695a000e7f81ec)) +* add retrieve study's instances of `WADO-RS` ([3c83337](https://github.com/Chinlinlee/raccoon-dicom/commit/3c83337313362c1d9e2d91db5bc72c1f4a6f12eb)) +* add retrieve study's series' instances ([1b0f235](https://github.com/Chinlinlee/raccoon-dicom/commit/1b0f235e047d387bf3868c3ce8d32c12afd1ab20)) +* add retrieve thumbnail APIs ([3bcc342](https://github.com/Chinlinlee/raccoon-dicom/commit/3bcc342169a49a4086cea71b067d12bb5de36505)) +* add swagger-jsdoc to generate openapi ([5b13539](https://github.com/Chinlinlee/raccoon-dicom/commit/5b1353983345c6eeede28844ac51a44a5c222869)) +* add url util function ([4971704](https://github.com/Chinlinlee/raccoon-dicom/commit/4971704cad77ca50b4a77ddb15d2e7511e54182d)) +* append `.dcm` of uploaded file ([a76eebf](https://github.com/Chinlinlee/raccoon-dicom/commit/a76eebfbdaa3baf702d171a5bc647a5c7bc122be)) +* change method of query sutdy ([9e3c0fd](https://github.com/Chinlinlee/raccoon-dicom/commit/9e3c0fd7fdce43a5fb543a57a5b25735ba2f28be)) +* config class to handle dotenv file ([ce47a4c](https://github.com/Chinlinlee/raccoon-dicom/commit/ce47a4c2be533b2e21a261c8c38b1946c4592475)) +* create FHIR resource when doing `STOW-RS` ([b381036](https://github.com/Chinlinlee/raccoon-dicom/commit/b381036c8a3ec6c5a431a8094faecf404d7e4f45)) +* **dcm2json:** correct DICOM when missing charset ([ad1f4d1](https://github.com/Chinlinlee/raccoon-dicom/commit/ad1f4d1c62ffbd20cbc575a01898c9cb5709b297)) +* dcm4che QRSCP ([75236be](https://github.com/Chinlinlee/raccoon-dicom/commit/75236be29338409c5ec50dc8e7034c6cf84b20b4)) +* emit background event status in `STOW-RS` ([3baf758](https://github.com/Chinlinlee/raccoon-dicom/commit/3baf758c6edb1cd5dac74db3abf85c33985ab9df)) +* flexible QIDO-RS 00081190 ([18988e5](https://github.com/Chinlinlee/raccoon-dicom/commit/18988e580472db8e0bacc204a2a939f79ceb6ca7)) +* generate API doc in docs/swagger folder ([d8ac79e](https://github.com/Chinlinlee/raccoon-dicom/commit/d8ac79e3224c278033d557b605a7da82a2c13423)) +* generate jpeg of DICOM file when `STOW-RS` ([51fbd36](https://github.com/Chinlinlee/raccoon-dicom/commit/51fbd3613d356df89438fb70ec4a879b121a4e52)) +* get DICOM frame image by dcmtk ([eeb7a76](https://github.com/Chinlinlee/raccoon-dicom/commit/eeb7a7607f996829f8bd80627131653e6fb303e8)) +* get DICOM frame image by python ([d994f88](https://github.com/Chinlinlee/raccoon-dicom/commit/d994f8881de2bfbc1e984eae5b977ddf7a4418eb)) +* get other fields in `getInstanceFrameObj` ([af5d59d](https://github.com/Chinlinlee/raccoon-dicom/commit/af5d59d9690920cb67959daa5ad57e16e5d40e81)) +* get{level}DicomJson for storing into mongo ([7ceb1bb](https://github.com/Chinlinlee/raccoon-dicom/commit/7ceb1bb016320bad503107f5e13f4e26e953cd26)) +* log processing of syncing DIOCM FHIR ([e549625](https://github.com/Chinlinlee/raccoon-dicom/commit/e54962511a18195c2023cf6ff4d725fa7c99db50)) +* make dcm2jpg to static object ([215072c](https://github.com/Chinlinlee/raccoon-dicom/commit/215072c4c660d6313b63dd0a949832e4c78c9c61)) +* multiple processes fn to one pipeline fn ([8e9e41d](https://github.com/Chinlinlee/raccoon-dicom/commit/8e9e41d93febfeb872c79c02ea286d14c3b52efd)) +* QIDO-RS patients ([ea2bffd](https://github.com/Chinlinlee/raccoon-dicom/commit/ea2bffdb5ebdc32942bb84cec86f68906470ff6d)) +* remove code about dcmtk and python ([ae5ceac](https://github.com/Chinlinlee/raccoon-dicom/commit/ae5ceac6bd28b2fd7c69df98d4921bf4ab311473)), closes [#2](https://github.com/Chinlinlee/raccoon-dicom/issues/2) +* remove user feature ([c494d36](https://github.com/Chinlinlee/raccoon-dicom/commit/c494d367c62180d89e164312ce7bcb534fac9640)) +* replace `00101000` to `00101002` ([4469096](https://github.com/Chinlinlee/raccoon-dicom/commit/4469096bc2ce25f538ecb68c97666d2e25fd2e9e)) +* response dicom multipart when `*` accept ([659c953](https://github.com/Chinlinlee/raccoon-dicom/commit/659c953a2a04edb015f7e30039145f2c04f23f2c)) +* response not found when get inexist DICOM ([dcd90ca](https://github.com/Chinlinlee/raccoon-dicom/commit/dcd90ca643e1c1e38111e2519ae5916887f4bc2b)) +* retrieve instance and move response method ([616cde2](https://github.com/Chinlinlee/raccoon-dicom/commit/616cde2258506559ecdabe4683bbbc8cde8535c4)) +* retrieve study's rendered instances ([bf7919d](https://github.com/Chinlinlee/raccoon-dicom/commit/bf7919d825db353096e4eaeee356b7a792e9f79d)) +* seperate dicom schema to 3 schemas ([e7684c1](https://github.com/Chinlinlee/raccoon-dicom/commit/e7684c19fd576dd51e4fa189111be8beafb1f591)) +* store bulk data url instead of binary ([37f94a6](https://github.com/Chinlinlee/raccoon-dicom/commit/37f94a6650328724d8837834c2fc889ed82e8f88)) +* **stow-rs:** calc and store additional tag ([4ae378c](https://github.com/Chinlinlee/raccoon-dicom/commit/4ae378c40e9cc08070d154a87cfb85127f8a04a3)) +* support `?` match any single character ([3845e38](https://github.com/Chinlinlee/raccoon-dicom/commit/3845e389b7df545d118f376831d3dd02f5fc0d50)) +* support `bulkdata` of DICOMweb API ([1dd1a23](https://github.com/Chinlinlee/raccoon-dicom/commit/1dd1a23956e0c4520d6a865b4717df24bfe22e64)) +* support `iccprofile` ([1a762b0](https://github.com/Chinlinlee/raccoon-dicom/commit/1a762b0240671d21c99b1a7cf7e8bb30905af772)) +* support `includefield` ([f23606d](https://github.com/Chinlinlee/raccoon-dicom/commit/f23606d65d01ee63adfec110e1d4db47206ec6e0)), closes [/dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_8](https://github.com/Chinlinlee//dicom.nema.org/medical/dicom/current/output/html/part18.html/issues/sect_8) +* support comma-separated frame numbers ([5cf3e73](https://github.com/Chinlinlee/raccoon-dicom/commit/5cf3e736c994289cdd7bb6d93aafc0d6f3f1147a)) +* support log RegExp ([5677940](https://github.com/Chinlinlee/raccoon-dicom/commit/56779400925429c01e742b36a26cfbfa4fd5a09c)) +* support PN query ([61118d0](https://github.com/Chinlinlee/raccoon-dicom/commit/61118d04940fad83dd4eaa95dbd88574ade2ae3d)) +* support WADO-URI ([6f29b12](https://github.com/Chinlinlee/raccoon-dicom/commit/6f29b12c95732dee0f12a8eef5cfff546629ffd2)) +* update `dicom` schema ([2a5f1e5](https://github.com/Chinlinlee/raccoon-dicom/commit/2a5f1e56550ab4c36e5ba68be9526dc999569175)) +* update dicom dictionary ([7d296da](https://github.com/Chinlinlee/raccoon-dicom/commit/7d296da7aed3086cb094ff2282de107c7f4a404b)) +* update DICOM elements dictionary ([b5a3c7d](https://github.com/Chinlinlee/raccoon-dicom/commit/b5a3c7db9ffb98c770a44eab0c53d68778de8491)) +* update store tag of study ([d95be91](https://github.com/Chinlinlee/raccoon-dicom/commit/d95be916f7b083964c1c83879fe1246744973732)) +* update store tags of series ([bcee96a](https://github.com/Chinlinlee/raccoon-dicom/commit/bcee96a407b95be17c356d74a9d401048e4bdb27)) +* use `cors()` instead of hardcode ([46dfb42](https://github.com/Chinlinlee/raccoon-dicom/commit/46dfb42637447dd6b77a6495dc92d094b716ec5e)) +* use `getBasicURL` instead of duplicate code ([d6e400e](https://github.com/Chinlinlee/raccoon-dicom/commit/d6e400e9ad1898068437d7dae7aeaa1c9e1afa71)) +* Use api logger appender instead of ApiLogger ([84f3621](https://github.com/Chinlinlee/raccoon-dicom/commit/84f3621680d2e2bdb068748e22ab4ed1bc760fd1)), closes [#1](https://github.com/Chinlinlee/raccoon-dicom/issues/1) +* use custom dcm2jpg wrapper ([fcdaedc](https://github.com/Chinlinlee/raccoon-dicom/commit/fcdaedc459bc806b8470c5a9b0813e6736cb9fbf)) +* use express instead of polka ([2e8dc0e](https://github.com/Chinlinlee/raccoon-dicom/commit/2e8dc0ea9f9c877442bf7b0ee7ebf7ad6c401722)) +* use reject instead of resolve false ([95e70d6](https://github.com/Chinlinlee/raccoon-dicom/commit/95e70d6e00023232bc2babe149b052d7159ed979)) +* validator can validate by custom joi object ([49ce0d6](https://github.com/Chinlinlee/raccoon-dicom/commit/49ce0d66c184e9b02db5f2dd2facd902e2623a6c)) +* **wado-rs:** send `content-length` of `zip` ([281927f](https://github.com/Chinlinlee/raccoon-dicom/commit/281927fbf9ef1d6b55df884fc351cf863d1a8faf)) +* **wado-uri:** [#5](https://github.com/Chinlinlee/raccoon-dicom/issues/5) ([a877cd4](https://github.com/Chinlinlee/raccoon-dicom/commit/a877cd49d7dbf802862597f4bf2203d58efc2055)) + + +### Bug Fixes + +* .env not load when start out of project path ([03834ce](https://github.com/Chinlinlee/raccoon-dicom/commit/03834ce2ca532ec501ba3631a8e2f66ba680ec38)) +* `ENV` env variable is not exists ([f80143c](https://github.com/Chinlinlee/raccoon-dicom/commit/f80143c3b0836e844fecd93758613f8d13b1594c)) +* `initQuery_` should after get limit and skip ([48bcb0d](https://github.com/Chinlinlee/raccoon-dicom/commit/48bcb0d289fe30c74d4c949440dc87b8674eca67)) +* add eslint and run it ([96ce2aa](https://github.com/Chinlinlee/raccoon-dicom/commit/96ce2aa9bf5b39b6eb7097f56a1f3028255e7e08)) +* BulkDataURI's UIDs undefined ([e0602a9](https://github.com/Chinlinlee/raccoon-dicom/commit/e0602a965ac26ef8feef477852f48f3b963e83e8)) +* cannot store value of VR `SQ` ([cf44360](https://github.com/Chinlinlee/raccoon-dicom/commit/cf443603c9d6266eac5bd29c710f870c2f629434)) +* cmove incorrect in study and instance level ([27ec8e0](https://github.com/Chinlinlee/raccoon-dicom/commit/27ec8e0ad42990d3f14caf82f94ee3ffdae330db)) +* DICOM dictionary.dicom is undefined ([fbc96d7](https://github.com/Chinlinlee/raccoon-dicom/commit/fbc96d7155b7d42f37aad0d72e28d7f7b32d58ae)) +* doc null in post findOneAndUpdate function ([c80d5d0](https://github.com/Chinlinlee/raccoon-dicom/commit/c80d5d0a9e77d13aee9782f725c9aaf9eeeae124)) +* docs.pop().pathList may undefined ([4a428d3](https://github.com/Chinlinlee/raccoon-dicom/commit/4a428d355afa31092b516eaf7374263515aa76a7)) +* forget to replace with `doPipeline` to multiple processes ([fa19b70](https://github.com/Chinlinlee/raccoon-dicom/commit/fa19b7035a977f516e91e757c75b50c85ef9374f)) +* frame number apply in `postProcessFrameImage` ([de2dd1a](https://github.com/Chinlinlee/raccoon-dicom/commit/de2dd1a5fb2605da20a35cb49eb0b48618650dc4)) +* incorrect date value store in DB ([365592f](https://github.com/Chinlinlee/raccoon-dicom/commit/365592fa29055da34f6abe2641beaa418a5d5ea3)) +* incorrect property ([5b0eb6c](https://github.com/Chinlinlee/raccoon-dicom/commit/5b0eb6c512de48467a009ca3e11c2b99497af23c)) +* incorrect property of object ([c85f999](https://github.com/Chinlinlee/raccoon-dicom/commit/c85f9997d8e63950dbdbad86f5568fd5c620ba49)) +* incorrect url value of study-series-instance ([c0d5aba](https://github.com/Chinlinlee/raccoon-dicom/commit/c0d5aba5c7aa954116d63110b0a0973d38ea3ae8)) +* incorrect way to convert 00080061 to FHIR ([6876174](https://github.com/Chinlinlee/raccoon-dicom/commit/68761746e81e7e536a7272f49b0d588b3255813d)) +* incorrect way to validate windowLevel's params ([161362e](https://github.com/Chinlinlee/raccoon-dicom/commit/161362e8042801d900c6088271c12987312ee6b0)) +* invisible character in dicom dictionary ([a73cd86](https://github.com/Chinlinlee/raccoon-dicom/commit/a73cd8697a84694bc281d5d7084948d23e3350a7)) +* missing 404 in retrieve rendered instance ([8e70d79](https://github.com/Chinlinlee/raccoon-dicom/commit/8e70d790c6914e52592e0bb768b3143097516584)) +* missing baseUrl in BulkDataURI ([07d4cad](https://github.com/Chinlinlee/raccoon-dicom/commit/07d4cad8c01fbfe2f3eef0821afbe4aa8d634475)) +* missing end res in retrieve instances ([6b5759b](https://github.com/Chinlinlee/raccoon-dicom/commit/6b5759b7f99de38616dc1a271ebc3c4f9ec60377)) +* missing unset `InlineBinary` ([73a2e24](https://github.com/Chinlinlee/raccoon-dicom/commit/73a2e24305b8456bf8fa4da7dcd5f9e7e3ad4832)) +* mongoose drop the QIDO filter ([d210647](https://github.com/Chinlinlee/raccoon-dicom/commit/d2106470a26e1a44416751d49ad18dca794b0386)) +* not allowed empty routers in plugin ([2fe1fc4](https://github.com/Chinlinlee/raccoon-dicom/commit/2fe1fc4653c7103fd9c75b51df72168e7cbdf2d3)) +* not response 204 when empty matched document ([4d8b4a1](https://github.com/Chinlinlee/raccoon-dicom/commit/4d8b4a18ece1fe75d30ad551087a1a18bf22443e)) +* object present _id in PN VR in MongoDB ([4b73691](https://github.com/Chinlinlee/raccoon-dicom/commit/4b7369195df56cef70966dbc6ec5fb31517642a8)) +* query date of end of day `yyyy-mm-dd` ([40b8ce4](https://github.com/Chinlinlee/raccoon-dicom/commit/40b8ce47abdbd0217919bebdbc21a43e18bc8a8e)) +* store incorrect instance path on linux ([807ff60](https://github.com/Chinlinlee/raccoon-dicom/commit/807ff60d256bcfd40f060d689d01dd4efa3bc50e)) +* study has `__v` field ([4f92965](https://github.com/Chinlinlee/raccoon-dicom/commit/4f92965d7404ed792de591f9a3076a53e5f925cc)) +* study level of exist imagingStudy not update ([0cebc85](https://github.com/Chinlinlee/raccoon-dicom/commit/0cebc858dc218b56d3de5d812df13dd517c9a284)) +* the instance store path is not relative ([c2c671e](https://github.com/Chinlinlee/raccoon-dicom/commit/c2c671e64763aa94d6ca1c0fb013b616cfde5151)) +* the VR of `BulkDataURI` is not `UR` ([d24adff](https://github.com/Chinlinlee/raccoon-dicom/commit/d24adffe0c73bedfb0abe6dc11a83a8d510883c9)) +* typo ([629e221](https://github.com/Chinlinlee/raccoon-dicom/commit/629e22196062fe333bbe303b1c63032c066a3a54)) +* typo ([15ca32f](https://github.com/Chinlinlee/raccoon-dicom/commit/15ca32f926691e138767927055de45ba9603dd88)) +* wado retrieve not exist instance must be 404 ([93596fd](https://github.com/Chinlinlee/raccoon-dicom/commit/93596fdc374b6b1182040445c3c2bd4de038e922)), closes [/dicom.nema.org/medical/dicom/current/output/html/part18.html#table_9](https://github.com/Chinlinlee//dicom.nema.org/medical/dicom/current/output/html/part18.html/issues/table_9) [/dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10](https://github.com/Chinlinlee//dicom.nema.org/medical/dicom/current/output/html/part18.html/issues/table_10) +* wrong `00081190` in `instance` level ([a3244e4](https://github.com/Chinlinlee/raccoon-dicom/commit/a3244e48076f89ab7e65e24941c2c3745dc4fab9)) + + +### Build + +* add `dicom.dic` for dcmtk ([d2cb179](https://github.com/Chinlinlee/raccoon-dicom/commit/d2cb179f8908b0e93bab5fbea586f4ebe31713ef)) +* add `FHIRSERVER_BASE_URL` in dotenv ([638d35a](https://github.com/Chinlinlee/raccoon-dicom/commit/638d35aa338755fedd1cdf4ff30cd631e8a18c48)) +* add test workflows ([c62b6d2](https://github.com/Chinlinlee/raccoon-dicom/commit/c62b6d2e18cf6a64699e4ceb18d7e12c989547df)) +* **ci:** add function test ([410728d](https://github.com/Chinlinlee/raccoon-dicom/commit/410728de016559bf06860bb3b14bb0201904e75d)) +* **ci:** disable dimse ([52519bf](https://github.com/Chinlinlee/raccoon-dicom/commit/52519bf45d771e8e9af12fcfd74ef88b4ff39d04)) +* **ci:** fix cp library path of dcm4che ([be40d11](https://github.com/Chinlinlee/raccoon-dicom/commit/be40d11f0188736ce5109fdccf980b96943d59fb)) +* **ci:** fix libssl not found ([82dd916](https://github.com/Chinlinlee/raccoon-dicom/commit/82dd91639ae1db77795b01b88b7a5a46441807f4)) +* **ci:** fix missing create .env ([f9d6e88](https://github.com/Chinlinlee/raccoon-dicom/commit/f9d6e88322e556c236b45e0ebeeeb93b38e53341)) +* **ci:** fix permission denied of DICOM root path ([f31b01d](https://github.com/Chinlinlee/raccoon-dicom/commit/f31b01d82d270e635f8cffde571bdfdf3cc32eba)) +* **ci:** missing echo to file ([9eab50c](https://github.com/Chinlinlee/raccoon-dicom/commit/9eab50c0dc52f15989da2d6213f0afafd8eb252f)) +* **ci:** standard version ([f027d0b](https://github.com/Chinlinlee/raccoon-dicom/commit/f027d0b9eb9443d92bc0cb3a46a26dbaec0c8198)) +* fix missing `}` ([7805e59](https://github.com/Chinlinlee/raccoon-dicom/commit/7805e594481bb656541709e0333ee24320356c64)) +* fix token name ([51a01e5](https://github.com/Chinlinlee/raccoon-dicom/commit/51a01e51ce6a45db9e807c2836a8726586969e77)) +* linux-x86-64, windows-x86-64 dcm4che's lib ([9ffb814](https://github.com/Chinlinlee/raccoon-dicom/commit/9ffb81441a73d5b10c34bd11156a170c4823ab6b)) +* **pm2:** increase `max_memory_restart` to 4G ([fcb8cf5](https://github.com/Chinlinlee/raccoon-dicom/commit/fcb8cf56419bba7f9ddaa3ad2d030cdf4dc777ce)) +* remove [@main](https://github.com/main) in raccoon-test ([7fca491](https://github.com/Chinlinlee/raccoon-dicom/commit/7fca49104b15774fa723b92aee8440cd3688510a)) +* remove test ([8b22238](https://github.com/Chinlinlee/raccoon-dicom/commit/8b2223883500408f3b5b55a5a156be7775a9ede1)) +* rename token to repo_token ([2b480a1](https://github.com/Chinlinlee/raccoon-dicom/commit/2b480a11b90bdd248390829b016d0d7305272e67)) +* specific test to main branch ([b0054e2](https://github.com/Chinlinlee/raccoon-dicom/commit/b0054e27c373c5412dceebdf371c8d46f28cc86e)) +* update .env.template ([7ed9be7](https://github.com/Chinlinlee/raccoon-dicom/commit/7ed9be713f52de283c70badc8b614e04ba6eeb2f)) +* update .env.template ([5bf6936](https://github.com/Chinlinlee/raccoon-dicom/commit/5bf69365bb77e136cea4431afd4fe68064959baf)) +* update .env.template ([c65f99c](https://github.com/Chinlinlee/raccoon-dicom/commit/c65f99cfd7faa590f8ab40f61fe739d224e48df0)) +* update `Dockerfile` and add docker-compose ([a936d00](https://github.com/Chinlinlee/raccoon-dicom/commit/a936d0010e0e4b9dd98b82862b279e286a85e310)) +* use PAT instead of GITHUB_TOKEN ([58cef54](https://github.com/Chinlinlee/raccoon-dicom/commit/58cef54d20f3572531eb871d9e7f3bbd4af10fbe)) diff --git a/README.md b/README.md index dcf5f73c..260be948 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,72 @@ -# raccoon-only-dicom -Another raccoon focus on dicom. The original raccoon combine the FHIR and DICOM together in MongoDB, I think it will cause performance and maintenance issues. So, here start a new project only for DICOM. +# raccoon-dicom + +

+ logo +

+
+ +Another Raccoon focus on DICOM. + +[English](README.md) | [繁體中文](README.zh-TW.md) + +--- + +**Raccoon-DICOM** is a noSQL-based medical image archive designed for managing DICOM images, utilizing MongoDB to store and manage the images while providing RESTful APIs that support [DICOMweb](https://www.dicomstandard.org/dicomweb/") protocols for querying, retrieving, and managing DICOM images. + +# Installation +- [Installation](https://github.com/Chinlinlee/raccoon-dicom/wiki/Installation) +- Step by Step guide to installing Raccoon-DICOM - Windows (WIP🚧) +- Step by Step guide to installing Raccoon-DICOM - Ubuntu (WIP🚧) + +# Troubleshooting on linux +- `Unknown VR: Tag not found in data dictionary` when using `STOW-RS` + - You need set the `DCMDICTPATH` environment variable + - The `dicom.dic` can find in the `/usr/share/libdcmtk{version}` or `./models/DICOM/dcmtk/dicom.dic` + > The {version} corresponds to dcmtk version, e.g. 3.6.5 => libdcmtk15 + + - Set `DCMDICTPATH` environment variable using command or you can add the command to profile file(`~/.bashrc`,`~/.profile` etc.), example **with dcmtk 3.6.5**: + ```sh + export DCMDICTPATH=/usr/share/libdcmtk15/dicom.dic + ``` + - Check the environment variable + ```sh + echo $DCMDICTPATH + ``` # Features The features implemented here: -- [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6) -- [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5) - - Convert DICOM to ImagingStudy, Endpoint, Patient of FHIR resources and sync FHIR resources to FHIR server -- [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4.1.1.1) - - [Retrieve Transaction Instance Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-1) - - [Retrieve Transaction Metadata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-2) - - [Retrieve Transaction Rendered Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-3) - - [Retrieve Transaction Bulkdata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1.5-1) - -# Environment Requirements -- node.js >= 16 -- Java JDK >= 11 - -> **Note** -> - You should copy opencv_java library to JDK's lib directory -> - In windows, copy `opencv_java.dll` -> - In linux, copy `libclib_jiio.so` and `libopencv_java.so` +## [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6) +### Support Format (Media Types) + +Format | Support | +---------|----------| + application/dicom+json | ✅ | + multipart/related; type="application/dicom+xml | ❌ | + +### Support Query Parameter + +Query Parameter | Support | +---------|----------| + fuzzymatching | ❌ | + includefield | ✅ | + limit | ✅ | + offset | ✅ | + + +## [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5) +- You can set `SYCN_TO_FHIR_SERVER=true` in .env to convert DICOM to ImagingStudy, Endpoint, Patient of FHIR resources and sync FHIR resources to FHIR server +## [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4.1.1.1) +- [Retrieve Transaction Instance Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-1) +- [Retrieve Transaction Metadata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-2) +- [Retrieve Transaction Rendered Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-3) +- [Retrieve Transaction Thumbnail Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-4) +- [Retrieve Transaction Bulkdata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1.5-1) + # API Documentation - raccoon-dicom uses swagger ui hosting openapi.json to generate documentation - [API Documentation](https://chinlinlee.github.io/raccoon-dicom/) - +# Wiki +Our [wiki](https://github.com/Chinlinlee/raccoon-dicom/wiki) includes a lot of information about raccoon-dicom, we heavily encourage you to take a look!! diff --git a/README.zh-TW.md b/README.zh-TW.md new file mode 100644 index 00000000..6d1454e0 --- /dev/null +++ b/README.zh-TW.md @@ -0,0 +1,72 @@ +# raccoon-dicom + +

+ logo +

+
+ +Another Raccoon focus on DICOM. + +[English](README.md) | [繁體中文](README.zh-TW.md) + +--- + +**Raccoon-DICOM** 是使用 no-SQL 資料庫實作的醫學影像儲存系統(DICOMweb PACS),其使用 MongoDB 管理 DICOM 影像並提供 [DICOMweb](https://www.dicomstandard.org/dicomweb/") RESTful API 功能進行儲存、查詢以及調閱 + + +# 安裝 +- [安裝手冊](https://github.com/Chinlinlee/raccoon-dicom/wiki/Installation.zh-TW) +- [從 0 開始部屬 Raccoon - Windows](https://github.com/Chinlinlee/raccoon-dicom/wiki/From-zero-to-deploy.zh-TW): 在 Windows 上,一步一步從安裝到部屬 +- 從 0 開始部屬 Raccoon - Ubuntu (WIP🚧) + +# Troubleshooting on linux +- `Unknown VR: Tag not found in data dictionary` when using `STOW-RS` + - 您必須設定 `DCMDICTPATH` 環境變數 + - `dicom.dic` 檔案可以在`/usr/share/libdcmtk{version}`或 `./models/DICOM/dcmtk/dicom.dic`找到 + > {version} 對應到dcmtk的版本, e.g. 3.6.5 => libdcmtk15 + + - 使用指令設定 `DCMDICTPATH` 或者您可以將指令加入到profile檔案中(`~/.bashrc`,`~/.profile` etc.), example **with dcmtk 3.6.5**: + ```sh + export DCMDICTPATH=/usr/share/libdcmtk15/dicom.dic + ``` + - 檢查環境變數 + ```sh + echo $DCMDICTPATH + ``` + +# 提供之功能 +目前以實作的功能如下: +## [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6) +### 支援的格式 (Media Types) + +Format | Support | +---------|----------| + application/dicom+json | ✅ | + multipart/related; type="application/dicom+xml | ❌ | + +### 支援的查詢參數 + +Query Parameter | Support | +---------|----------| + fuzzymatching | ❌ | + includefield | ✅ | + limit | ✅ | + offset | ✅ | + + +## [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5) +- 您可以在 .env 設定 `SYCN_TO_FHIR_SERVER=true` 以將 DICOM 轉換為 FHIR ImagingStudy, Endpoint 以及 Patient,並同步這些 Resources 至 FHIR server +## [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4.1.1.1) +- [Retrieve Transaction Instance Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-1) +- [Retrieve Transaction Metadata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-2) +- [Retrieve Transaction Rendered Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-3) +- [Retrieve Transaction Thumbnail Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1-4) +- [Retrieve Transaction Bulkdata Resources](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#table_10.4.1.5-1) + + +# API Documentation +- raccoon-dicom uses swagger ui hosting openapi.json to generate documentation +- [API Documentation](https://chinlinlee.github.io/raccoon-dicom/) + +# Wiki +我們的[Wiki](https://github.com/Chinlinlee/raccoon-dicom/wiki)含有更多與 Raccoon-DICOM 更多的資訊,非常建議您閱讀一下 diff --git a/api-sql/WADO-URI/service/WADO-URI.service.js b/api-sql/WADO-URI/service/WADO-URI.service.js new file mode 100644 index 00000000..681070aa --- /dev/null +++ b/api-sql/WADO-URI/service/WADO-URI.service.js @@ -0,0 +1,99 @@ +const fs = require("fs"); +const _ = require("lodash"); +const renderedService = require("../../dicom-web/controller/WADO-RS/service/rendered.service"); +const { Dcm2JpgExecutor } = require("@models/DICOM/dcm4che/wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor"); +const { Dcm2JpgExecutor$Dcm2JpgOptions } = require("@models/DICOM/dcm4che/wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor$Dcm2JpgOptions"); +const sharp = require('sharp'); +const Magick = require("@models/magick"); +const { NotFoundInstanceError, InvalidFrameNumberError, InstanceGoneError } = require("@error/dicom-instance"); +const { WadoUriService } = require("@root/api/WADO-URI/service/WADO-URI.service"); +const { InstanceModel } = require("@models/sql/models/instance.model"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +class SqlWadoUriService extends WadoUriService{ + + /** + * + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + */ + constructor(req, res, apiLogger) { + super(req, res); + this.apiLogger = apiLogger; + } + + async getDicomInstancePathObj() { + let { + studyUID, + seriesUID, + objectUID: instanceUID + } = this.request.query; + + let imagePathObj = await InstanceModel.getPathOfInstance({ + studyUID, + seriesUID, + instanceUID + }); + + if (imagePathObj) { + + try { + await fs.promises.access(imagePathObj.instancePath, fs.constants.F_OK); + } catch(e) { + console.error(e); + throw new InstanceGoneError("The image is deleted permanently, but meta data remain"); + } + + return imagePathObj; + } + + throw new NotFoundInstanceError("Not found instance"); + } + + async handleFrameNumberAndGetImageObj() { + let imagePathObj = await this.getDicomInstancePathObj(); + let instanceFramesObj = await renderedService.getInstanceFrameObj(imagePathObj); + let instanceTotalFrameNumber = _.get(instanceFramesObj, "x00280008") ? _.get(instanceFramesObj, "x00280008") : 1; + + let windowCenter = _.get(instanceFramesObj, "x00281050.0", ""); + let windowWidth = _.get(instanceFramesObj, "x00281051.0", ""); + + let transferSyntax = _.get(instanceFramesObj, "x00020010"); + let frameNumber = parseInt(_.get(this.request.query, "frameNumber", 1)); + + if (frameNumber > instanceTotalFrameNumber) { + throw new InvalidFrameNumberError(`Invalid Frame Number, total ${instanceTotalFrameNumber}, but requested ${frameNumber}`); + } + + /** @type {Dcm2JpgExecutor$Dcm2JpgOptions} */ + let options = await Dcm2JpgExecutor$Dcm2JpgOptions.newInstanceAsync(); + options.frameNumber = frameNumber; + + if (windowCenter && windowWidth) { + options.windowCenter = windowCenter; + options.windowWidth = windowWidth; + } + + let dicomFilename = instanceFramesObj.instancePath; + let jpegFile = dicomFilename.replace(/\.dcm\b/gi , `.${frameNumber-1}.jpg`); + + let getFrameImageStatus = await Dcm2JpgExecutor.convertDcmToJpgFromFilename( + dicomFilename, + jpegFile, + options + ); + + if (getFrameImageStatus.status) { + + return { + imageSharp: sharp(jpegFile), + magick: new Magick(jpegFile) + }; + } + + throw new NotFoundInstanceError("Not found DICOM Instance's Jpeg, may convert error"); + } + +} + +module.exports.WadoUriService = SqlWadoUriService; +module.exports.NotFoundInstanceError = NotFoundInstanceError; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status.js b/api-sql/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status.js new file mode 100644 index 00000000..b07fb75b --- /dev/null +++ b/api-sql/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status.js @@ -0,0 +1,52 @@ +const { MwlItemModel } = require("@models/sql/models/mwlitems.model"); +const { ChangeFilteredMwlItemStatusService } = require("@root/api/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status"); +const { cloneDeep, set } = require("lodash"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); +const { MwlQueryBuilder } = require("./query/mwlQueryBuilder"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); + +class SqlChangeFilteredMwlItemStatusService extends ChangeFilteredMwlItemStatusService { + constructor(req, res) { + super(req, res); + } + + async changeMwlItemsStatus() { + let { status } = this.request.params; + let mwlItems = await this.getMwlItems(); + + if (mwlItems.length === 0) { + throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchObjectInstance, "Can not found any MWL item from query", 404); + } + + for (let mwlItem of mwlItems) { + mwlItem.status = status; + set(mwlItem.json, `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}.Value.0`, status); + mwlItem.changed("json", true); + await mwlItem.save(); + } + + return mwlItems.length; + } + + async getMwlItems() { + let queryBuilder = new MwlQueryBuilder({ + query: this.query + }); + let q = queryBuilder.build(); + return await MwlItemModel.findAll(q); + } + + 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, false); + } +} + +module.exports.ChangeFilteredMwlItemStatusService = SqlChangeFilteredMwlItemStatusService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/MWL-RS/service/change-mwlItem-status.js b/api-sql/dicom-web/controller/MWL-RS/service/change-mwlItem-status.js new file mode 100644 index 00000000..746738ae --- /dev/null +++ b/api-sql/dicom-web/controller/MWL-RS/service/change-mwlItem-status.js @@ -0,0 +1,44 @@ +const _ = require("lodash"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { ChangeMwlItemStatusService } = require("@root/api/dicom-web/controller/MWL-RS/service/change-mwlItem-status"); +const { MwlItemModel } = require("@models/sql/models/mwlitems.model"); +const { Op } = require("sequelize"); + +class SqlChangeMwlItemStatusService extends ChangeMwlItemStatusService { + constructor(req, res) { + super(req, res); + } + + async changeMwlItemsStatus() { + let { status } = this.request.params; + let mwlItem = await this.getMwlItemByStudyUIDAndSpsID(); + if (!mwlItem) { + throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchObjectInstance, "No such object instance", 404); + } + + mwlItem.sps_status = status; + _.set(mwlItem.json, `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}.Value.0`, status); + mwlItem.changed("json", true); + await mwlItem.save(); + + return mwlItem.json; + } + + async getMwlItemByStudyUIDAndSpsID() { + return await MwlItemModel.findOne({ + where: { + [Op.and]: [ + { + sps_id: this.request.params.spsID + }, + { + study_instance_uid: this.request.params.studyUID + } + ] + } + }); + } +} + +module.exports.ChangeMwlItemStatusService = SqlChangeMwlItemStatusService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/MWL-RS/service/count-mwlItem.service.js b/api-sql/dicom-web/controller/MWL-RS/service/count-mwlItem.service.js new file mode 100644 index 00000000..144cbd57 --- /dev/null +++ b/api-sql/dicom-web/controller/MWL-RS/service/count-mwlItem.service.js @@ -0,0 +1,27 @@ +const { MwlItemModel } = require("@models/sql/models/mwlitems.model"); +const { GetMwlItemCountService } = require("@root/api/dicom-web/controller/MWL-RS/service/count-mwlItem.service"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); +const { cloneDeep } = require("lodash"); + +class SqlGetMwlItemCountService extends GetMwlItemCountService{ + constructor(req, res) { + super(req, res); + } + + async getMwlItemCount() { + return await MwlItemModel.getCount(this.query); + } + + 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, false); + } +} + +module.exports.GetMwlItemCountService = SqlGetMwlItemCountService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/MWL-RS/service/create-mwlItem.service.js b/api-sql/dicom-web/controller/MWL-RS/service/create-mwlItem.service.js new file mode 100644 index 00000000..b9e30767 --- /dev/null +++ b/api-sql/dicom-web/controller/MWL-RS/service/create-mwlItem.service.js @@ -0,0 +1,40 @@ +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { PatientModel } = require("@models/sql/models/patient.model"); +const { CreateMwlItemService } = require("@root/api/dicom-web/controller/MWL-RS/service/create-mwlitem.service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { MwlItemPersistentObject } = require("@models/sql/po/mwlItem.po"); +const { DicomJsonModel } = require("@models/DICOM/dicom-json-model"); + +class SqlCreateMwlItemService extends CreateMwlItemService { + constructor(req, res) { + super(req, res); + this.requestMwlItemDicomJsonModel = new DicomJsonModel(this.requestMwlItem[0]); + } + + async checkPatientExist() { + let patientID = this.requestMwlItemDicomJsonModel.getString("00100020"); + let patientCount = await PatientModel.count({ + where: { + x00100020: patientID + } + }); + if (patientCount <= 0) { + throw new DicomWebServiceError( + DicomWebStatusCodes.MissingAttribute, + `Patient[id=${patientID}] does not exists`, + 404 + ); + } + } + + async createOrUpdateMwl(mwlDicomJson) { + let studyInstanceUID = mwlDicomJson.getValue(dictionary.keyword.StudyInstanceUID); + let patientID = this.requestMwlItemDicomJsonModel.getString("00100020"); + let mwlItemPO = new MwlItemPersistentObject(mwlDicomJson.dicomJson, await PatientModel.findOne({ where: { x00100020: patientID } })); + let mwlItem = await mwlItemPO.save(); + this.apiLogger.logger.info(`create mwl item: ${studyInstanceUID}`); + return mwlItem.json; + } +} + +module.exports.CreateMwlItemService = SqlCreateMwlItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/MWL-RS/service/get-mwlItem.service.js b/api-sql/dicom-web/controller/MWL-RS/service/get-mwlItem.service.js new file mode 100644 index 00000000..f6bf5fac --- /dev/null +++ b/api-sql/dicom-web/controller/MWL-RS/service/get-mwlItem.service.js @@ -0,0 +1,36 @@ +const { MwlItemModel } = require("@models/sql/models/mwlitems.model"); +const { GetMwlItemService } = require("@root/api/dicom-web/controller/MWL-RS/service/get-mwlItem.service"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); +const { cloneDeep } = require("lodash"); + +class SqlGetMwlItemService extends GetMwlItemService { + constructor(req, res) { + super(req, res); + } + + async getMwlItems() { + let queryOptions = { + query: this.query, + skip: this.skip_, + limit: this.limit_, + requestParams: this.request.params + }; + + let docs = await MwlItemModel.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, false); + } +} + +module.exports.GetMwlItemService = SqlGetMwlItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/MWL-RS/service/query/mwlQueryBuilder.js b/api-sql/dicom-web/controller/MWL-RS/service/query/mwlQueryBuilder.js new file mode 100644 index 00000000..31d04db5 --- /dev/null +++ b/api-sql/dicom-web/controller/MWL-RS/service/query/mwlQueryBuilder.js @@ -0,0 +1,227 @@ +const sequelize = require("@models/sql/instance"); +const { PatientQueryBuilder } = require("../../../QIDO-RS/service/patientQueryBuilder"); +const { BaseQueryBuilder } = require("../../../QIDO-RS/service/querybuilder"); +const { DicomCodeQueryBuilder } = require("../../../QIDO-RS/service/dicomCodeQueryBuilder"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); + +class MwlQueryBuilder extends BaseQueryBuilder { + constructor(queryOptions) { + super(queryOptions); + + let patientQueryBuilder = new PatientQueryBuilder(queryOptions); + let patientQuery = patientQueryBuilder.build(); + this.includeQueries.push({ + model: sequelize.model("Patient"), + attributes: ["x00100020"], + ...patientQuery, + required: true + }); + + this.createCodeQueries(dictionary.keyword.InstitutionalDepartmentTypeCodeSequence); + this.createCodeQueries(dictionary.keyword.InstitutionCodeSequence); + this.createCodeQueries(dictionary.keyword.ScheduledProtocolCodeSequence); + this.createIssuerOfAccessionNumberSequenceQueries(); + this.createIssuerOfAdmissionIdSequenceQueries(); + this.createSpsQueries(); + } + + getStudyInstanceUID(values) { + return this.getOrQuery( + dictionary.keyword.StudyInstanceUID, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + getAccessionNumber(values) { + return this.getOrQuery( + dictionary.keyword.AccessionNumber, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + getRequestedProcedureID(values) { + return this.getOrQuery( + dictionary.keyword.RequestedProcedureID, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + getAdmissionID(values) { + return this.getOrQuery( + dictionary.keyword.AdmissionID, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + getInstitutionName(values) { + return this.getOrQuery( + dictionary.keyword.InstitutionName, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + getInstitutionDepartmentName(values) { + return this.getOrQuery( + dictionary.keyword.InstitutionalDepartmentName, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + createCodeQueries(tag) { + let dicomCodeQueryBuilder = new DicomCodeQueryBuilder(this, dictionary.tag[tag]); + this[`${tag}.00080100`] = (values) => dicomCodeQueryBuilder.getCodeValue(values); + this[`${tag}.00080102`] = (values) => dicomCodeQueryBuilder.getCodingSchemeDesignator(values); + this[`${tag}.00080103`] = (values) => dicomCodeQueryBuilder.getCodingSchemeVersion(values); + this[`${tag}.00080104`] = (values) => dicomCodeQueryBuilder.getCodeMeaning(values); + } + + createIssuerOfAccessionNumberSequenceQueries() { + this[`${dictionary.keyword.IssuerOfAccessionNumberSequence}.${dictionary.keyword.LocalNamespaceEntityID}`] = (values) => { + return this.getOrQuery( + `accno_local_id`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.IssuerOfAccessionNumberSequence}.${dictionary.keyword.UniversalEntityID}`] = (values) => { + return this.getOrQuery( + `accno_universal_id`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.IssuerOfAccessionNumberSequence}.${dictionary.keyword.UniversalEntityIDType}`] = (values) => { + return this.getOrQuery( + `accno_universal_id_type`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + } + + createIssuerOfAdmissionIdSequenceQueries() { + this[`${dictionary.keyword.IssuerOfAdmissionIDSequence}.${dictionary.keyword.LocalNamespaceEntityID}`] = (values) => { + return this.getOrQuery( + `issuer_admission_local_id`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.IssuerOfAdmissionIDSequence}.${dictionary.keyword.UniversalEntityID}`] = (values) => { + return this.getOrQuery( + `issuer_admission_universal_id`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.IssuerOfAdmissionIDSequence}.${dictionary.keyword.UniversalEntityIDType}`] = (values) => { + return this.getOrQuery( + `issuer_admission_universal_id_type`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + } + + createSpsQueries() { + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.StationAETitle}`] = (values) => { + return this.getOrQuery( + `station_ae_title`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.StationName}`] = (values) => { + return this.getOrQuery( + `station_name`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepStartDate}`] = (values) => { + return this.getOrQuery( + `start_date`, + values, + BaseQueryBuilder.prototype.getDateQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepEndDate}`] = (values) => { + return this.getOrQuery( + `end_date`, + values, + BaseQueryBuilder.prototype.getDateQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepStartTime}`] = (values) => { + return this.getOrQuery( + `start_time`, + values, + BaseQueryBuilder.prototype.getTimeQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepEndTime}`] = (values) => { + return this.getOrQuery( + `end_time`, + values, + BaseQueryBuilder.prototype.getTimeQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledPerformingPhysicianName}`] = (values) => { + let q = this.getOrQuery( + `physician_name`, + values, + BaseQueryBuilder.prototype.getPersonNameQuery.bind(this) + ); + this.includeQueries.push({ + model: sequelize.model("PersonName"), + as: dictionary.tag["00400006"], + where: { + ...q + }, + attributes: [] + }); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepDescription}`] = (values) => { + return this.getOrQuery( + `description`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepID}`] = (values) => { + return this.getOrQuery( + `sps_id`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.ScheduledProcedureStepSequence}.${dictionary.keyword.ScheduledProcedureStepStatus}`] = (values) => { + return this.getOrQuery( + `sps_status`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + } +} + +module.exports.MwlQueryBuilder = MwlQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js b/api-sql/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js new file mode 100644 index 00000000..fdd27330 --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js @@ -0,0 +1,17 @@ +const _ = require("lodash"); + +const { QidoRsService } = require("@root/api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); + +QidoRsService.prototype.initQuery_ = function () { + 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, false); +}; + +module.exports.QidoRsService = QidoRsService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/dicomCodeQueryBuilder.js b/api-sql/dicom-web/controller/QIDO-RS/service/dicomCodeQueryBuilder.js new file mode 100644 index 00000000..6f03d413 --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/dicomCodeQueryBuilder.js @@ -0,0 +1,90 @@ +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { BaseQueryBuilder } = require("./querybuilder"); +const { DicomCodeModel } = require("@models/sql/models/dicomCode.model"); +const { Op } = require("sequelize"); + +class DicomCodeQueryBuilder { + constructor(queryBuilder, codeTableName) { + /** @type { import("../../UPS-RS/service/query/upsQueryBuilder") } */ + this.queryBuilder = queryBuilder; + this.codeTableName = codeTableName; + } + + isModelIncluded() { + this.queryBuilder.includeQueries.forEach(v=> console.log(v.model.getTableName())); + return this.queryBuilder.includeQueries.find(v => v.model.getTableName() === this.codeTableName || v.as === this.codeTableName); + } + + /** + * + * @param {string[]} values + */ + getCodeValue(values) { + let q = this.queryBuilder.getOrQuery( + dictionary.keyword.CodeValue, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.queryBuilder) + ); + this.addQuery(q); + } + + /** + * + * @param {string[]} values + */ + getCodingSchemeDesignator(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodingSchemeDesignator, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + /** + * + * @param {string[]} values + */ + getCodingSchemeVersion(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodingSchemeVersion, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + /** + * + * @param {string[]} values + */ + getCodeMeaning(values) { + let q = this.getOrQuery( + dictionary.keyword.CodeMeaning, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + addQuery(q) { + let currentCodeModel = this.isModelIncluded(); + if (currentCodeModel) { + currentCodeModel.where = { + ...currentCodeModel.where, + ...q + }; + } else { + this.queryBuilder.includeQueries.push({ + model: DicomCodeModel, + where: { + ...q + }, + as: this.codeTableName, + attributes: [] + }); + } + } +} + +module.exports.DicomCodeQueryBuilder = DicomCodeQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/instanceQueryBuilder.js b/api-sql/dicom-web/controller/QIDO-RS/service/instanceQueryBuilder.js new file mode 100644 index 00000000..655ce353 --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/instanceQueryBuilder.js @@ -0,0 +1,435 @@ +const _ = require("lodash"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { BaseQueryBuilder, StudyQueryBuilder } = require("./querybuilder"); +const { DicomCodeModel } = require("@models/sql/models/dicomCode.model"); +const { DicomContentSqModel } = require("@models/sql/models/dicomContentSQ.model"); +const { VerifyIngObserverSqModel } = require("@models/sql/models/verifyingObserverSQ.model"); +const { PersonNameModel } = require("@models/sql/models/personName.model"); +const sequelize = require("@models/sql/instance"); +const { SeriesQueryBuilder } = require("./seriesQueryBuilder"); +const { Op } = require("sequelize"); + +class InstanceQueryBuilder extends BaseQueryBuilder { + constructor(queryOptions) { + super(queryOptions); + + let conceptNameCodeQueryBuilder = new ConceptNameCodeSqQueryBuilder(this); + this["0040A043.00080100"] = ConceptNameCodeSqQueryBuilder.prototype.getCodeValue.bind(conceptNameCodeQueryBuilder); + this["0040A043.00080102"] = ConceptNameCodeSqQueryBuilder.prototype.getCodingSchemeDesignator.bind(conceptNameCodeQueryBuilder); + this["0040A043.00080103"] = ConceptNameCodeSqQueryBuilder.prototype.getCodingSchemeVersion.bind(conceptNameCodeQueryBuilder); + this["0040A043.00080104"] = ConceptNameCodeSqQueryBuilder.prototype.getCodeMeaning.bind(conceptNameCodeQueryBuilder); + + let contentQueryBuilder = new ContentSqQueryBuilder(this); + this["0040A730.0040A040"] = ContentSqQueryBuilder.prototype.getValueType.bind(contentQueryBuilder); + this["0040A730.0040A010"] = ContentSqQueryBuilder.prototype.getRelationshipType.bind(contentQueryBuilder); + this["0040A730.0040A160"] = ContentSqQueryBuilder.prototype.getTextValue.bind(contentQueryBuilder); + this["0040A730.0040A043.00080100"] = ContentSqQueryBuilder.prototype.getConceptNameCodeValue.bind(contentQueryBuilder); + this["0040A730.0040A043.00080102"] = ContentSqQueryBuilder.prototype.getConceptNameCodingSchemeDesignator.bind(contentQueryBuilder); + this["0040A730.0040A043.00080103"] = ContentSqQueryBuilder.prototype.getConceptNameCodingSchemeVersion.bind(contentQueryBuilder); + this["0040A730.0040A043.00080104"] = ContentSqQueryBuilder.prototype.getConceptNameCodeMeaning.bind(contentQueryBuilder); + this["0040A730.0040A168.00080100"] = ContentSqQueryBuilder.prototype.getConceptCodeValue.bind(contentQueryBuilder); + this["0040A730.0040A168.00080102"] = ContentSqQueryBuilder.prototype.getConceptCodingSchemeDesignator.bind(contentQueryBuilder); + this["0040A730.0040A168.00080103"] = ContentSqQueryBuilder.prototype.getConceptCodingSchemeVersion.bind(contentQueryBuilder); + this["0040A730.0040A168.00080104"] = ContentSqQueryBuilder.prototype.getConceptCodeMeaning.bind(contentQueryBuilder); + + let verifyingObserverQueryBuilder = new VerifyingObserverQueryBuilder(this); + this["0040A073.0040A075"] = VerifyingObserverQueryBuilder.prototype.getName.bind(verifyingObserverQueryBuilder); + this["0040A073.0040A030"] = VerifyingObserverQueryBuilder.prototype.getDateTime.bind(verifyingObserverQueryBuilder); + this["0040A073.0040A027"] = VerifyingObserverQueryBuilder.prototype.getOrganization.bind(verifyingObserverQueryBuilder); + + + let seriesQueryBuilder = new SeriesQueryBuilder(queryOptions); + let seriesQuery = seriesQueryBuilder.build(); + this.includeQueries.push({ + model: sequelize.model("Series"), + attributes: ["x0020000E"], + ...seriesQuery, + required: true + }); + + let instanceUidInParams = _.get(this.queryOptions.requestParams, "instanceUID"); + if (instanceUidInParams) { + this.query = { + x00080018: instanceUidInParams + }; + } + } + + /** + * + * @param {string[]} values + */ + getSOPClassUID(values) { + return this.getOrQuery(dictionary.keyword.SOPClassUID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + */ + getSOPInstanceUID(values) { + return this.getOrQuery(dictionary.keyword.SOPInstanceUID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + */ + getContentDate(values) { + return this.getOrQuery(dictionary.keyword.ContentDate, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + */ + getContentTime(values) { + return this.getOrQuery(dictionary.keyword.ContentTime, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + */ + getInstanceNumber(values) { + return this.getOrQuery(dictionary.keyword.InstanceNumber, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } +} + +class ConceptNameCodeSqQueryBuilder { + constructor(instanceQueryBuilder) { + /** @type {InstanceQueryBuilder} */ + this.instanceQueryBuilder = instanceQueryBuilder; + } + + isModelIncluded() { + return this.instanceQueryBuilder.includeQueries.find(v => v.model.getTableName() === "ConceptNameCodeSQ"); + } + + getCodeValue(value) { + let q = this.instanceQueryBuilder.getStringQuery(dictionary.keyword.CodeValue, value); + this.addQuery(q); + } + + getCodingSchemeDesignator(value) { + let q = this.instanceQueryBuilder.getStringQuery(dictionary.keyword.CodingSchemeDesignator, value); + this.addQuery(q); + } + + getCodingSchemeVersion(value) { + let q = this.instanceQueryBuilder.getStringQuery(dictionary.keyword.CodingSchemeVersion, value); + this.addQuery(q); + } + + getCodeMeaning(value) { + let q = this.instanceQueryBuilder.getStringQuery(dictionary.keyword.CodeMeaning, value); + this.addQuery(q); + } + + addQuery(q) { + let currentModel = this.isModelIncluded(); + if (currentModel) { + currentModel.where = { + ...currentModel.where, + ...q + }; + } else { + this.instanceQueryBuilder.includeQueries.push({ + model: DicomCodeModel, + where: { + ...q + }, + attributes: [] + }); + } + } +} + +class ContentSqQueryBuilder { + constructor(instanceQueryBuilder) { + /** @type {InstanceQueryBuilder} */ + this.instanceQueryBuilder = instanceQueryBuilder; + this.conceptNameCodeInclude = undefined; + } + + isModelIncluded() { + return this.instanceQueryBuilder.includeQueries.find(v => v.model.getTableName() === "DicomContentSQ"); + } + + getConceptNameCodeInclude() { + let currentModel = this.isModelIncluded(); + if (currentModel && currentModel.include) { + return currentModel.include.find(v => v.model.getTableName() === "ConceptNameCode"); + } + return undefined; + } + + getConceptCodeInclude() { + let currentModel = this.isModelIncluded(); + if (currentModel && currentModel.include) { + return currentModel.include.find(v => v.model.getTableName() === "ConceptCode"); + } + return undefined; + } + + getValueType(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.ValueType, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + getTextValue(value) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.TextValue, + value, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + getRelationshipType(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.RelationshipType, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + getConceptNameCodeValue(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodeValue, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptNameCodeQuery(q); + } + + getConceptNameCodingSchemeDesignator(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodingSchemeDesignator, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptNameCodeQuery(q); + } + + getConceptNameCodingSchemeVersion(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodingSchemeVersion, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptNameCodeQuery(q); + } + + getConceptNameCodeMeaning(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodeMeaning, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptNameCodeQuery(q); + } + + getConceptCodeValue(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodeValue, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptCodeQuery(q); + } + + getConceptCodingSchemeDesignator(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodingSchemeDesignator, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptCodeQuery(q); + } + + getConceptCodingSchemeVersion(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.CodingSchemeVersion, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptCodeQuery(q); + } + + getConceptCodeMeaning(values) { + let q = this.getOrQuery( + dictionary.keyword.CodeMeaning, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addConceptCodeQuery(q); + } + + addQuery(q) { + let currentModel = this.isModelIncluded(); + if (!currentModel) { + this.instanceQueryBuilder.includeQueries.push({ + model: DicomContentSqModel, + where: { + [Op.and]: [ + q + ] + }, + attributes: [] + }); + } else { + currentModel.where[Op.and] = { + ...currentModel.where[Op.and], + q + }; + } + } + + addConceptNameCodeQuery(q) { + if (!this.conceptNameCodeInclude) { + this.conceptNameCodeInclude = { + model: DicomCodeModel, + as: "ConceptNameCode", + where: { + [Op.and]: [ + q + ] + }, + attributes: [] + }; + } else { + this.conceptNameCodeInclude.where[Op.and] = { + ...this.conceptNameCodeInclude.where[Op.and], + q + }; + } + + let conceptNameIncluded = this.getConceptNameCodeInclude(); + + if (conceptNameIncluded) { + conceptNameIncluded = this.conceptNameCodeInclude; + } else { + this.addQuery({}); + let currentModel = this.isModelIncluded(); + currentModel.include = currentModel.include ? currentModel.include : []; + conceptNameIncluded = this.getConceptNameCodeInclude(); + currentModel.include.push(this.conceptNameCodeInclude); + } + } + + addConceptCodeQuery(q) { + if (!this.conceptCodeInclude) { + this.conceptCodeInclude = { + model: DicomCodeModel, + as: "ConceptCode", + where: { + ...q + }, + attributes: [] + }; + } else { + this.conceptCodeInclude.where = { + ...this.conceptCodeInclude.where, + ...q + }; + } + + let conceptCodeIncluded = this.getConceptCodeInclude(); + + if (conceptCodeIncluded) { + conceptCodeIncluded = this.conceptCodeInclude; + } else { + this.addQuery({}); + let currentModel = this.isModelIncluded(); + currentModel.include = currentModel.include ? currentModel.include : []; + conceptCodeIncluded = this.getConceptCodeInclude(); + currentModel.include.push(this.conceptCodeInclude); + } + } +} + +class VerifyingObserverQueryBuilder { + constructor(instanceQueryBuilder) { + /** @type {InstanceQueryBuilder} */ + this.instanceQueryBuilder = instanceQueryBuilder; + } + + isModelIncluded() { + return this.instanceQueryBuilder.includeQueries.find(v => v.model.getTableName() === "VerifyingObserverSQ"); + } + + isPersonNameIncluded() { + let currentModel = this.isModelIncluded(); + if (currentModel && currentModel.include) { + return currentModel.include.find(v => v.model.getTableName() === "PersonName"); + } + return false; + } + + getName(values) { + let query = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.VerifyingObserverName, + values, + BaseQueryBuilder.prototype.getPersonNameQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery({}); + let currentModel = this.isModelIncluded(); + currentModel.include = currentModel.include ? currentModel.include : []; + let personNameIncluded = this.isPersonNameIncluded(); + if (!personNameIncluded) { + currentModel.include.push({ + model: PersonNameModel, + where: { + [Op.or]: query[Op.or] + }, + attributes: [] + }); + } + } + + getDateTime(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.VerificationDateTime, + values, + BaseQueryBuilder.prototype.getDateTimeQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + getOrganization(values) { + let q = this.instanceQueryBuilder.getOrQuery( + dictionary.keyword.VerifyingOrganization, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.instanceQueryBuilder) + ); + this.addQuery(q); + } + + addQuery(q) { + let currentModel = this.isModelIncluded(); + if (!currentModel) { + this.instanceQueryBuilder.includeQueries.push({ + model: VerifyIngObserverSqModel, + where: { + [Op.and]: [ + q + ] + }, + attributes: [] + }); + } else { + currentModel.where[Op.and] = [ + ...currentModel.where[Op.and], + q + ]; + } + } +} + +module.exports.InstanceQueryBuilder = InstanceQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/patientQueryBuilder.js b/api-sql/dicom-web/controller/QIDO-RS/service/patientQueryBuilder.js new file mode 100644 index 00000000..96d962aa --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/patientQueryBuilder.js @@ -0,0 +1,50 @@ +const _ = require("lodash"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { BaseQueryBuilder } = require("./querybuilder"); +const sequelize = require("@models/sql/instance"); +const { PersonNameModel } = require("@models/sql/models/personName.model"); +const { Op } = require("sequelize"); + +class PatientQueryBuilder extends BaseQueryBuilder { + constructor(queryOptions) { + super(queryOptions); + } + + getIncludedPersonNameModel() { + if (this.includeQueries.length > 0) { + return this.includeQueries.find(v => v.model.getTableName() === "PersonName"); + } + return undefined; + } + + getPatientName(values) { + let query = this.getOrQuery(dictionary.keyword.PatientName, values, this.getPersonNameQuery.bind(this)); + + let includedPersonNameModel = this.getIncludedPersonNameModel(); + if (!includedPersonNameModel) { + this.includeQueries.push({ + model: PersonNameModel, + required: true, + where: { + [Op.or]: query[Op.or] + } + }); + } + + } + + getPatientID(values) { + return this.getOrQuery(dictionary.keyword.PatientID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + getPatientBirthDate(value) { + return this.getOrQuery(dictionary.keyword.PatientBirthDate, value, BaseQueryBuilder.prototype.getDateQuery.bind(this)); + } + + getIssuerOfPatientID(value) { + return this.getOrQuery(dictionary.keyword.IssuerOfPatientID, value, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + +} + +module.exports.PatientQueryBuilder = PatientQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js b/api-sql/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js new file mode 100644 index 00000000..7921b927 --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js @@ -0,0 +1,17 @@ +const { + QueryDicomJsonFactory, + QueryPatientDicomJsonFactory, + QueryStudyDicomJsonFactory, + QuerySeriesDicomJsonFactory, + QueryInstanceDicomJsonFactory +} = require("@root/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory"); + +QueryDicomJsonFactory.prototype.getDicomJson = async function () { + return await this.model.getDicomJson(this.queryOptions); +}; + +module.exports.QueryDicomJsonFactory = QueryDicomJsonFactory; +module.exports.QueryPatientDicomJsonFactory = QueryPatientDicomJsonFactory; +module.exports.QueryStudyDicomJsonFactory = QueryStudyDicomJsonFactory; +module.exports.QuerySeriesDicomJsonFactory = QuerySeriesDicomJsonFactory; +module.exports.QueryInstanceDicomJsonFactory = QueryInstanceDicomJsonFactory; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/querybuilder.js b/api-sql/dicom-web/controller/QIDO-RS/service/querybuilder.js new file mode 100644 index 00000000..f4222271 --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/querybuilder.js @@ -0,0 +1,495 @@ +const _ = require("lodash"); +const moment = require("moment"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { Op, Sequelize, cast, col } = require("sequelize"); +const { raccoonConfig } = require("@root/config-class"); +const { PersonNameModel } = require("@models/sql/models/personName.model"); +const sequelize = require("@models/sql/instance"); +const { logger } = require("@root/utils/logs/log"); + +class BaseQueryBuilder { + constructor(queryOptions) { + this.queryOptions = queryOptions; + /** @type {import("sequelize").IncludeOptions[]} */ + this.includeQueries = []; + this.bind = []; + this.query = { + [Op.and]: [] + }; + } + + build() { + for (let key in this.queryOptions.query) { + this.getQueryByParam_(key); + } + + let sequelizeQuery = { + where: this.query + }; + + let includesPersonNameQuery = this.getSequelizeIncludePersonNameQuery(); + if (includesPersonNameQuery.length > 0) { + _.set(sequelizeQuery, "include", includesPersonNameQuery); + } + + if (this.bind.length > 0) { + _.set(sequelizeQuery, "bind", this.bind); + } + return sequelizeQuery; + } + + /** + * @private + * @param {string} key + */ + getQueryByParam_(key) { + let value = this.queryOptions.query[key]; + let values = Array.isArray(value) ? value : [value]; + + for (let i = 0; i < values.length; i++) { + let paramValue = values[i]; + let commaValue = this.comma(key, paramValue); + let wildCardValues = commaValue.map(v => this.getWildCardQuery(v[key])); + try { + let query; + if (key.includes(".")) { + query = this[key](wildCardValues); + } else { + query = this[`get${dictionary.tag[key]}`](wildCardValues); + } + + this.query[Op.and] = [ + ...this.query[Op.and], + query + ]; + } catch (e) { + if (e.message.includes("not a function")) break; + logger.error(e); + throw e; + } + + } + } + + getSequelizeIncludePersonNameQuery() { + let includes = []; + + for (let includeQuery of this.includeQueries) { + includes.push(includeQuery); + } + return includes; + } + + comma(key, value) { + let $or = []; + let valueCommaSplit = value.split(","); + for (let i = 0; i < valueCommaSplit.length; i++) { + let obj = {}; + obj[key] = valueCommaSplit[i]; + $or.push(obj); + } + return $or; + } + + /** + * + * @param {string} tag + * @param {string} value + * @returns + */ + getStringQuery(tag, value) { + let queryField = this.getQueryField(tag); + if (value.includes("%") || value.includes("_")) { + return { + [queryField]: { + [Op.like]: value + } + }; + } + return { + [queryField]: value + }; + } + + /** + * + * @param {string} tag + * @param {string[]} values + * @param {(tag: string, values: string[]) => void} queryFn + */ + getOrQuery(tag, values, queryFn) { + if (values.length === 1) { + return queryFn(tag, values[0]); + } + + let or = { + [Op.or]: [] + }; + for (let i = 0; i < values.length; i++) { + let q = queryFn(tag, values[i]); + or[Op.or].push(q); + } + return or; + } + + getStringArrayJsonQuery(tag, value) { + let queryField = this.getQueryField(tag); + if (raccoonConfig.dbConfig.dialect === "postgres") { + return { + [queryField]: { + [Op.contains]: cast(JSON.stringify([value]), "jsonb") + } + }; + } else { + return { + [Op.or]: [Sequelize.where(Sequelize.fn("JSON_CONTAINS", Sequelize.col(queryField), `[${value}]`))] + }; + } + } + + getNumberQuery(tag, value) { + let queryField = this.getQueryField(tag); + return { + [queryField]: Number(value) + }; + } + + /** + * + * @param {string} tag + * @param {string} value + * @returns + */ + getPersonNameQuery(tag, value) { + if (value.includes("%") || value.includes("_")) { + return { + [Op.or]: [ + { + alphabetic: { + [Op.like]: value + } + }, + { + ideographic: { + [Op.like]: value + } + }, + { + phonetic: { + [Op.like]: value + } + } + ] + }; + } + + return { + [Op.or]: [ + { alphabetic: value }, + { ideographic: value }, + { phonetic: value } + ] + }; + } + + /** + * + * @param {string} tag + * @param {string} value + */ + getDateQuery(tag, value) { + let queryField = this.getQueryField(tag); + let dashIndex = value.indexOf("-"); + if (dashIndex === 0) { // -YYYYMMDD + return { + [queryField]: { + [Op.lte]: this.dateStringToSqlDateOnly(value.substring(1)) + } + }; + } else if (dashIndex === value.length - 1) { // YYYYMMDD- + return { + [queryField]: { + [Op.gte]: this.dateStringToSqlDateOnly(value.substring(0, dashIndex)) + } + }; + } else if (dashIndex > 0) { // YYYYMMDD-YYYYMMDD + return { + [queryField]: { + [Op.and]: [ + { [Op.gte]: this.dateStringToSqlDateOnly(value.substring(0, dashIndex)) }, + { [Op.lte]: this.dateStringToSqlDateOnly(value.substring(dashIndex + 1)) } + ] + } + }; + } else { // YYYYMMDD + return { + [queryField]: this.dateStringToSqlDateOnly(value) + }; + } + } + + /** + * + * @param {string} tag + * @param {string} value + */ + getTimeQuery(tag, value) { + let queryField = this.getQueryField(tag); + let dashIndex = value.indexOf("-"); + if (dashIndex === 0) { // -HHmmss + return { + [queryField]: { + [Op.lte]: this.timeStringToSqlTimeDecimal(value.substring(1)) + } + }; + } else if (dashIndex === value.length - 1) { // HHmmss- + return { + [queryField]: { + [Op.gte]: this.timeStringToSqlTimeDecimal(value.substring(0, dashIndex)) + } + }; + } else if (dashIndex > 0) { // HHmmss-HHmmss + return { + [queryField]: { + [Op.and]: [ + { [Op.gte]: this.timeStringToSqlTimeDecimal(value.substring(0, dashIndex)) }, + { [Op.lte]: this.timeStringToSqlTimeDecimal(value.substring(dashIndex + 1)) } + ] + } + }; + } else { + return { + [queryField]: this.timeStringToSqlTimeDecimal(value) + }; + } + } + + /** + * + * @param {string} tag + * @param {string} value + */ + getDateTimeQuery(tag, value) { + let queryField = this.getQueryField(tag); + let dashIndex = value.indexOf("-"); + if (dashIndex === 0) { // -YYYYMMDD + return { + [queryField]: { + [Op.lte]: this.dateTimeStringToSqlDateTime(value.substring(1)) + } + }; + } else if (dashIndex === value.length - 1) { // YYYYMMDD- + return { + [queryField]: { + [Op.gte]: this.dateTimeStringToSqlDateTime(value.substring(0, dashIndex)) + } + }; + } else if (dashIndex > 0) { // YYYYMMDD-YYYYMMDD + return { + [queryField]: { + [Op.and]: [ + { [Op.gte]: this.dateTimeStringToSqlDateTime(value.substring(0, dashIndex)) }, + { [Op.lte]: this.dateTimeStringToSqlDateTime(value.substring(dashIndex + 1)) } + ] + } + }; + } else { // YYYYMMDD + return { + [queryField]: this.dateTimeStringToSqlDateTime(value) + }; + } + } + + dateStringToSqlDateOnly(value) { + return moment(value, "YYYYMMDD").format("YYYY-MM-DD"); + } + + dateTimeStringToSqlDateTime(value) { + return moment(value, "YYYYMMDDhhmmss.SSSSSSZZ").toISOString(); + } + + /** + * + * @param {string} timeStr + */ + getTimePadding(timeStr) { + let hhmmssStr = timeStr.padEnd(6, "0"); + if (timeStr.length === 5) { + hhmmssStr.padStart(6, "0"); + } + return hhmmssStr; + } + + + timeStringToSqlTimeDecimal(value) { + let hhmmssStr = this.getTimePadding(value); + if (hhmmssStr.includes(".")) { + let [timeStr, millionthSecondStr] = hhmmssStr.split("."); + hhmmssStr = this.getTimePadding(timeStr) + "." + millionthSecondStr; + } + return parseFloat(hhmmssStr); + } + + /** + * + * @param {string} value + * @returns + */ + getWildCardQuery(value) { + let wildCardIndex = value.indexOf("*"); + let questionIndex = value.indexOf("?"); + + if (wildCardIndex >= 0 || questionIndex >= 0) { + value = value.replace(/\*/gm, "%"); + value = value.replace(/\?/gm, "_"); + } + + return value; + } + + getWildCardRegexString(value) { + let wildCardIndex = value.indexOf("%"); + let questionIndex = value.indexOf("_"); + + if (wildCardIndex >= 0 || questionIndex >= 0) { + value = value.replace(/%/gm, ".*"); + value = value.replace(/_/gm, "."); + value = value.replace(/\^/gm, "\\^"); + value = "^" + value; + } + + return value; + } + + /** + * + * @param {*} q + * @see {@link https://stackoverflow.com/questions/60598225/how-to-merge-javascript-object-containing-symbols "How to merge javascript object containing symbols?"} + */ + mergeQuery(q) { + _.mergeWith(this.query, q, (a, b) => { + if (!_.isObject(b)) return b; + + return Array.isArray(a) ? [...a, ...b] : { ...a, ...b }; + }); + } + + /** + * + * @param {string} tag + * @returns + */ + getQueryField(tag) { + return /^[0-9a-zA-Z]{8}$/.test(tag.substring(0, 8)) ? `x${tag}` : tag; + } +} + +class StudyQueryBuilder extends BaseQueryBuilder { + constructor(queryOptions) { + super(queryOptions); + + let studyInstanceUidInParams = _.get(this.queryOptions.requestParams, "studyUID"); + if (studyInstanceUidInParams) { + this.query = { + x0020000D: studyInstanceUidInParams + }; + } + } + + getIncludedPatientModel() { + if (this.includeQueries.length > 0) { + return this.includeQueries.find(v => v.model.getTableName() === "Patient"); + } + return undefined; + } + + getIncludedPersonNameModelInPatient() { + let includedPatientModel = this.getIncludedPatientModel(); + if (includedPatientModel) { + return includedPatientModel.include.find(v => v.model.getTableName() === "PersonName"); + } + return undefined; + } + + getIncludedPersonNameModel() { + if (this.includeQueries.length > 0) { + return this.includeQueries.find(v => v.model.getTableName() === "PersonName"); + } + return undefined; + } + + + getStudyInstanceUID(values) { + return this.getOrQuery(dictionary.keyword.StudyInstanceUID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + getPatientName(values) { + let query = this.getOrQuery(dictionary.keyword.PatientName, values, BaseQueryBuilder.prototype.getPersonNameQuery.bind(this)); + + let includedPatientModel = this.getIncludedPatientModel(); + if (!includedPatientModel) { + this.includeQueries.push({ + model: sequelize.model("Patient"), + include: [{ + model: sequelize.model("PersonName"), + where: { + [Op.or]: query[Op.or] + }, + required: true + }], + required: true + }); + } + } + + getPatientID(values) { + return this.getOrQuery(dictionary.keyword.PatientID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + getStudyDate(values) { + return this.getOrQuery(dictionary.keyword.StudyDate, values, BaseQueryBuilder.prototype.getDateQuery.bind(this)); + } + + getStudyTime(values) { + return this.getOrQuery(dictionary.keyword.StudyTime, values, BaseQueryBuilder.prototype.getTimeQuery.bind(this)); + } + + getAccessionNumber(values) { + return this.getOrQuery(dictionary.keyword.AccessionNumber, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + getModalitiesInStudy(values) { + let stringQuery = this.getOrQuery(dictionary.keyword.Modality, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + this.includeQueries.push({ + model: sequelize.model("Series"), + where: { + ...stringQuery + }, + attributes: [] + }); + } + + getReferringPhysicianName(values) { + let query = this.getOrQuery(dictionary.keyword.ReferringPhysicianName, values, BaseQueryBuilder.prototype.getPersonNameQuery.bind(this)); + let includedPersonNameModel = this.getIncludedPersonNameModel(); + if (!includedPersonNameModel) { + this.includeQueries.push({ + model: PersonNameModel, + where: { + [Op.or]: [ + ...query[Op.or] + ] + }, + required: true + }); + } + } + + getStudyID(values) { + return this.getOrQuery(dictionary.keyword.StudyID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } +} + + +module.exports.BaseQueryBuilder = BaseQueryBuilder; +module.exports.StudyQueryBuilder = StudyQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/QIDO-RS/service/seriesQueryBuilder.js b/api-sql/dicom-web/controller/QIDO-RS/service/seriesQueryBuilder.js new file mode 100644 index 00000000..779a7692 --- /dev/null +++ b/api-sql/dicom-web/controller/QIDO-RS/service/seriesQueryBuilder.js @@ -0,0 +1,209 @@ +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { BaseQueryBuilder, StudyQueryBuilder } = require("./querybuilder"); +const { Op, Sequelize } = require("sequelize"); +const { raccoonConfig } = require("@root/config-class"); +const _ = require("lodash"); +const { PersonNameModel } = require("@models/sql/models/personName.model"); +const sequelize = require("@models/sql/instance"); +const { SeriesRequestAttributesModel } = require("@models/sql/models/seriesRequestAttributes.model"); + +class SeriesQueryBuilder extends BaseQueryBuilder { + constructor(queryOptions) { + super(queryOptions); + let seriesRequestAttributeSequence = new SeriesRequestAttributeSequence(this); + this["00400275.00080050"] = SeriesRequestAttributeSequence.prototype.getAccessionNumber.bind(seriesRequestAttributeSequence); + this["00400275.00080051.00400031"] = SeriesRequestAttributeSequence.prototype.getIssuerLocalNameSpaceEntityID.bind(seriesRequestAttributeSequence); + this["00400275.00080051.00400032"] = SeriesRequestAttributeSequence.prototype.getIssuerUniversalEntityID.bind(seriesRequestAttributeSequence); + this["00400275.00080051.00400033"] = SeriesRequestAttributeSequence.prototype.getIssuerUniversalEntityIDType.bind(seriesRequestAttributeSequence); + this["00400275.00321033"] = SeriesRequestAttributeSequence.prototype.getRequestingService.bind(seriesRequestAttributeSequence); + this["00400275.00401001"] = SeriesRequestAttributeSequence.prototype.getRequestedProcedureID.bind(seriesRequestAttributeSequence); + this["00400275.0020000D"] = SeriesRequestAttributeSequence.prototype.getStudyInstanceUID.bind(seriesRequestAttributeSequence); + + let studyQueryBuilder = new StudyQueryBuilder(queryOptions); + let studyQuery = studyQueryBuilder.build(); + this.includeQueries.push({ + model: sequelize.model("Study"), + attributes: ["x0020000D"], + ...studyQuery, + required: true + }); + + let seriesInstanceUidInParams = _.get(this.queryOptions.requestParams, "seriesUID"); + if (seriesInstanceUidInParams) { + this.query = { + x0020000E: seriesInstanceUidInParams + }; + } + } + + getIncludedPerformingPhysicianNameModel() { + if (this.includeQueries.length > 0) { + return this.includeQueries.find(v => _.get(v, "as", "") === "performingPhysicianName"); + } + return undefined; + } + + getIncludedOperatorsNameModel() { + if (this.includeQueries.length > 0) { + return this.includeQueries.find(v => _.get(v, "as", "") === "operatorsName"); + } + return undefined; + } + getSeriesDate(values) { + return this.getOrQuery(dictionary.keyword.SeriesDate, values, BaseQueryBuilder.prototype.getDateQuery.bind(this)); + } + getModality(values) { + return this.getOrQuery(dictionary.keyword.Modality, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + getSeriesDescription(values) { + return this.getOrQuery(dictionary.keyword.SeriesDescription, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + getPersonNameJsonArrayQuery(tag, value) { + if (raccoonConfig.dbConfig.dialect === "postgres") { + value = this.getWildCardRegexString(value); + return { + [Op.or]: [ + Sequelize.literal(`"x${tag}" @? '$[*].Alphabetic ? (@ like_regex "${value}" flag "is")'`) + ] + }; + } + throw new Error("Not implemented"); + } + + getPerformingPhysicianName(values) { + let query = this.getOrQuery(dictionary.keyword.PerformingPhysicianName, values, this.getPersonNameQuery.bind(this)); + let includedPerformingPhysicianNameModel = this.getIncludedPerformingPhysicianNameModel(); + if (!includedPerformingPhysicianNameModel) { + this.includeQueries.push({ + model: PersonNameModel, + as: "performingPhysicianName", + where: { + [Op.or]: query[Op.or] + }, + attributes: [] + }); + } + + } + + getOperatorsName(values) { + let query = this.getOrQuery(dictionary.keyword.OperatorsName, values, this.getPersonNameQuery.bind(this)); + this.includeQueries.push({ + model: PersonNameModel, + as: "operatorsName", + where: { + [Op.or]: query[Op.or] + }, + attributes: [] + }); + } + + getSeriesNumber(values) { + return this.getOrQuery(dictionary.keyword.SeriesNumber, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + getSeriesInstanceUID(values) { + return this.getOrQuery(dictionary.keyword.SeriesInstanceUID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + +} + +class SeriesRequestAttributeSequence { + constructor(seriesQueryBuilder) { + /** @type {SeriesQueryBuilder} */ + this.seriesQueryBuilder = seriesQueryBuilder; + } + isModelIncluded() { + return this.seriesQueryBuilder.includeQueries.find(v => v.model.getTableName() === "SeriesRequestAttributes"); + } + getAccessionNumber(values) { + let q = this.seriesQueryBuilder.getOrQuery( + dictionary.keyword.AccessionNumber, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + getIssuerLocalNameSpaceEntityID(values) { + let q = this.seriesQueryBuilder.getOrQuery( + "00080051_x00400031", + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + getIssuerUniversalEntityID(values) { + let q = this.seriesQueryBuilder.getOrQuery( + "00080051_x00400032", + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + getIssuerUniversalEntityIDType(values) { + let q = this.seriesQueryBuilder.getOrQuery( + "00080051_x00400033", + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + /** + * + * @param {string[]} values - The values to be used in the query generation. + */ + getRequestingService(values) { + let q = this.seriesQueryBuilder.getOrQuery( + dictionary.keyword.RequestingService, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + getRequestedProcedureID(values) { + let q = this.seriesQueryBuilder.getOrQuery( + dictionary.keyword.RequestedProcedureID, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + getStudyInstanceUID(values) { + let q = this.seriesQueryBuilder.getOrQuery( + dictionary.keyword.StudyInstanceUID, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this.seriesQueryBuilder) + ); + this.addQuery(q); + } + + addQuery(q) { + let currentModel = this.isModelIncluded(); + if (currentModel) { + currentModel.where[Op.and] = [ + ...currentModel.where[Op.and], + q + ]; + } else { + this.seriesQueryBuilder.includeQueries.push({ + model: SeriesRequestAttributesModel, + where: { + [Op.and]: [ + q + ] + }, + attributes: [] + }); + } + } + +} + +module.exports.SeriesQueryBuilder = SeriesQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/base-workItem.service.js b/api-sql/dicom-web/controller/UPS-RS/service/base-workItem.service.js new file mode 100644 index 00000000..915915b1 --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/base-workItem.service.js @@ -0,0 +1,76 @@ +const _ = require("lodash"); +const { DicomJsonModel } = require("@dicom-json-model"); +const { SUBSCRIPTION_STATE } = require("@models/DICOM/ups"); +const { UpsGlobalSubscriptionModel } = require("@dbModels/upsGlobalSubscription.model"); +const { UpsSubscriptionModel } = require("@dbModels/upsSubscription.model"); +const { WorkItemModel } = require("@dbModels/workitems.model"); +const { BaseWorkItemService } = require("@root/api/dicom-web/controller/UPS-RS/service/base-workItem.service"); +const { UpsQueryBuilder } = require("./query/upsQueryBuilder"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); +class SqlBaseWorkItemService extends BaseWorkItemService { + + constructor(req, res) { + super(req, res); + } + + async isAeTileSubscribed(aeTitle) { + let subscription = await UpsSubscriptionModel.findOne({ + where: { + aeTitle: aeTitle + } + }); + + if (!subscription) + return false; + + return subscription.subscribed === SUBSCRIPTION_STATE.SUBSCRIBED_LOCK || + subscription.subscribed === SUBSCRIPTION_STATE.SUBSCRIBED_NO_LOCK; + } + + async getGlobalSubscriptionsCursor() { + return UpsGlobalSubscriptionModel.cursor({}); + } + + /** + * @param {DicomJsonModel} workItem + */ + async getHitGlobalSubscriptions(workItem) { + let globalSubscriptionsCursor = await this.getGlobalSubscriptionsCursor(); + let hitGlobalSubscriptions = []; + let globalSubscription = await globalSubscriptionsCursor.next(); + while (globalSubscription) { + if (!globalSubscription.queryKeys) { + hitGlobalSubscriptions.push(globalSubscription); + } else { + let query = convertAllQueryToDicomTag(globalSubscription.queryKeys, false); + _.set(query, "upsInstanceUID", workItem.dicomJson.upsInstanceUID); + let queryOptions = { + query: query + }; + let upsQueryBuilder = new UpsQueryBuilder(queryOptions); + let dbQuery = upsQueryBuilder.build(); + let count = await WorkItemModel.count({ + ...dbQuery + }); + if (count > 0) + hitGlobalSubscriptions.push(globalSubscription); + } + globalSubscription = await globalSubscriptionsCursor.next(); + } + return hitGlobalSubscriptions; + } + + /** + * + * @param {DicomJsonModel} workItem + * @returns + */ + async getHitSubscriptions(workItem) { + let hitSubscriptions = await workItem.dicomJson.getUpsSubscriptions(); + + return hitSubscriptions; + } + +} + +module.exports.BaseWorkItemService = SqlBaseWorkItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/cancel.service.js b/api-sql/dicom-web/controller/UPS-RS/service/cancel.service.js new file mode 100644 index 00000000..e22027b9 --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/cancel.service.js @@ -0,0 +1,26 @@ +const _ = require("lodash"); +const { WorkItemModel } = require("@models/sql/models/workitems.model"); +const { CancelWorkItemService } = require("@root/api/dicom-web/controller/UPS-RS/service/cancel.service"); + +class SqlCancelWorkItemService extends CancelWorkItemService { + + /** + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ + constructor(req, res) { + super(req, res); + this.upsInstanceUID = this.request.params.workItem; + this.workItem = null; + this.requestWorkItem = /** @type {Object[]} */(this.request.body).pop(); + } + + async initWorkItem() { + this.workItem = await WorkItemModel.findOneWorkItemDicomJsonModel(this.upsInstanceUID); + } + +} + + +module.exports.CancelWorkItemService = SqlCancelWorkItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js b/api-sql/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js new file mode 100644 index 00000000..3895cdb9 --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/change-workItem-state.service.js @@ -0,0 +1,56 @@ +const { WorkItemModel } = require("@dbModels/workitems.model"); +const { DicomJsonModel } = require("@models/DICOM/dicom-json-model"); +const { ChangeWorkItemStateService } = require("@root/api/dicom-web/controller/UPS-RS/service/change-workItem-state.service"); +const { UPS_EVENT_TYPE } = require("@root/api/dicom-web/controller/UPS-RS/service/workItem-event"); + +class SqlChangeWorkItemStateService extends ChangeWorkItemStateService { + /** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ + constructor(req, res) { + super(req, res); + } + + async changeWorkItemState() { + let foundWorkItem = await WorkItemModel.findOneWorkItemByUpsInstanceUID(this.request.params.workItem); + this.workItem = foundWorkItem.toDicomJsonModel(); + + this.workItemState = this.workItem.getString("00741000"); + this.workItemTransactionUID = this.workItem.getString("00081195"); + 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 foundWorkItem.changeWorkItemState(this.requestState); + await foundWorkItem.reload(); + // TODO: change work item state event + this.triggerUpsChangeStateEvent(foundWorkItem); + } + + /** + * + * @param {WorkItemModel} updatedWorkItem + * @returns + */ + async triggerUpsChangeStateEvent(updatedWorkItem) { + let updatedWorkItemDicomJsonModelObj = updatedWorkItem.toDicomJsonModel(); + + let hitSubscriptions = await this.getHitSubscriptions(new DicomJsonModel(updatedWorkItem)); + + if (hitSubscriptions.length === 0) return; + + let hitSubscriptionAeTitleArray = hitSubscriptions.map(sub => sub.aeTitle); + this.addUpsEvent(UPS_EVENT_TYPE.StateReport, updatedWorkItemDicomJsonModelObj.dicomJson.upsInstanceUID, this.stateReportOf(updatedWorkItemDicomJsonModelObj), hitSubscriptionAeTitleArray); + this.triggerUpsEvents(); + } +} + +module.exports.ChangeWorkItemStateService = SqlChangeWorkItemStateService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/create-workItem.service.js b/api-sql/dicom-web/controller/UPS-RS/service/create-workItem.service.js new file mode 100644 index 00000000..7a64639a --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/create-workItem.service.js @@ -0,0 +1,39 @@ +const { PatientModel } = require("@dbModels/patient.model"); +const { UIDUtils } = require("@dcm4che/util/UIDUtils"); +const { WorkItemModel } = require("@dbModels/workitems.model"); +const { CreateWorkItemService } = require("@root/api/dicom-web/controller/UPS-RS/service/create-workItem.service"); +const { get, set } = require("lodash"); +const { UpsWorkItemPersistentObject } = require("@models/sql/po/upsWorkItem.po"); +const { PatientPersistentObject } = require("@models/sql/po/patient.po"); + +class SqlCreateWorkItemService extends CreateWorkItemService { + constructor(req, res) { + super(req, res); + } + + async createUps() { + let uid = get(this.request, "query.workitem", + await UIDUtils.createUID() + ); + await this.dataAdjustBeforeCreatingUps(uid); + await this.validateWorkItem(uid); + + let patient = await PatientModel.updateOrCreatePatient(this.requestWorkItem.dicomJson); + let workItem = new UpsWorkItemPersistentObject(this.requestWorkItem.dicomJson, patient); + let savedWorkItem = await workItem.save(); + + this.triggerCreateEvent(savedWorkItem); + + return workItem; + } + + async isUpsExist(uid) { + return await WorkItemModel.findOne({ + where: { + upsInstanceUID: uid + } + }); + } +} + +module.exports.CreateWorkItemService = SqlCreateWorkItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/get-workItem.service.js b/api-sql/dicom-web/controller/UPS-RS/service/get-workItem.service.js new file mode 100644 index 00000000..87359185 --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/get-workItem.service.js @@ -0,0 +1,39 @@ +const { cloneDeep } = require("lodash"); +const { GetWorkItemService } = require("@root/api/dicom-web/controller/UPS-RS/service/get-workItem.service"); +const { QidoRsService } = require("@root/api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service"); +const { WorkItemModel } = require("@models/sql/models/workitems.model"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); + +class SqlGetWorkItemService extends GetWorkItemService { + constructor(req, res) { + super(req, res); + } + + async getUps() { + let queryOptions = { + query: this.query, + skip: this.skip_, + limit: this.limit_, + requestParams: this.request.params + }; + + let docs = await WorkItemModel.getDicomJson(queryOptions); + + return this.adjustDocs(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, false); + } +} + +SqlGetWorkItemService.prototype.initQuery_ = QidoRsService.prototype.initQuery_; + +module.exports.GetWorkItemService = SqlGetWorkItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/query/upsQueryBuilder.js b/api-sql/dicom-web/controller/UPS-RS/service/query/upsQueryBuilder.js new file mode 100644 index 00000000..8fa740ca --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/query/upsQueryBuilder.js @@ -0,0 +1,229 @@ +const sequelize = require("@models/sql/instance"); +const { PatientQueryBuilder } = require("../../../QIDO-RS/service/patientQueryBuilder"); +const { BaseQueryBuilder } = require("../../../QIDO-RS/service/querybuilder"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { DicomCodeQueryBuilder } = require("../../../QIDO-RS/service/dicomCodeQueryBuilder"); +const { get } = require("lodash"); + +class UpsQueryBuilder extends BaseQueryBuilder { + + constructor(queryOptions) { + super(queryOptions); + + let patientQueryBuilder = new PatientQueryBuilder(queryOptions); + let patientQuery = patientQueryBuilder.build(); + this.includeQueries.push({ + model: sequelize.model("Patient"), + attributes: ["x00100020"], + ...patientQuery, + required: true + }); + + this.createCodeQueries(dictionary.keyword.ScheduledStationNameCodeSequence); + this.createCodeQueries(dictionary.keyword.ScheduledStationClassCodeSequence); + this.createCodeQueries(dictionary.keyword.ScheduledStationGeographicLocationCodeSequence); + this.createCodeQueries(dictionary.keyword.HumanPerformerCodeSequence); + this.createCodeQueries(dictionary.keyword.ScheduledWorkitemCodeSequence); + this.createScheduledHumanPerformersSequenceQueries(); + this.createIssuerOfAdmissionIdSequenceQueries(); + + let upsInstanceUIDInParams = get(this.queryOptions.requestParams, "workItem"); + if (upsInstanceUIDInParams) { + this.query = { + upsInstanceUID: upsInstanceUIDInParams + }; + } + } + + /** + * + * @param {string[]} values + * @returns + */ + getSOPInstanceUID(values) { + return this.getOrQuery(dictionary.keyword.SOPInstanceUID, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + * @returns + */ + getScheduledProcedureStepPriority(values) { + return this.getOrQuery( + dictionary.keyword.ScheduledProcedureStepPriority, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getScheduledProcedureStepModificationDateTime(values) { + return this.getOrQuery( + dictionary.keyword.ScheduledProcedureStepModificationDateTime, + values, + BaseQueryBuilder.prototype.getDateTimeQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getProcedureStepLabel(values) { + return this.getOrQuery(dictionary.keyword.ProcedureStepLabel, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + * @returns + */ + getWorklistLabel(values) { + return this.getOrQuery(dictionary.keyword.WorklistLabel, values, BaseQueryBuilder.prototype.getStringQuery.bind(this)); + } + + /** + * + * @param {string[]} values + * @returns + */ + getScheduledProcedureStepStartDateTime(values) { + return this.getOrQuery( + dictionary.keyword.ScheduledProcedureStepStartDateTime, + values, + BaseQueryBuilder.prototype.getDateTimeQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getExpectedCompletionDateTime(values) { + return this.getOrQuery( + dictionary.keyword.ExpectedCompletionDateTime, + values, + BaseQueryBuilder.prototype.getDateTimeQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getScheduledProcedureStepExpirationDateTime(values) { + return this.getOrQuery( + dictionary.keyword.ScheduledProcedureStepExpirationDateTime, + values, + BaseQueryBuilder.prototype.getDateTimeQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getInputReadinessState(values) { + return this.getOrQuery( + dictionary.keyword.InputReadinessState, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getAdmissionID(values) { + return this.getOrQuery( + dictionary.keyword.AdmissionID, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + /** + * + * @param {string[]} values + * @returns + */ + getProcedureStepState(values) { + return this.getOrQuery( + dictionary.keyword.ProcedureStepState, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + } + + createCodeQueries(tag) { + let dicomCodeQueryBuilder = new DicomCodeQueryBuilder(this, dictionary.tag[tag]); + this[`${tag}.00080100`] = (values) => dicomCodeQueryBuilder.getCodeValue(values); + this[`${tag}.00080102`] = (values) => dicomCodeQueryBuilder.getCodingSchemeDesignator(values); + this[`${tag}.00080103`] = (values) => dicomCodeQueryBuilder.getCodingSchemeVersion(values); + this[`${tag}.00080104`] = (values) => dicomCodeQueryBuilder.getCodeMeaning(values); + } + + createIssuerOfAdmissionIdSequenceQueries() { + this[`${dictionary.keyword.IssuerOfAdmissionIDSequence}.${dictionary.keyword.LocalNamespaceEntityID}`] = (values) => { + return this.getOrQuery( + `${dictionary.keyword.IssuerOfAdmissionIDSequence}_x${dictionary.keyword.LocalNamespaceEntityID}`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.IssuerOfAdmissionIDSequence}.${dictionary.keyword.UniversalEntityID}`] = (values) => { + return this.getOrQuery( + `${dictionary.keyword.IssuerOfAdmissionIDSequence}_x${dictionary.keyword.UniversalEntityID}`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + + this[`${dictionary.keyword.IssuerOfAdmissionIDSequence}.${dictionary.keyword.UniversalEntityIDType}`] = (values) => { + return this.getOrQuery( + `${dictionary.keyword.IssuerOfAdmissionIDSequence}_x${dictionary.keyword.UniversalEntityIDType}`, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + } + + createScheduledHumanPerformersSequenceQueries() { + this[`${dictionary.keyword.ScheduledHumanPerformersSequence}.${dictionary.keyword.HumanPerformerName}`] = (values) => { + let q = this.getOrQuery( + `${dictionary.keyword.ScheduledHumanPerformersSequence}.${dictionary.keyword.HumanPerformerName}`, + values, + BaseQueryBuilder.prototype.getPersonNameQuery.bind(this) + ); + this.includeQueries.push({ + model: sequelize.model("PersonName"), + as: dictionary.tag["00404037"], + where: { + ...q + }, + attributes: [] + }); + }; + this[`${dictionary.keyword.ScheduledHumanPerformersSequence}.${dictionary.keyword.HumanPerformerOrganization}`] = (values) => { + return this.getOrQuery( + dictionary.keyword.HumanPerformerOrganization, + values, + BaseQueryBuilder.prototype.getStringQuery.bind(this) + ); + }; + } +} + +module.exports.UpsQueryBuilder = UpsQueryBuilder; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/subscribe.service.js b/api-sql/dicom-web/controller/UPS-RS/service/subscribe.service.js new file mode 100644 index 00000000..c5e80c04 --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/subscribe.service.js @@ -0,0 +1,147 @@ +const _ = require("lodash"); +const { DicomJsonModel } = require("@dicom-json-model"); +const { UpsSubscriptionModel } = require("@dbModels/upsSubscription.model"); +const { UpsGlobalSubscriptionModel } = require("@dbModels/upsGlobalSubscription.model"); +const { + DicomWebServiceError, + DicomWebStatusCodes +} = require("@error/dicom-web-service"); +const { SUBSCRIPTION_STATE, SUBSCRIPTION_FIXED_UIDS } = require("@models/DICOM/ups"); +const { SubscribeService } = require("@root/api/dicom-web/controller/UPS-RS/service/subscribe.service"); +const { WorkItemModel } = require("@models/sql/models/workitems.model"); +const { UPS_EVENT_TYPE } = require("@root/api/dicom-web/controller/UPS-RS/service/workItem-event"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); + +class SqlSubscribeService extends SubscribeService { + + /** + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ + constructor(req, res) { + super(req, res); + } + + async create() { + + if (this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.GlobalUID || + this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.FilteredGlobalUID) { + + this.query = convertAllQueryToDicomTag(this.request.query, false); + await this.createOrUpdateGlobalSubscription(); + } else { + let workItem = await WorkItemModel.findOneWorkItemByUpsInstanceUID(this.upsInstanceUID); + let workItemDicomJsonModel = workItem.toDicomJsonModel(); + await this.createOrUpdateSubscription(workItem); + this.addUpsEvent(UPS_EVENT_TYPE.StateReport, this.upsInstanceUID, this.stateReportOf(workItemDicomJsonModel), [this.subscriberAeTitle]); + } + + await this.triggerUpsEvents(); + } + + //#region Subscription + async findOneSubscription() { + return await UpsSubscriptionModel.findOne({ + where: { + aeTitle: this.subscriberAeTitle + } + }); + } + + /** + * + * @param {DicomJsonModel} workItem + * @returns + */ + async createOrUpdateSubscription(workItem) { + let subscription = await this.findOneSubscription(); + let subscribed = this.deletionLock ? SUBSCRIPTION_STATE.SUBSCRIBED_NO_LOCK : SUBSCRIPTION_STATE.SUBSCRIBED_LOCK; + await this.updateWorkItemSubscription(workItem, subscribed); + if (!subscription) { + // Create + let subscriptionObj = await UpsSubscriptionModel.create({ + aeTitle: this.subscriberAeTitle, + isDeletionLock: this.deletionLock, + subscribed: subscribed + }); + + await workItem.addUpsSubscription(subscriptionObj); + return subscriptionObj; + } else { + // Update + subscription.isDeletionLock = this.deletionLock; + subscription.subscribed = subscribed; + if (!await workItem.hasUpsSubscription(subscription)) { + workItem.addUpsSubscription(subscription); + } + let updatedSubscription = await subscription.save(); + return updatedSubscription; + } + } + + async updateWorkItemSubscription(workItem, subscription) { + workItem.subscribed = subscription; + await workItem.save(); + } + //#endregion + + //#region Global Subscriptions + async createOrUpdateGlobalSubscription() { + let subscribed = this.deletionLock ? SUBSCRIPTION_STATE.SUBSCRIBED_NO_LOCK : SUBSCRIPTION_STATE.SUBSCRIBED_LOCK; + let subscription = await this.findOneGlobalSubscription(); + if (this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.GlobalUID) this.query = undefined; + if (this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.FilteredGlobalUID && _.isEmpty(this.query)) { + throw new DicomWebServiceError( + DicomWebStatusCodes.InvalidArgumentValue, + `Missing "filter", The Filtered Worklist Subscription must have "filter"`, + 400 + ); + } + if (!subscription) { + //Create + let subscriptionObj = UpsGlobalSubscriptionModel.build({ + aeTitle: this.subscriberAeTitle, + isDeletionLock: this.deletionLock, + subscribed: subscribed, + queryKeys: this.query + }); + + let createdSubscription = await subscriptionObj.save(); + } else { + //Update + subscription.isDeletionLock = this.deletionLock; + subscription.subscribed = subscribed; + subscription.queryKeys = this.query; + subscription.changed("queryKeys"); + await subscription.save(); + } + + let notSubscribedWorkItems = await this.findNotSubscribedWorkItems(); + for(let notSubscribedWorkItem of notSubscribedWorkItems) { + await this.createOrUpdateSubscription(notSubscribedWorkItem); + + this.addUpsEvent(UPS_EVENT_TYPE.StateReport, notSubscribedWorkItem.upsInstanceUID, this.stateReportOf(notSubscribedWorkItem.toDicomJsonModel()), [this.subscriberAeTitle]); + } + } + + async findOneGlobalSubscription() { + return await UpsGlobalSubscriptionModel.findOne({ + where: { + aeTitle: this.subscriberAeTitle + } + }); + } + //#endregion + + async findNotSubscribedWorkItems() { + return await WorkItemModel.findAll({ + where: { + subscribed: SUBSCRIPTION_STATE.NOT_SUBSCRIBED + } + }) || []; + } +} + + +module.exports.SubscribeService = SqlSubscribeService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js b/api-sql/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js new file mode 100644 index 00000000..2a0a24ea --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js @@ -0,0 +1,53 @@ +const _ = require("lodash"); +const { + DicomWebServiceError, + DicomWebStatusCodes +} = require("@error/dicom-web-service"); +const { SuspendSubscribeService } = require("@root/api/dicom-web/controller/UPS-RS/service/suspend-subscription.service"); +const { UpsGlobalSubscriptionModel } = require("@models/sql/models/upsGlobalSubscription.model"); + +class SqlSuspendSubscribeService extends SuspendSubscribeService { + + /** + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ + constructor(req, res) { + super(req, res); + } + + async delete() { + + if (!(await this.isGlobalSubscriptionExist())) { + throw new DicomWebServiceError( + DicomWebStatusCodes.ProcessingFailure, + "The target Subscription was not found.", + 404 + ); + } + + await this.deleteGlobalSubscription(); + + } + + async deleteGlobalSubscription() { + await UpsGlobalSubscriptionModel.destroy({ + where: { + aeTitle: this.subscriberAeTitle + } + }); + } + + async isGlobalSubscriptionExist() { + return await UpsGlobalSubscriptionModel.destroy({ + where: { + aeTitle: this.subscriberAeTitle + } + }) > 0; + } + +} + + +module.exports.SuspendSubscribeService = SqlSuspendSubscribeService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/unsubscribe.service.js b/api-sql/dicom-web/controller/UPS-RS/service/unsubscribe.service.js new file mode 100644 index 00000000..cafc5d37 --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/unsubscribe.service.js @@ -0,0 +1,105 @@ +const _ = require("lodash"); +const { DicomJsonModel } = require("@dicom-json-model"); +const { DicomCode } = require("@models/DICOM/code"); +const { + DicomWebServiceError, + DicomWebStatusCodes +} = require("@error/dicom-web-service"); +const { SUBSCRIPTION_FIXED_UIDS } = require("@models/DICOM/ups"); +const { UnSubscribeService } = require("@root/api/dicom-web/controller/UPS-RS/service/unsubscribe.service"); +const { WorkItemModel } = require("@models/sql/models/workitems.model"); +const { UpsSubscriptionModel } = require("@models/sql/models/upsSubscription.model"); +const { UpsGlobalSubscriptionModel } = require("@models/sql/models/upsGlobalSubscription.model"); + +class SqlUnSubscribeService extends UnSubscribeService { + + /** + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ + constructor(req, res) { + super(req, res); + } + + async delete() { + + if (this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.GlobalUID || + this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.FilteredGlobalUID) { + + if (!(await this.isGlobalSubscriptionExist())) { + throw new DicomWebServiceError( + DicomWebStatusCodes.ProcessingFailure, + "The target Subscription was not found.", + 404 + ); + } + + await this.deleteGlobalSubscription(); + + } else { + let workItem = await WorkItemModel.findOneWorkItemByUpsInstanceUID(this.upsInstanceUID); + + if (!(await this.isSubscriptionExist())) { + throw new DicomWebServiceError( + DicomWebStatusCodes.ProcessingFailure, + "The target Subscription was not found.", + 404 + ); + } + + await this.deleteSubscription(workItem); + } + + } + + /** + * + * @param {WorkItemModel} workItem + */ + async deleteSubscription(workItem) { + let subscription = await UpsSubscriptionModel.findOne({ + where: { + aeTitle: this.subscriberAeTitle + } + }); + await subscription.removeUPSWorkItem(workItem); + } + + async deleteGlobalSubscription() { + + await Promise.all([ + UpsSubscriptionModel.destroy({ + where: { + aeTitle: this.subscriberAeTitle + } + }), + UpsGlobalSubscriptionModel.destroy({ + where: { + aeTitle: this.subscriberAeTitle + } + }) + ]); + + } + + async isSubscriptionExist() { + return await UpsSubscriptionModel.count({ + where: { + aeTitle: this.subscriberAeTitle + } + }) > 0; + } + + async isGlobalSubscriptionExist() { + return await UpsGlobalSubscriptionModel.count({ + where: { + aeTitle: this.subscriberAeTitle + } + }) > 0; + } + +} + + +module.exports.UnSubscribeService = SqlUnSubscribeService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/UPS-RS/service/update-workItem.service.js b/api-sql/dicom-web/controller/UPS-RS/service/update-workItem.service.js new file mode 100644 index 00000000..f89389dc --- /dev/null +++ b/api-sql/dicom-web/controller/UPS-RS/service/update-workItem.service.js @@ -0,0 +1,73 @@ +const { WorkItemModel } = require("@dbModels/workitems.model"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { DicomJsonModel } = require("@dicom-json-model"); +const { UpdateWorkItemService } = require("@root/api/dicom-web/controller/UPS-RS/service/update-workItem.service"); +const { UpsWorkItemPersistentObject } = require("@models/sql/po/upsWorkItem.po"); +const { set, get } = require("lodash"); +const { PatientModel } = require("@models/sql/models/patient.model"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { UPS_EVENT_TYPE } = require("@root/api/dicom-web/controller/UPS-RS/service/workItem-event"); + +class SqlUpdateWorkItemService extends UpdateWorkItemService { + constructor(req, res) { + super(req, res); + this.transactionUID = this.requestWorkItem.getString("00081195"); + set(this.requestWorkItem.dicomJson, "upsInstanceUID", this.request.params.workItem); + } + + async updateUps() { + this.workItem = await WorkItemModel.findOneWorkItemDicomJsonModel(this.request.params.workItem); + await this.checkRequestUpsIsValid(); + this.adjustRequestWorkItem(); + + let patient = await PatientModel.updateOrCreatePatient(this.requestWorkItem.dicomJson); + let workItem = new UpsWorkItemPersistentObject(this.requestWorkItem.dicomJson, patient); + let savedWorkItem = await workItem.save(); + + this.triggerUpdateWorkItemEvent(savedWorkItem); + } + + /** + * replace not allowed updating attribute in request work item + */ + adjustRequestWorkItem() { + for (let i = 0; i < UpdateWorkItemService.notAllowedAttributes.length; i++) { + let notAllowedAttr = UpdateWorkItemService.notAllowedAttributes[i]; + let originalValueOfNotAllowedAttr = get(this.workItem.dicomJson, notAllowedAttr); + set(this.requestWorkItem.dicomJson, originalValueOfNotAllowedAttr); + } + } + + /** + * + * @param {WorkItemModel} workItem + * @returns + */ + async triggerUpdateWorkItemEvent(workItem) { + let updateWorkItemDicomJson = new DicomJsonModel(workItem); + let hitSubscriptions = await this.getHitSubscriptions(updateWorkItemDicomJson); + if (hitSubscriptions.length === 0) { + return workItem; + } + let hitSubscriptionAeTitleArray = hitSubscriptions.map(sub => sub.aeTitle); + + //Each time the SCP changes the Input Readiness State (0040,4041) Attribute for a UPS instance, the SCP shall send a UPS State Report Event to subscribed SCUs. + let modifiedInputReadLineState = this.requestWorkItem.getString(`${dictionary.keyword.InputReadinessState}`); + let originalInputReadLineState = this.workItem.getString(`${dictionary.keyword.InputReadinessState}`); + if (modifiedInputReadLineState && modifiedInputReadLineState !== originalInputReadLineState) { + this.addUpsEvent( + UPS_EVENT_TYPE.StateReport, + this.workItem.dicomJson.upsInstanceUID, + this.stateReportOf(workItem.toDicomJsonModel()), + hitSubscriptionAeTitleArray + ); + } + + this.addProgressInfoUpdatedEvent(workItem.toDicomJsonModel(), hitSubscriptionAeTitleArray); + this.addAssignedEvents(workItem.toDicomJsonModel(), hitSubscriptionAeTitleArray); + + this.triggerUpsEvents(); + } +} + +module.exports.UpdateWorkItemService = SqlUpdateWorkItemService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js b/api-sql/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js new file mode 100644 index 00000000..22a6ec05 --- /dev/null +++ b/api-sql/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js @@ -0,0 +1,88 @@ +const { + BulkDataService, + StudyBulkDataFactory, + SeriesBulkDataFactory, + InstanceBulkDataFactory, + SpecificBulkDataFactory +} = require("@root/api/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata"); +const { DicomBulkDataModel } = require("@models/sql/models/dicomBulkData.model"); +const { Op } = require("sequelize"); + +StudyBulkDataFactory.prototype.getBulkData = async function () { + let { + studyUID + } = this.uids; + + let studyBulkDataArray = await DicomBulkDataModel.findAll({ + where: { + studyUID + } + }); + + return studyBulkDataArray; +}; + +SeriesBulkDataFactory.prototype.getBulkData = async function () { + let { + studyUID, + seriesUID + } = this.uids; + + let seriesBulkDataArray = await DicomBulkDataModel.findAll({ + where: { + studyUID, + seriesUID + } + }); + + return seriesBulkDataArray; +}; + +InstanceBulkDataFactory.prototype.getBulkData = async function () { + let { + studyUID, + seriesUID, + instanceUID + } = this.uids; + + let instanceBulkDataArray = await DicomBulkDataModel.findAll({ + where: { + studyUID, + seriesUID, + instanceUID + } + }); + + return instanceBulkDataArray; +}; + +SpecificBulkDataFactory.prototype.getBulkData = async function () { + let { + studyUID, + seriesUID, + instanceUID, + binaryValuePath + } = this.uids; + + /** @type { import("sequelize").FindOptions } */ + let findOption = { + where: { + studyUID, + seriesUID, + instanceUID, + binaryValuePath: { + [Op.like]: `%${binaryValuePath}%` + } + } + }; + + let bulkData = await DicomBulkDataModel.findOne(findOption); + + return bulkData; +}; + +module.exports.BulkDataService = BulkDataService; +module.exports.StudyBulkDataFactory = StudyBulkDataFactory; +module.exports.SeriesBulkDataFactory = SeriesBulkDataFactory; +module.exports.InstanceBulkDataFactory = InstanceBulkDataFactory; +module.exports.SpecificBulkDataFactory = SpecificBulkDataFactory; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/WADO-RS/deletion/service/delete.js b/api-sql/dicom-web/controller/WADO-RS/deletion/service/delete.js new file mode 100644 index 00000000..15476362 --- /dev/null +++ b/api-sql/dicom-web/controller/WADO-RS/deletion/service/delete.js @@ -0,0 +1,74 @@ +const { NotFoundInstanceError } = require("../../../../../../error/dicom-instance"); +const { StudyModel } = require("@models/sql/models/study.model"); +const { SeriesModel } = require("@models/sql/models/series.model"); +const { InstanceModel } = require("@models/sql/models/instance.model"); + +class DeleteService { + /** + * + * @param {import("express").Request} req + * @param {import("express").Response} res + * @param { "study" | "series" | "instance" } level + */ + constructor(req, res, level = "study") { + this.request = req; + this.response = res; + this.level = level; + } + + async delete() { + let deleteFns = {}; + deleteFns["study"] = async () => this.deleteStudy(); + deleteFns["series"] = async () => this.deleteSeries(); + deleteFns["instance"] = async () => this.deleteInstance(); + + await deleteFns[this.level](); + } + + async deleteStudy() { + let study = await StudyModel.findOne({ + where: { + x0020000D: this.request.params.studyUID + } + }); + + if (!study) { + throw new NotFoundInstanceError(`Can not found studyUID: ${this.request.params.studyUID} instances' files`); + } + + await study.incrementDeleteStatus(); + } + + async deleteSeries() { + let aSeries = await SeriesModel.findOne({ + where: { + x0020000D: this.request.params.studyUID, + x0020000E: this.request.params.seriesUID + } + }); + + if (!aSeries) { + throw new NotFoundInstanceError(`Can not found studyUID: ${this.request.params.studyUID}, seriesUID: ${this.request.params.seriesUID}' files`); + } + + await aSeries.incrementDeleteStatus(); + } + + async deleteInstance() { + let instance = await InstanceModel.findOne({ + where: { + x0020000D: this.request.params.studyUID, + x0020000E: this.request.params.seriesUID, + x00080018: this.request.params.instanceUID + } + }); + + if (!instance) { + throw new NotFoundInstanceError(`Can not found studyUID: ${this.request.params.studyUID}, seriesUID: ${this.request.params.seriesUID}, instanceUID: ${this.request.params.instanceUID} instances' files`); + } + + await instance.incrementDeleteStatus(); + } +} + +module.exports.DeleteService = DeleteService; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/WADO-RS/service/WADO-RS.service.js b/api-sql/dicom-web/controller/WADO-RS/service/WADO-RS.service.js new file mode 100644 index 00000000..4efe492f --- /dev/null +++ b/api-sql/dicom-web/controller/WADO-RS/service/WADO-RS.service.js @@ -0,0 +1,46 @@ +const { InstanceModel } = require("@models/sql/models/instance.model"); +const { StudyModel } = require("@models/sql/models/study.model"); +const { SeriesModel } = require("@models/sql/models/series.model"); +const { + getAcceptType, + supportInstanceMultipartType, + sendNotSupportedMediaType, + addHostnameOfBulkDataUrl, + multipartContentTypeWriter, + ImageMultipartWriter, + getUidsString, + ImagePathFactory, + StudyImagePathFactory, + SeriesImagePathFactory, + InstanceImagePathFactory +} = require("@root/api/dicom-web/controller/WADO-RS/service/WADO-RS.service"); + + +StudyImagePathFactory.prototype.getImagePaths = async function () { + this.imagePaths = await StudyModel.getPathGroupOfInstances(this.uids); +}; + +SeriesImagePathFactory.prototype.getImagePaths = async function () { + this.imagePaths = await SeriesModel.getPathGroupOfInstances(this.uids); +}; + +InstanceImagePathFactory.prototype.getImagePaths = async function () { + let imagePath = await InstanceModel.getPathOfInstance(this.uids); + + if (imagePath) + this.imagePaths = [imagePath]; + else + this.imagePaths = []; +}; + +module.exports.getAcceptType = getAcceptType; +module.exports.supportInstanceMultipartType = supportInstanceMultipartType; +module.exports.sendNotSupportedMediaType = sendNotSupportedMediaType; +module.exports.addHostnameOfBulkDataUrl = addHostnameOfBulkDataUrl; +module.exports.ImagePathFactory = ImagePathFactory; +module.exports.StudyImagePathFactory = StudyImagePathFactory; +module.exports.SeriesImagePathFactory = SeriesImagePathFactory; +module.exports.InstanceImagePathFactory = InstanceImagePathFactory; +module.exports.multipartContentTypeWriter = multipartContentTypeWriter; +module.exports.ImageMultipartWriter = ImageMultipartWriter; +module.exports.getUidsString = getUidsString; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/WADO-RS/service/rendered.service.js b/api-sql/dicom-web/controller/WADO-RS/service/rendered.service.js new file mode 100644 index 00000000..cdaab2ed --- /dev/null +++ b/api-sql/dicom-web/controller/WADO-RS/service/rendered.service.js @@ -0,0 +1,318 @@ +const { + handleImageQuality, + handleViewport, + writeRenderedImages, + writeSpecificFramesRenderedImages, + RenderedImageMultipartWriter +} = require("@root/api/dicom-web/controller/WADO-RS/service/rendered.service"); +const fs = require("fs"); +const sharp = require("sharp"); +const path = require("path"); +const _ = require("lodash"); +const { MultipartWriter } = require("@root/utils/multipartWriter"); +const notImageSOPClass = require("@models/DICOM/dicomWEB/notImageSOPClass"); +const Magick = require("@models/magick"); + +const errorResponse = require("@root/utils/errorResponse/errorResponseMessage"); +const { logger } = require("@root/utils/logs/log"); + +const { raccoonConfig } = require("@root/config-class"); +const { Op } = require("sequelize"); +const { InstanceModel } = require("@models/sql/models/instance.model.js"); +const { DicomBulkDataModel } = require("@models/sql/models/dicomBulkData.model"); +const { default: Dcm2JpgExecutor } = require("@java-wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor"); +const { Dcm2JpgExecutor$Dcm2JpgOptions } = require("@java-wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor$Dcm2JpgOptions"); + +/** + * + * @param {*} param The req.query + * @param {Magick} magick + * @param {string} instanceID + */ +async function handleImageICCProfile(param, magick, instanceID) { + let iccProfileAction = { + "no" : async ()=> {}, + "yes": async ()=> { + let iccProfileBinaryFile = await DicomBulkDataModel.findOne({ + where: { + binaryValuePath: "00480105.Value.0.00282000.InlineBinary", + instanceUID: instanceID + } + }); + if(!iccProfileBinaryFile) throw new Error("The Image dose not have icc profile tag"); + let iccProfileSrc = path.join(raccoonConfig.dicomWebConfig.storeRootPath, iccProfileBinaryFile.filename); + let dest = path.join(raccoonConfig.dicomWebConfig.storeRootPath, iccProfileBinaryFile.filename + `.icc`); + if (!fs.existsSync(dest)) fs.copyFileSync(iccProfileSrc, dest); + magick.iccProfile(dest); + }, + "srgb": async ()=> { + magick.iccProfile(path.join(process.cwd(), "models/DICOM/dicomWEB/iccprofiles/sRGB.icc")); + }, + "adobergb": async () => { + magick.iccProfile(path.join(process.cwd(), "models/DICOM/dicomWEB/iccprofiles/adobeRGB.icc")); + }, + "rommrgb": async ()=> { + magick.iccProfile(path.join(process.cwd(), "models/DICOM/dicomWEB/iccprofiles/rommRGB.icc")); + } + }; + try { + if (param.iccprofile) { + await iccProfileAction[param.iccprofile](); + } + } catch(e) { + console.error("set icc profile error:" , e); + throw e; + } +} + +class FramesWriter { + /** + * + * @param {import("../../../../../utils/typeDef/WADO-RS/WADO-RS.def").ImagePathObj[]} imagePaths + */ + constructor(req, res, imagePaths) { + this.request = req; + this.response = res; + this.imagePaths = imagePaths; + } + + async write() { + let multipartWriter = new MultipartWriter([], this.request, this.response); + for (let imagePathObj of this.imagePaths) { + let instanceFramesObj = await getInstanceFrameObj(imagePathObj); + if (_.isUndefined(instanceFramesObj)) continue; + let dicomNumberOfFrames = _.get(instanceFramesObj, "x00280008", 1); + dicomNumberOfFrames = parseInt(dicomNumberOfFrames); + await writeRenderedImages(this.request, dicomNumberOfFrames, instanceFramesObj, multipartWriter); + } + multipartWriter.writeFinalBoundary(); + } +} + +class StudyFramesWriter extends FramesWriter { + /** + * + * @param {import("../../../../../utils/typeDef/WADO-RS/WADO-RS.def").ImagePathObj[]} imagePaths + */ + constructor(req, res, imagePaths) { + super(req, res, imagePaths); + } +} + +class SeriesFramesWriter extends FramesWriter { + constructor(req, res, imagePaths) { + super(req, res, imagePaths); + } +} + +class InstanceFramesWriter extends FramesWriter { + constructor(req, res, imagePaths) { + super(req, res, imagePaths); + } + + async write() { + let multipartWriter = new MultipartWriter([], this.request, this.response); + let instanceFramesObj = await getInstanceFrameObj(this.imagePaths[0]); + if (_.isUndefined(instanceFramesObj)) { + return this.response.status(400).json( + errorResponse.getBadRequestErrorMessage(`instance: ${this.request.params.instanceUID} doesn't have pixel data`) + ); + } + let dicomNumberOfFrames = _.get(instanceFramesObj, "x00280008", 1); + dicomNumberOfFrames = parseInt(dicomNumberOfFrames); + await writeRenderedImages(this.request, dicomNumberOfFrames, instanceFramesObj, multipartWriter); + multipartWriter.writeFinalBoundary(); + } +} + +class InstanceFramesListWriter extends FramesWriter { + constructor(req, res, imagePaths) { + super(req, res, imagePaths); + this.instanceFramesObj = {}; + this.dicomNumberOfFrames = 1; + } + + async write() { + let { frameNumber } = this.request.params; + + this.instanceFramesObj = await getInstanceFrameObj(this.imagePaths[0]); + if (_.isUndefined(this.instanceFramesObj)) { + return this.response.status(400).json( + errorResponse.getBadRequestErrorMessage(`instance: ${this.request.params.instanceUID} doesn't have pixel data`) + ); + } + this.dicomNumberOfFrames = _.get(this.instanceFramesObj, "x00280008", 1); + this.dicomNumberOfFrames = parseInt(this.dicomNumberOfFrames); + + if (this.isInvalidFrameNumber()) return; + + if (frameNumber.length == 1) { + return this.writeSingleFrame(); + } else { + let multipartWriter = new MultipartWriter([], this.request, this.response); + await writeSpecificFramesRenderedImages(this.request, frameNumber, this.instanceFramesObj, multipartWriter); + multipartWriter.writeFinalBoundary(); + return true; + } + } + + isInvalidFrameNumber() { + for (let i = 0; i < this.request.params.frameNumber.length; i++) { + let frame = this.request.params.frameNumber[i]; + if (frame > this.dicomNumberOfFrames) { + let badRequestMessage = errorResponse.getBadRequestErrorMessage(`Bad frame number , \ +This instance NumberOfFrames is : ${this.dicomNumberOfFrames} , But request ${JSON.stringify(this.request.params.frameNumber)}`); + this.response.writeHead(badRequestMessage.HttpStatus, { + "Content-Type": "application/dicom+json" + }); + + let badRequestMessageStr = JSON.stringify(badRequestMessage); + + logger.warn(badRequestMessageStr); + + return this.response.end(JSON.stringify(badRequestMessageStr)); + } + } + return false; + } + + async writeSingleFrame() { + let postProcessResult = await postProcessFrameImage(this.request, this.request.params.frameNumber[0], this.instanceFramesObj); + if (postProcessResult.status) { + this.response.writeHead(200, { + "Content-Type": "image/jpeg" + }); + + return postProcessResult.magick.toBuffer(); + } + throw new Error(`Can not process this image, instanceUID: ${this.instanceFramesObj.instanceUID}, frameNumber: ${this.request.frameNumber[0]}`); + } +} + +/** + * SQL 的彈性比較低,此 function 採與 MongoDB 相同呼叫方式,但在欄位設計上較死,otherFields 無用處 + * + * SQL has lower flexibility compared to MongoDB. + * This function adopts the same calling method as MongoDB, but it is more rigid in terms of field design. + * Note that otherFields is not used. + * @param {Object} iParam + * @return { Promise | Promise } + */ +async function getInstanceFrameObj(iParam, otherFields = {}) { + let { studyUID, seriesUID, instanceUID } = iParam; + try { + /** @type { import("sequelize").FindOptions } */ + let query = { + where: { + x0020000D: studyUID, + x0020000E: seriesUID, + x00080018: instanceUID, + x00080016: { + [Op.notIn]: notImageSOPClass + }, + deleteStatus: 0 + }, + attributes: [ + "instancePath", + "x00020010", + "x0020000D", + "x0020000E", + "x00080018", + "x00280008", + "x00281050", + "x00281051" + ] + }; + + let instance = await InstanceModel.findOne(query); + if (instance) { + let instanceJson = instance.toJSON(); + + _.set(instanceJson, "studyUID", instanceJson.x0020000D); + _.set(instanceJson, "seriesUID", instanceJson.x0020000E); + _.set(instanceJson, "instanceUID", instanceJson.x00080018); + _.set(instanceJson, "instancePath", path.join( + raccoonConfig.dicomWebConfig.storeRootPath, + instanceJson.instancePath + )); + + return instanceJson; + } + + return undefined; + } catch (e) { + throw e; + } +} + +/** + * + * @param {import("http").IncomingMessage} req + * @param {import("../../../../../utils/typeDef/WADO-RS/WADO-RS.def").InstanceFrameObj} instanceFramesObj + * @returns + */ +async function postProcessFrameImage(req, frameNumber, instanceFramesObj) { + try { + + let dicomFilename = instanceFramesObj.instancePath; + let jpegFile = dicomFilename.replace(/\.dcm\b/gi , `.${frameNumber-1}.jpg`); + + + + let dcm2jpgOptions = await Dcm2JpgExecutor$Dcm2JpgOptions.newInstanceAsync(); + dcm2jpgOptions.frameNumber = frameNumber; + let getFrameImageStatus = await Dcm2JpgExecutor.convertDcmToJpgFromFilename( + dicomFilename, + jpegFile, + dcm2jpgOptions + ); + + if (getFrameImageStatus.status) { + let imageSharp = sharp(jpegFile); + let magick = new Magick(jpegFile); + handleImageQuality( + req.query, + magick + ); + await handleImageICCProfile( + req.query, + magick, + instanceFramesObj.instanceUID + ); + await handleViewport( + req.query, + imageSharp, + magick + ); + await magick.execCommand(); + return { + status: true, + message: "process successful", + magick: magick + }; + } + return { + status: false, + message: "get frame image failed", + magick: undefined + }; + } catch(e) { + console.error(e); + return { + status: false, + message: e, + magick: undefined + }; + } +} + + +module.exports.postProcessFrameImage = postProcessFrameImage; +module.exports.writeRenderedImages = writeRenderedImages; +module.exports.writeSpecificFramesRenderedImages = writeSpecificFramesRenderedImages; +module.exports.getInstanceFrameObj = getInstanceFrameObj; +module.exports.RenderedImageMultipartWriter = RenderedImageMultipartWriter; +module.exports.StudyFramesWriter = StudyFramesWriter; +module.exports.SeriesFramesWriter = SeriesFramesWriter; +module.exports.InstanceFramesWriter = InstanceFramesWriter; +module.exports.InstanceFramesListWriter = InstanceFramesListWriter; \ No newline at end of file diff --git a/api-sql/dicom-web/controller/WADO-RS/service/thumbnail.service.js b/api-sql/dicom-web/controller/WADO-RS/service/thumbnail.service.js new file mode 100644 index 00000000..1e635bc4 --- /dev/null +++ b/api-sql/dicom-web/controller/WADO-RS/service/thumbnail.service.js @@ -0,0 +1,33 @@ +const renderedService = require("@rendered-service"); +const _ = require("lodash"); +const { + ThumbnailService, + StudyThumbnailFactory, + SeriesThumbnailFactory, + InstanceThumbnailFactory +} = require("@root/api/dicom-web/controller/WADO-RS/service/thumbnail.service"); + +ThumbnailService.prototype.getThumbnailByInstance = async function (instanceFramesObj) { + let dicomNumberOfFrames = _.get(instanceFramesObj, "x00280008", 1); + dicomNumberOfFrames = parseInt(dicomNumberOfFrames); + let medianFrame = 1; + if (dicomNumberOfFrames > 1) medianFrame = dicomNumberOfFrames >> 1; + if (this.request.params.frameNumber) { + medianFrame = this.request.params.frameNumber[0]; + } + + let postProcessResult = await renderedService.postProcessFrameImage(this.request, medianFrame, instanceFramesObj); + if (postProcessResult.status) { + this.response.writeHead(200, { + "Content-Type": "image/jpeg" + }); + this.apiLogger.logger.info(`Get instance's thumbnail successfully, instance UID: ${instanceFramesObj.instanceUID}`); + return postProcessResult.magick.toBuffer(); + } + return undefined; +}; + +module.exports.ThumbnailService = ThumbnailService; +module.exports.StudyThumbnailFactory = StudyThumbnailFactory; +module.exports.SeriesThumbnailFactory = SeriesThumbnailFactory; +module.exports.InstanceThumbnailFactory = InstanceThumbnailFactory; \ No newline at end of file diff --git a/api/WADO-URI/controller/retrieveInstance.js b/api/WADO-URI/controller/retrieveInstance.js index efb12700..4af01fba 100644 --- a/api/WADO-URI/controller/retrieveInstance.js +++ b/api/WADO-URI/controller/retrieveInstance.js @@ -1,4 +1,4 @@ -const { WadoUriService, NotFoundInstanceError } = require("../service/WADO-URI.service"); +const { WadoUriService, NotFoundInstanceError } = require("@wado-uri-service"); const { Controller } = require("../../controller.class"); const { ApiLogger } = require("../../../utils/logs/api-logger"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); diff --git a/api/dicom-web/controller/MWL-RS/change-filtered-mwlItem-status.js b/api/dicom-web/controller/MWL-RS/change-filtered-mwlItem-status.js new file mode 100644 index 00000000..9836a99a --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/change-filtered-mwlItem-status.js @@ -0,0 +1,40 @@ +const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); +const { Controller } = require("@root/api/controller.class"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { ChangeFilteredMwlItemStatusService } = require("@mwl-service/change-filtered-mwlItem-status"); + +class ChangeFilteredMwlItemStatusController extends Controller { + constructor(req, res) { + super(req, res); + this.apiLogger = new ApiLogger(this.request, "MWL-RS"); + } + + async mainProcess() { + try { + let changeFilteredMwlItemService = new ChangeFilteredMwlItemStatusService(this.request, this.response); + let changedMwlItemsCount = await changeFilteredMwlItemService.changeMwlItemsStatus(); + + return this.response + .set("Content-Type", "application/dicom+json") + .status(200) + .json({ + updatedCount: changedMwlItemsCount + }); + + } catch (e) { + let apiErrorArrayHandler = new ApiErrorArrayHandler(this.response, this.apiLogger, e); + return apiErrorArrayHandler.doErrorResponse(); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new ChangeFilteredMwlItemStatusController(req, res); + + await controller.doPipeline(); +}; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/change-mwlItem-status.js b/api/dicom-web/controller/MWL-RS/change-mwlItem-status.js new file mode 100644 index 00000000..5b7a04aa --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/change-mwlItem-status.js @@ -0,0 +1,38 @@ +const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); +const { Controller } = require("@root/api/controller.class"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { ChangeMwlItemStatusService } = require("@mwl-service/change-mwlItem-status"); + +class ChangeMwlItemStatusController extends Controller { + constructor(req, res) { + super(req, res); + this.apiLogger = new ApiLogger(this.request, "MWL-RS"); + } + + async mainProcess() { + try { + let changeMwlItemService = new ChangeMwlItemStatusService(this.request, this.response); + let changedMwlItems = await changeMwlItemService.changeMwlItemsStatus(); + + return this.response + .set("Content-Type", "application/dicom+json") + .status(200) + .json(changedMwlItems); + + } catch (e) { + let apiErrorArrayHandler = new ApiErrorArrayHandler(this.response, this.apiLogger, e); + return apiErrorArrayHandler.doErrorResponse(); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new ChangeMwlItemStatusController(req, res); + + await controller.doPipeline(); +}; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/count-mwlItem.js b/api/dicom-web/controller/MWL-RS/count-mwlItem.js new file mode 100644 index 00000000..f64debfa --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/count-mwlItem.js @@ -0,0 +1,39 @@ +const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); +const { Controller } = require("@root/api/controller.class"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { GetMwlItemCountService } = require("@mwl-service/count-mwlItem.service"); + +class GetMwlItemCountController extends Controller { + constructor(req, res) { + super(req, res); + this.apiLogger = new ApiLogger(this.request, "MWL-RS"); + } + + async mainProcess() { + try { + let getMwlItemCountService = new GetMwlItemCountService(this.request, this.response); + let count = await getMwlItemCountService.getMwlItemCount(); + return this.response + .set("Content-Type", "application/dicom+json") + .status(200) + .json({ + count + }); + + } catch (e) { + let apiErrorArrayHandler = new ApiErrorArrayHandler(this.response, this.apiLogger, e); + return apiErrorArrayHandler.doErrorResponse(); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new GetMwlItemCountController(req, res); + + await controller.doPipeline(); +}; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/create-mwlItem.js b/api/dicom-web/controller/MWL-RS/create-mwlItem.js new file mode 100644 index 00000000..aa387248 --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/create-mwlItem.js @@ -0,0 +1,37 @@ +const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); +const { Controller } = require("@root/api/controller.class"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { CreateMwlItemService } = require("@mwl-service/create-mwlitem.service"); + +class CreateMwlItemController extends Controller { + constructor(req, res) { + super(req, res); + this.apiLogger = new ApiLogger(this.request, "MWL-RS"); + } + + async mainProcess() { + try { + let createMwlItemService = new CreateMwlItemService(this.request, this.response); + let mwlItem = await createMwlItemService.create(); + return this.response + .set("Content-Type", "application/dicom+json") + .status(201) + .json(mwlItem); + + } catch (e) { + let apiErrorArrayHandler = new ApiErrorArrayHandler(this.response, this.apiLogger, e); + return apiErrorArrayHandler.doErrorResponse(); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new CreateMwlItemController(req, res); + + await controller.doPipeline(); +}; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/delete-mwlItem.js b/api/dicom-web/controller/MWL-RS/delete-mwlItem.js new file mode 100644 index 00000000..d038ae12 --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/delete-mwlItem.js @@ -0,0 +1,38 @@ +const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); +const { Controller } = require("@root/api/controller.class"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { DeleteMwlItemService } = require("./service/delete-mwlItem.service"); + +class DeleteMwlItemCountController extends Controller { + constructor(req, res) { + super(req, res); + this.apiLogger = new ApiLogger(this.request, "MWL-RS"); + } + + async mainProcess() { + try { + let deleteMwlItemService = new DeleteMwlItemService(this.request, this.response); + await deleteMwlItemService.deleteMwlItem(); + + return this.response + .set("Content-Type", "application/dicom+json") + .status(200) + .send(); + + } catch (e) { + let apiErrorArrayHandler = new ApiErrorArrayHandler(this.response, this.apiLogger, e); + return apiErrorArrayHandler.doErrorResponse(); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new DeleteMwlItemCountController(req, res); + + await controller.doPipeline(); +}; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/get-mwlItem.js b/api/dicom-web/controller/MWL-RS/get-mwlItem.js new file mode 100644 index 00000000..b3065fec --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/get-mwlItem.js @@ -0,0 +1,40 @@ +const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); +const { Controller } = require("@root/api/controller.class"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); +const { GetMwlItemService } = require("@mwl-service/get-mwlItem.service"); + +class GetMwlItemController extends Controller { + constructor(req, res) { + super(req, res); + this.apiLogger = new ApiLogger(this.request, "MWL-RS"); + } + + async mainProcess() { + try { + let mwlItems = await new GetMwlItemService(this.request, this.response).getMwlItems(); + + + if (mwlItems.length === 0) return this.response.status(204).end(); + + return this.response + .set("Content-Type", "application/dicom+json") + .status(200) + .json(mwlItems); + + } catch (e) { + let apiErrorArrayHandler = new ApiErrorArrayHandler(this.response, this.apiLogger, e); + return apiErrorArrayHandler.doErrorResponse(); + } + } +} + +/** + * + * @param {import('express').Request} req + * @param {import('express').Response} res + */ +module.exports = async function (req, res) { + let controller = new GetMwlItemController(req, res); + + await controller.doPipeline(); +}; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status.js b/api/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status.js new file mode 100644 index 00000000..4711b742 --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/service/change-filtered-mwlItem-status.js @@ -0,0 +1,34 @@ +const _ = require("lodash"); +const { MwlItemModel } = require("@models/mongodb/models/mwlitems.model"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { convertRequestQueryToMongoQuery } = require("../../QIDO-RS/service/query-dicom-json-factory"); +const { BaseQueryService } = require("@root/api/dicom-web/service/base-query.service"); + +class ChangeFilteredMwlItemStatusService extends BaseQueryService { + constructor(req, res) { + super(req, res); + } + + async changeMwlItemsStatus() { + let { status } = this.request.params; + let mwlItems = await this.getMwlItems(); + if (mwlItems.length === 0) { + throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchObjectInstance, "Can not found any MWL item from query", 404); + } + + for (let mwlItem of mwlItems) { + _.set(mwlItem, `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}.Value.0`, status); + await mwlItem.save(); + } + + return mwlItems.length; + } + + async getMwlItems() { + let query = (await convertRequestQueryToMongoQuery(this.query)).$match; + return await MwlItemModel.find(query); + } +} + +module.exports.ChangeFilteredMwlItemStatusService = ChangeFilteredMwlItemStatusService; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/service/change-mwlItem-status.js b/api/dicom-web/controller/MWL-RS/service/change-mwlItem-status.js new file mode 100644 index 00000000..e0e0ba2c --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/service/change-mwlItem-status.js @@ -0,0 +1,41 @@ +const _ = require("lodash"); +const { MwlItemModel } = require("@models/mongodb/models/mwlitems.model"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); + +class ChangeMwlItemStatusService { + constructor(req, res) { + /** @type {import("express").Request} */ + this.request = req; + /** @type {import("express").Response} */ + this.response = res; + } + + async changeMwlItemsStatus() { + let { status } = this.request.params; + let mwlItem = await this.getMwlItemByStudyUIDAndSpsID(); + if (!mwlItem) { + throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchObjectInstance, "No such object instance", 404); + } + + _.set(mwlItem, `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}.Value.0`, status); + await mwlItem.save(); + + return mwlItem.toDicomJson(); + } + + async getMwlItemByStudyUIDAndSpsID() { + return await MwlItemModel.findOne({ + $and: [ + { + "00400100.Value.0.00400009.Value.0": this.request.params.spsID + }, + { + "0020000D.Value.0": this.request.params.studyUID + } + ] + }); + } +} + +module.exports.ChangeMwlItemStatusService = ChangeMwlItemStatusService; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/service/count-mwlItem.service.js b/api/dicom-web/controller/MWL-RS/service/count-mwlItem.service.js new file mode 100644 index 00000000..449059be --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/service/count-mwlItem.service.js @@ -0,0 +1,16 @@ +const { MwlItemModel } = require("@models/mongodb/models/mwlitems.model"); +const { BaseQueryService } = require("@root/api/dicom-web/service/base-query.service"); +const { convertRequestQueryToMongoQuery } = require("../../QIDO-RS/service/query-dicom-json-factory"); + +class GetMwlItemCountService extends BaseQueryService { + constructor(req, res) { + super(req, res); + } + + async getMwlItemCount() { + this.query = (await convertRequestQueryToMongoQuery(this.query)).$match; + return await MwlItemModel.getCount(this.query); + } +} + +module.exports.GetMwlItemCountService = GetMwlItemCountService; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/service/create-mwlitem.service.js b/api/dicom-web/controller/MWL-RS/service/create-mwlitem.service.js new file mode 100644 index 00000000..dd43c762 --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/service/create-mwlitem.service.js @@ -0,0 +1,142 @@ +const _ = require("lodash"); +const { MwlItemModel } = require("@models/mongodb/models/mwlitems.model"); +const { PatientModel } = require("@models/mongodb/models/patient.model"); +const { StudyModel } = require("@models/mongodb/models/study.model"); +const crypto = require('crypto'); +const moment = require("moment"); +const { UIDUtils } = require("@dcm4che/util/UIDUtils"); +const { + DicomWebServiceError, + DicomWebStatusCodes +} = require("@error/dicom-web-service"); +const { DicomJsonModel, BaseDicomJson } = require("@models/DICOM/dicom-json-model"); +const { v4: uuidV4 } = require("uuid"); +const shortHash = require("shorthash2"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); + +class CreateMwlItemService { + constructor(req, res) { + this.request = req; + this.response = res; + this.requestMwlItem = /** @type {Object} */(this.request.body); + /** @type {DicomJsonModel} */ + this.requestMwlItemDicomJsonModel = new DicomJsonModel(this.requestMwlItem[0]); + this.apiLogger = new ApiLogger(req, "Create Mwl Item Service"); + this.apiLogger.addTokenValue(); + } + + async create() { + + await this.checkPatientExist(); + + let mwlItem = this.requestMwlItem[0]; + let mwlDicomJson = new BaseDicomJson(mwlItem); + + let spsItem = new BaseDicomJson({ + [dictionary.keyword.ScheduledProcedureStepSequence]: { + ...mwlItem[dictionary.keyword.ScheduledProcedureStepSequence] + } + }); + + let spsStatusPath = `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}`; + if (!spsItem.getValue(spsStatusPath)) { + spsItem.setValue(spsStatusPath, "SCHEDULED"); + } + + let spsIDPath = `${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepID}`; + if (!spsItem.getValue(spsIDPath)) { + let spsID = shortHash(uuidV4()); + spsItem.setValue(spsIDPath, `SPS-${spsID}`); + } + mwlItem[dictionary.keyword.ScheduledProcedureStepSequence] = { + ...mwlItem[dictionary.keyword.ScheduledProcedureStepSequence], + ...spsItem.dicomJson[dictionary.keyword.ScheduledProcedureStepSequence] + }; + + if (!mwlDicomJson.getValue(dictionary.keyword.RequestedProcedureID)) { + let rpID = shortHash(uuidV4()); + _.set(mwlItem, dictionary.keyword.RequestedProcedureID, { + vr: BaseDicomJson.getTagVrOfTag(dictionary.keyword.RequestedProcedureID), + Value: [ + `RP-${rpID}` + ] + }); + } + + if (!mwlDicomJson.getValue(dictionary.keyword.StudyInstanceUID)) { + let studyInstanceUID = await UIDUtils.createUID(); + _.set(mwlItem, dictionary.keyword.StudyInstanceUID, { + vr: BaseDicomJson.getTagVrOfTag(dictionary.keyword.StudyInstanceUID), + Value: [ + studyInstanceUID + ] + }); + } + + if (!mwlDicomJson.getValue(dictionary.keyword.AccessionNumber)) { + let accessionNumber = shortHash(uuidV4()); + _.set(mwlItem, dictionary.keyword.AccessionNumber, { + vr: BaseDicomJson.getTagVrOfTag(dictionary.keyword.AccessionNumber), + Value: [ + accessionNumber + ] + }); + } + + return await this.createOrUpdateMwl(mwlDicomJson); + } + + async checkPatientExist() { + let patientID = this.requestMwlItemDicomJsonModel.getString("00100020"); + let patientCount = await PatientModel.count({ + patientID + }); + if (patientCount <= 0) { + throw new DicomWebServiceError( + DicomWebStatusCodes.MissingAttribute, + `Patient[id=${patientID}] does not exists`, + 404 + ); + } + } + + /** + * + * @param {BaseDicomJson} mwlDicomJson + */ + async createOrUpdateMwl(mwlDicomJson) { + let studyInstanceUID = mwlDicomJson.getValue(dictionary.keyword.StudyInstanceUID); + let spsItem = new BaseDicomJson(mwlDicomJson.getValue(dictionary.keyword.ScheduledProcedureStepSequence)); + let spsID = spsItem.getValue(dictionary.keyword.ScheduledProcedureStepID); + let foundMwl = await MwlItemModel.findOne({ + $and: [ + { + "0020000D.Value.0": studyInstanceUID + }, + { + "00400100.Value.0.00400009.Value.0": spsID + } + ] + }); + + if (!foundMwl) { + // create + let mwlItemModelObj = new MwlItemModel(mwlDicomJson.dicomJson); + await mwlItemModelObj.save(); + this.apiLogger.logger.info(`create mwl item: ${studyInstanceUID}`); + return mwlItemModelObj.toDicomJson(); + } else { + // update + foundMwl.$set({ + ...mwlDicomJson.dicomJson + }); + await foundMwl.save(); + this.apiLogger.logger.info(`update mwl item: ${studyInstanceUID}`); + return foundMwl.toDicomJson(); + } + } + +} + +module.exports.CreateMwlItemService = CreateMwlItemService; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/service/delete-mwlItem.service.js b/api/dicom-web/controller/MWL-RS/service/delete-mwlItem.service.js new file mode 100644 index 00000000..634300ff --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/service/delete-mwlItem.service.js @@ -0,0 +1,22 @@ +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { MwlItemModel } = require("@dbModels/mwlitems.model"); + +class DeleteMwlItemService { + constructor(req, res) { + /** @type { import("express").Request } */ + this.request = req; + /** @type { import("express").Response } */ + this.response = res; + } + + async deleteMwlItem() { + const { studyUID, spsID } = this.request.params; + let { deletedCount } = await MwlItemModel.deleteByStudyInstanceUIDAndSpsID(studyUID, spsID); + + if (!deletedCount) { + throw new DicomWebServiceError(DicomWebStatusCodes.NoSuchSOPInstance, "Modality Worklist Item not found.", 404); + } + } +} + +module.exports.DeleteMwlItemService = DeleteMwlItemService; \ No newline at end of file diff --git a/api/dicom-web/controller/MWL-RS/service/get-mwlItem.service.js b/api/dicom-web/controller/MWL-RS/service/get-mwlItem.service.js new file mode 100644 index 00000000..e277b18e --- /dev/null +++ b/api/dicom-web/controller/MWL-RS/service/get-mwlItem.service.js @@ -0,0 +1,25 @@ +const { MwlItemModel } = require("@models/mongodb/models/mwlitems.model"); +const { BaseQueryService } = require("@root/api/dicom-web/service/base-query.service"); +const { convertRequestQueryToMongoQuery } = require("../../QIDO-RS/service/query-dicom-json-factory"); + +class GetMwlItemService extends BaseQueryService { + constructor(req, res) { + super(req, res); + } + + async getMwlItems() { + let query = (await convertRequestQueryToMongoQuery(this.query)).$match; + let queryOptions = { + query, + skip: this.skip_, + limit: this.limit_, + includeFields: this.includeFields_ + }; + + let docs = await MwlItemModel.getDicomJson(queryOptions); + + return docs; + } +} + +module.exports.GetMwlItemService = GetMwlItemService; \ No newline at end of file diff --git a/api/dicom-web/controller/QIDO-RS/base.controller.js b/api/dicom-web/controller/QIDO-RS/base.controller.js index d94d85e1..ce69d448 100644 --- a/api/dicom-web/controller/QIDO-RS/base.controller.js +++ b/api/dicom-web/controller/QIDO-RS/base.controller.js @@ -1,6 +1,6 @@ const { Controller } = require("@root/api/controller.class"); const { ApiLogger } = require("@root/utils/logs/api-logger"); -const { QidoRsService } = require("./service/QIDO-RS.service"); +const { QidoRsService } = require("@qido-rs-service"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); class BaseQueryController extends Controller { diff --git a/api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js b/api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js index 38f02d49..2d269133 100644 --- a/api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js +++ b/api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js @@ -9,6 +9,7 @@ const { AuditManager } = require("@models/DICOM/audit/auditManager"); const { EventType } = require("@models/DICOM/audit/eventType"); const { EventOutcomeIndicator } = require("@models/DICOM/audit/auditUtils"); const { QueryPatientDicomJsonFactory, QueryStudyDicomJsonFactory, QuerySeriesDicomJsonFactory, QueryInstanceDicomJsonFactory } = require("@query-dicom-json-factory"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); const HierarchyQueryDicomJsonFactory = Object.freeze({ patient: QueryPatientDicomJsonFactory, @@ -69,7 +70,7 @@ class QidoRsService { if (!query[queryKey]) delete query[queryKey]; } - this.query = convertAllQueryToDICOMTag(query); + this.query = convertAllQueryToDicomTag(query); } async getAndResponseDicomJson() { @@ -134,44 +135,4 @@ class QidoRsService { } -/** - * Convert All of name(tags, keyword) of queries to tags number - * @param {Object} iParam The request query. - * @returns - */ -function convertAllQueryToDICOMTag(iParam) { - let keys = Object.keys(iParam); - let newQS = {}; - for (let i = 0; i < keys.length; i++) { - let keyName = keys[i]; - let keyNameSplit = keyName.split("."); - let newKeyNames = []; - for (let x = 0; x < keyNameSplit.length; x++) { - if (dictionary.keyword[keyNameSplit[x]]) { - newKeyNames.push(dictionary.keyword[keyNameSplit[x]]); - } else if (dictionary.tag[keyNameSplit[x]]) { - newKeyNames.push(keyNameSplit[x]); - } - } - let retKeyName; - if (newKeyNames.length === 0) { - throw new DicomWebServiceError( - DicomWebStatusCodes.InvalidArgumentValue, - `Invalid request query: ${keyNameSplit}`, - 400 - ); - } else if (newKeyNames.length >= 2) { - retKeyName = newKeyNames.map(v => v + ".Value").join("."); - } else { - newKeyNames.push("Value"); - retKeyName = newKeyNames.join("."); - } - - newQS[retKeyName] = iParam[keyName]; - } - return newQS; -} -//#endregion - module.exports.QidoRsService = QidoRsService; -module.exports.convertAllQueryToDICOMTag = convertAllQueryToDICOMTag; diff --git a/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js b/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js index b123280c..3a020c86 100644 --- a/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js +++ b/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js @@ -204,6 +204,7 @@ class QueryInstanceDicomJsonFactory extends QueryDicomJsonFactory { } } +module.exports.QueryDicomJsonFactory = QueryDicomJsonFactory; module.exports.QueryPatientDicomJsonFactory = QueryPatientDicomJsonFactory; module.exports.QueryStudyDicomJsonFactory = QueryStudyDicomJsonFactory; module.exports.QuerySeriesDicomJsonFactory = QuerySeriesDicomJsonFactory; diff --git a/api/dicom-web/controller/UPS-RS/cancel.js b/api/dicom-web/controller/UPS-RS/cancel.js index d8a6bbc4..a0da82f9 100644 --- a/api/dicom-web/controller/UPS-RS/cancel.js +++ b/api/dicom-web/controller/UPS-RS/cancel.js @@ -6,7 +6,7 @@ const { CancelWorkItemService -} = require("./service/cancel.service"); +} = require("@api/dicom-web/controller/UPS-RS/service/cancel.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/UPS-RS/change-workItem-state.js b/api/dicom-web/controller/UPS-RS/change-workItem-state.js index 05593e71..ce353d7f 100644 --- a/api/dicom-web/controller/UPS-RS/change-workItem-state.js +++ b/api/dicom-web/controller/UPS-RS/change-workItem-state.js @@ -1,6 +1,6 @@ const { ChangeWorkItemStateService -} = require("./service/change-workItem-state.service"); +} = require("@ups-service/change-workItem-state.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/UPS-RS/create-workItems.js b/api/dicom-web/controller/UPS-RS/create-workItems.js index e99a3f3c..78269bcd 100644 --- a/api/dicom-web/controller/UPS-RS/create-workItems.js +++ b/api/dicom-web/controller/UPS-RS/create-workItems.js @@ -1,7 +1,7 @@ const _ = require("lodash"); const { CreateWorkItemService -} = require("./service/create-workItem.service"); +} = require("@ups-service/create-workItem.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/UPS-RS/get-workItem.js b/api/dicom-web/controller/UPS-RS/get-workItem.js index 17e66fde..133e0c6f 100644 --- a/api/dicom-web/controller/UPS-RS/get-workItem.js +++ b/api/dicom-web/controller/UPS-RS/get-workItem.js @@ -1,7 +1,7 @@ const _ = require("lodash"); const { GetWorkItemService -} = require("./service/get-workItem.service"); +} = require("@ups-service/get-workItem.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); diff --git a/api/dicom-web/controller/UPS-RS/service/base-workItem.service.js b/api/dicom-web/controller/UPS-RS/service/base-workItem.service.js index ec356ad5..7f03a3f2 100644 --- a/api/dicom-web/controller/UPS-RS/service/base-workItem.service.js +++ b/api/dicom-web/controller/UPS-RS/service/base-workItem.service.js @@ -6,7 +6,7 @@ const { SUBSCRIPTION_STATE } = require("@models/DICOM/ups"); const { convertRequestQueryToMongoQuery } = require("@root/api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory"); const globalSubscriptionModel = require("@models/mongodb/models/upsGlobalSubscription"); const subscriptionModel = require("@models/mongodb/models/upsSubscription"); -const workItemModel = require("@dbModels/workItems"); +const { WorkItemModel } = require("@dbModels/workitems.model"); const { dictionary } = require("@models/DICOM/dicom-tags-dic"); const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); class BaseWorkItemService { @@ -115,7 +115,7 @@ class BaseWorkItemService { $match.$and.push({ upsInstanceUID: workItem.dicomJson.upsInstanceUID }); - let count = await workItemModel.countDocuments({ + let count = await WorkItemModel.countDocuments({ ...$match }); if (count > 0) @@ -167,7 +167,7 @@ class BaseWorkItemService { * @returns */ async findOneWorkItem(upsInstanceUID, toObject=false) { - let workItem = await workItemModel.findOne({ + let workItem = await WorkItemModel.findOne({ upsInstanceUID: upsInstanceUID }); diff --git a/api/dicom-web/controller/UPS-RS/service/cancel.service.js b/api/dicom-web/controller/UPS-RS/service/cancel.service.js index 08122b32..d5384075 100644 --- a/api/dicom-web/controller/UPS-RS/service/cancel.service.js +++ b/api/dicom-web/controller/UPS-RS/service/cancel.service.js @@ -1,12 +1,10 @@ const _ = require("lodash"); -const workItemModel = require("@models/mongodb/models/workItems"); const { DicomJsonModel, BaseDicomJson } = require("@dicom-json-model"); -const globalSubscriptionModel = require("@models/mongodb/models/upsGlobalSubscription"); const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); -const { BaseWorkItemService } = require("./base-workItem.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); const { dictionary } = require("@models/DICOM/dicom-tags-dic"); const { UPS_EVENT_TYPE } = require("./workItem-event"); const { raccoonConfig } = require("@root/config-class"); @@ -25,9 +23,12 @@ class CancelWorkItemService extends BaseWorkItemService { this.requestWorkItem = /** @type {Object[]} */(this.request.body).pop(); } - async cancel() { - + async initWorkItem() { this.workItem = await this.findOneWorkItem(this.upsInstanceUID); + } + + async cancel() { + await this.initWorkItem(); let procedureStepState = this.workItem.getString(dictionary.keyword.ProcedureStepState); if (procedureStepState === "IN PROGRESS") { 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 index 44e7f6ca..f3536065 100644 --- 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 @@ -2,12 +2,12 @@ const _ = require("lodash"); const moment = require("moment"); const { DicomJsonModel } = require("@dicom-json-model"); const { DicomCode } = require("@models/DICOM/code"); -const workItemModel = require("@models/mongodb/models/workItems"); +const { WorkItemModel } = require("@models/mongodb/models/workitems.model"); const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); -const { BaseWorkItemService } = require("./base-workItem.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); const { UPS_EVENT_TYPE } = require("./workItem-event"); class ChangeWorkItemStateService extends BaseWorkItemService { @@ -51,7 +51,7 @@ class ChangeWorkItemStateService extends BaseWorkItemService { this.completeChange(); } - let updatedWorkItem = await workItemModel.findOneAndUpdate({ + let updatedWorkItem = await WorkItemModel.findOneAndUpdate({ upsInstanceUID: this.request.params.workItem }, { ...this.requestState.dicomJson 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 index 186ea403..9553539c 100644 --- a/api/dicom-web/controller/UPS-RS/service/create-workItem.service.js +++ b/api/dicom-web/controller/UPS-RS/service/create-workItem.service.js @@ -1,5 +1,5 @@ const _ = require("lodash"); -const workItemModel = require("@models/mongodb/models/workItems"); +const workItemModel = require("@models/mongodb/models/workitems.model"); const { PatientModel } = require("@dbModels/patient.model"); const { UIDUtils } = require("@dcm4che/util/UIDUtils"); const { @@ -7,8 +7,8 @@ const { DicomWebStatusCodes } = require("@error/dicom-web-service"); const { DicomJsonModel } = require("@dicom-json-model"); -const { BaseWorkItemService } = require("./base-workItem.service"); -const { SubscribeService } = require("./subscribe.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); +const { SubscribeService } = require("@ups-service/subscribe.service"); const { UPS_EVENT_TYPE } = require("./workItem-event"); const { dictionary } = require("@models/DICOM/dicom-tags-dic"); @@ -24,6 +24,20 @@ class CreateWorkItemService extends BaseWorkItemService { let uid = _.get(this.request, "query.workitem", await UIDUtils.createUID() ); + await this.dataAdjustBeforeCreatingUps(uid); + await this.validateWorkItem(uid); + + let patientId = this.requestWorkItem.getString("00100020"); + let patient = await PatientModel.findOneOrCreatePatient(patientId, this.requestWorkItem.dicomJson); + let workItem = new workItemModel(this.requestWorkItem.dicomJson); + let savedWorkItem = await workItem.save(); + + this.triggerCreateEvent(savedWorkItem); + + return workItem; + } + + async dataAdjustBeforeCreatingUps(uid) { _.set(this.requestWorkItem.dicomJson, "upsInstanceUID", uid); _.set(this.requestWorkItem.dicomJson, "00080018", { vr: "UI", @@ -41,6 +55,11 @@ class CreateWorkItemService extends BaseWorkItemService { }); } + let patientId = this.requestWorkItem.getString("00100020"); + _.set(this.requestWorkItem.dicomJson, "patientID", patientId); + } + + async validateWorkItem(uid) { if (this.requestWorkItem.getString("00741000") !== "SCHEDULED") { throw new DicomWebServiceError( DicomWebStatusCodes.UPSNotScheduled, @@ -49,10 +68,6 @@ class CreateWorkItemService extends BaseWorkItemService { ); } - let patient = await this.findOneOrCreatePatient(); - - let workItem = new workItemModel(this.requestWorkItem.dicomJson); - if (await this.isUpsExist(uid)) { throw new DicomWebServiceError( DicomWebStatusCodes.DuplicateSOPinstance, @@ -60,12 +75,12 @@ class CreateWorkItemService extends BaseWorkItemService { 400 ); } - let savedWorkItem = await workItem.save(); - + } - let workItemDicomJson = new DicomJsonModel(savedWorkItem); + async triggerCreateEvent(workItem) { + let workItemDicomJson = new DicomJsonModel(workItem); let hitGlobalSubscriptions = await this.getHitGlobalSubscriptions(workItemDicomJson); - for(let hitGlobalSubscription of hitGlobalSubscriptions) { + for (let hitGlobalSubscription of hitGlobalSubscriptions) { let subscribeService = new SubscribeService(this.request, this.response); subscribeService.upsInstanceUID = workItemDicomJson.dicomJson.upsInstanceUID; subscribeService.deletionLock = hitGlobalSubscription.isDeletionLock; @@ -74,42 +89,22 @@ class CreateWorkItemService extends BaseWorkItemService { } let hitSubscriptions = await this.getHitSubscriptions(workItemDicomJson); - + if (hitSubscriptions) { let hitSubscriptionAeTitleArray = hitSubscriptions.map(sub => sub.aeTitle); - this.addUpsEvent(UPS_EVENT_TYPE.StateReport, workItemDicomJson.dicomJson.upsInstanceUID, this.stateReportOf(workItemDicomJson), hitSubscriptionAeTitleArray); + this.addUpsEvent(UPS_EVENT_TYPE.StateReport, workItemDicomJson.dicomJson.upsInstanceUID, this.stateReportOf(workItem.toDicomJsonModel()), hitSubscriptionAeTitleArray); let assignedEventInformationArray = await this.getAssignedEventInformationArray( workItemDicomJson, _.get(workItemDicomJson.dicomJson, `${dictionary.keyword.ScheduledStationNameCodeSequence}`, false), _.get(workItemDicomJson.dicomJson, `${dictionary.keyword.ScheduledHumanPerformersSequence}`, false) ); - - for(let assignedEventInfo of assignedEventInformationArray) { + + for (let assignedEventInfo of assignedEventInformationArray) { this.addUpsEvent(UPS_EVENT_TYPE.Assigned, workItemDicomJson.dicomJson.upsInstanceUID, assignedEventInfo, hitSubscriptionAeTitleArray); } } - - this.triggerUpsEvents(); - - 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; + this.triggerUpsEvents(); } async isUpsExist(uid) { 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 index 9b9f88d5..3906c345 100644 --- a/api/dicom-web/controller/UPS-RS/service/get-workItem.service.js +++ b/api/dicom-web/controller/UPS-RS/service/get-workItem.service.js @@ -1,9 +1,9 @@ const _ = require("lodash"); -const workItemsModel = require("@models/mongodb/models/workItems"); +const workItemsModel = require("@models/mongodb/models/workitems.model"); const { convertRequestQueryToMongoQuery } = require("../../QIDO-RS/service/query-dicom-json-factory"); -const { convertAllQueryToDICOMTag } = require("../../QIDO-RS/service/QIDO-RS.service"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); class GetWorkItemService { constructor(req, res) { @@ -60,7 +60,7 @@ class GetWorkItemService { if (!query[queryKey]) delete query[queryKey]; } - this.query = convertAllQueryToDICOMTag(query); + this.query = convertAllQueryToDicomTag(query); } } diff --git a/api/dicom-web/controller/UPS-RS/service/subscribe.service.js b/api/dicom-web/controller/UPS-RS/service/subscribe.service.js index a71eae9b..41195066 100644 --- a/api/dicom-web/controller/UPS-RS/service/subscribe.service.js +++ b/api/dicom-web/controller/UPS-RS/service/subscribe.service.js @@ -1,7 +1,7 @@ const _ = require("lodash"); const { DicomJsonModel } = require("@dicom-json-model"); const { DicomCode } = require("@models/DICOM/code"); -const workItemModel = require("@models/mongodb/models/workItems"); +const workItemModel = require("@models/mongodb/models/workitems.model"); const subscriptionModel = require("@models/mongodb/models/upsSubscription"); const globalSubscriptionModel = require("@models/mongodb/models/upsGlobalSubscription"); const { @@ -9,9 +9,9 @@ const { DicomWebStatusCodes } = require("@error/dicom-web-service"); const { SUBSCRIPTION_STATE, SUBSCRIPTION_FIXED_UIDS } = require("@models/DICOM/ups"); -const { BaseWorkItemService } = require("./base-workItem.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); const { UPS_EVENT_TYPE } = require("./workItem-event"); -const { convertAllQueryToDICOMTag } = require("../../QIDO-RS/service/QIDO-RS.service"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); class SubscribeService extends BaseWorkItemService { @@ -34,7 +34,7 @@ class SubscribeService extends BaseWorkItemService { if (this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.GlobalUID || this.upsInstanceUID === SUBSCRIPTION_FIXED_UIDS.FilteredGlobalUID) { - this.query = convertAllQueryToDICOMTag(this.request.query); + this.query = convertAllQueryToDicomTag(this.request.query); await this.createOrUpdateGlobalSubscription(); } else { let workItem = await this.findOneWorkItem(this.upsInstanceUID); diff --git a/api/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js b/api/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js index 3a3c47bc..e2bf8e60 100644 --- a/api/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js +++ b/api/dicom-web/controller/UPS-RS/service/suspend-subscription.service.js @@ -4,7 +4,7 @@ const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); -const { BaseWorkItemService } = require("./base-workItem.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); class SuspendSubscribeService extends BaseWorkItemService { diff --git a/api/dicom-web/controller/UPS-RS/service/unsubscribe.service.js b/api/dicom-web/controller/UPS-RS/service/unsubscribe.service.js index 62fac381..c48ca2c2 100644 --- a/api/dicom-web/controller/UPS-RS/service/unsubscribe.service.js +++ b/api/dicom-web/controller/UPS-RS/service/unsubscribe.service.js @@ -1,7 +1,7 @@ const _ = require("lodash"); const { DicomJsonModel } = require("@dicom-json-model"); const { DicomCode } = require("@models/DICOM/code"); -const workItemModel = require("@models/mongodb/models/workItems"); +const workItemModel = require("@models/mongodb/models/workitems.model"); const subscriptionModel = require("@models/mongodb/models/upsSubscription"); const globalSubscriptionModel = require("@models/mongodb/models/upsGlobalSubscription"); const { @@ -9,8 +9,7 @@ const { DicomWebStatusCodes } = require("@error/dicom-web-service"); const { SUBSCRIPTION_STATE, SUBSCRIPTION_FIXED_UIDS } = require("@models/DICOM/ups"); -const { BaseWorkItemService } = require("./base-workItem.service"); -const { convertAllQueryToDICOMTag } = require("../../QIDO-RS/service/QIDO-RS.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); class UnSubscribeService extends BaseWorkItemService { diff --git a/api/dicom-web/controller/UPS-RS/service/update-workItem.service.js b/api/dicom-web/controller/UPS-RS/service/update-workItem.service.js index b1b33d8e..e028d3b2 100644 --- a/api/dicom-web/controller/UPS-RS/service/update-workItem.service.js +++ b/api/dicom-web/controller/UPS-RS/service/update-workItem.service.js @@ -1,5 +1,5 @@ const _ = require("lodash"); -const workItemModel = require("@models/mongodb/models/workItems"); +const workItemModel = require("@models/mongodb/models/workitems.model"); const { PatientModel } = require("@dbModels/patient.model"); const { UIDUtils } = require("@dcm4che/util/UIDUtils"); const { @@ -7,28 +7,29 @@ const { DicomWebStatusCodes } = require("@error/dicom-web-service"); const { DicomJsonModel } = require("@dicom-json-model"); -const { BaseWorkItemService } = require("./base-workItem.service"); +const { BaseWorkItemService } = require("@ups-service/base-workItem.service"); const { dictionary } = require("@models/DICOM/dicom-tags-dic"); const { UPS_EVENT_TYPE } = require("./workItem-event"); -const notAllowedAttributes = [ - "00080016", - "00080018", - "00100010", - "00100020", - "00100030", - "00100040", - "00380010", - "00380014", - "00081080", - "00081084", - "0040A370", - "00741224", - "00741000" -]; + class UpdateWorkItemService extends BaseWorkItemService { + static notAllowedAttributes = Object.freeze([ + "00080016", + "00080018", + "00100010", + "00100020", + "00100030", + "00100040", + "00380010", + "00380014", + "00081080", + "00081084", + "0040A370", + "00741224", + "00741000" + ]); /** * * @param {import('express').Request} req @@ -57,10 +58,14 @@ class UpdateWorkItemService extends BaseWorkItemService { new: true }); - let updateWorkItemDicomJson = new DicomJsonModel(updatedWorkItem); + this.triggerUpdateWorkItemEvent(updatedWorkItem); + } + + async triggerUpdateWorkItemEvent(workItem) { + let updateWorkItemDicomJson = new DicomJsonModel(workItem); let hitSubscriptions = await this.getHitSubscriptions(updateWorkItemDicomJson); if (hitSubscriptions.length === 0) { - return updatedWorkItem; + return workItem; } let hitSubscriptionAeTitleArray = hitSubscriptions.map(sub => sub.aeTitle); @@ -162,8 +167,8 @@ class UpdateWorkItemService extends BaseWorkItemService { * remove not allowed updating attribute in request work item */ adjustRequestWorkItem() { - for (let i = 0; i < notAllowedAttributes.length; i++) { - let notAllowedAttr = notAllowedAttributes[i]; + for (let i = 0; i < UpdateWorkItemService.notAllowedAttributes.length; i++) { + let notAllowedAttr = UpdateWorkItemService.notAllowedAttributes[i]; _.unset(this.requestWorkItem.dicomJson, notAllowedAttr); } } diff --git a/api/dicom-web/controller/UPS-RS/subscribe.js b/api/dicom-web/controller/UPS-RS/subscribe.js index 4ad936c1..eaef8621 100644 --- a/api/dicom-web/controller/UPS-RS/subscribe.js +++ b/api/dicom-web/controller/UPS-RS/subscribe.js @@ -1,6 +1,6 @@ const { SubscribeService -} = require("./service/subscribe.service"); +} = require("@ups-service/subscribe.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/UPS-RS/suspend-subscription.js b/api/dicom-web/controller/UPS-RS/suspend-subscription.js index 8161f525..a3128cdf 100644 --- a/api/dicom-web/controller/UPS-RS/suspend-subscription.js +++ b/api/dicom-web/controller/UPS-RS/suspend-subscription.js @@ -6,7 +6,7 @@ const { SuspendSubscribeService -} = require("./service/suspend-subscription.service"); +} = require("@ups-service/suspend-subscription.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/UPS-RS/unsubscribe.js b/api/dicom-web/controller/UPS-RS/unsubscribe.js index e2cf36be..f6316372 100644 --- a/api/dicom-web/controller/UPS-RS/unsubscribe.js +++ b/api/dicom-web/controller/UPS-RS/unsubscribe.js @@ -1,6 +1,6 @@ const { UnSubscribeService -} = require("./service/unsubscribe.service"); +} = require("@ups-service/unsubscribe.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/UPS-RS/update-workItem.js b/api/dicom-web/controller/UPS-RS/update-workItem.js index f1c77d2b..267ccfbb 100644 --- a/api/dicom-web/controller/UPS-RS/update-workItem.js +++ b/api/dicom-web/controller/UPS-RS/update-workItem.js @@ -1,6 +1,6 @@ const { UpdateWorkItemService -} = require("./service/update-workItem.service"); +} = require("@ups-service/update-workItem.service"); const { ApiLogger } = require("../../../../utils/logs/api-logger"); const { Controller } = require("../../../controller.class"); const { DicomWebServiceError } = require("@error/dicom-web-service"); diff --git a/api/dicom-web/controller/WADO-RS/base.controller.js b/api/dicom-web/controller/WADO-RS/base.controller.js index 7636a755..0cb72f13 100644 --- a/api/dicom-web/controller/WADO-RS/base.controller.js +++ b/api/dicom-web/controller/WADO-RS/base.controller.js @@ -3,7 +3,15 @@ const { RetrieveAuditService } = require("./service/retrieveAudit.service"); const { EventOutcomeIndicator } = require("@models/DICOM/audit/auditUtils"); const { WADOZip } = require("./service/WADOZip"); const { ApiLogger } = require("@root/utils/logs/api-logger"); -const { sendNotSupportedMediaType, getAcceptType, supportInstanceMultipartType, ImageMultipartWriter, InstanceImagePathFactory, multipartContentTypeWriter, StudyImagePathFactory, SeriesImagePathFactory } = require("./service/WADO-RS.service"); +const { + sendNotSupportedMediaType, + getAcceptType, + supportInstanceMultipartType, + ImageMultipartWriter, + InstanceImagePathFactory, + multipartContentTypeWriter, + StudyImagePathFactory, + SeriesImagePathFactory } = require("@wado-rs-service"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); class BaseRetrieveController extends Controller { @@ -115,7 +123,7 @@ class BaseMultipartRelatedResponseHandler { let imageMultipartWriter = new ImageMultipartWriter( this.request, this.response, - this.imagePathFactoryType , + this.imagePathFactoryType, multipartContentTypeWriter[type] ); diff --git a/api/dicom-web/controller/WADO-RS/bulkdata/base.controller.js b/api/dicom-web/controller/WADO-RS/bulkdata/base.controller.js index cfb4dbdc..3d1e56d1 100644 --- a/api/dicom-web/controller/WADO-RS/bulkdata/base.controller.js +++ b/api/dicom-web/controller/WADO-RS/bulkdata/base.controller.js @@ -1,7 +1,7 @@ const { Controller } = require("@root/api/controller.class"); -const { StudyBulkDataFactory, BulkDataService } = require("./service/bulkdata"); +const { StudyBulkDataFactory, BulkDataService } = require("@bulkdata-service"); const { ApiLogger } = require("@root/utils/logs/api-logger"); -const { StudyImagePathFactory } = require("../service/WADO-RS.service"); +const { StudyImagePathFactory } = require("@wado-rs-service"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); class BaseBulkDataController extends Controller { @@ -22,7 +22,6 @@ class BaseBulkDataController extends Controller { this.logAction(); try { - this.logAction(); let bulkData = await this.bulkDataService.getBulkData(); if (Array.isArray(bulkData)) { diff --git a/api/dicom-web/controller/WADO-RS/bulkdata/bulkdata.js b/api/dicom-web/controller/WADO-RS/bulkdata/bulkdata.js index 419c5297..7da20b55 100644 --- a/api/dicom-web/controller/WADO-RS/bulkdata/bulkdata.js +++ b/api/dicom-web/controller/WADO-RS/bulkdata/bulkdata.js @@ -1,11 +1,5 @@ -const mongoose = require("mongoose"); -const { Controller } = require("../../../../controller.class"); -const { ApiLogger } = require("../../../../../utils/logs/api-logger"); -const { BulkDataService, SpecificBulkDataFactory } = require("./service/bulkdata"); -const { getInternalServerErrorMessage } = require("../../../../../utils/errorResponse/errorResponseMessage"); +const { SpecificBulkDataFactory } = require("@bulkdata-service"); const { BaseBulkDataController } = require("./base.controller"); -const { InstanceImagePathFactory } = require("../service/WADO-RS.service"); -const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); class BulkDataController extends BaseBulkDataController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/bulkdata/instance.js b/api/dicom-web/controller/WADO-RS/bulkdata/instance.js index b164cd33..eaa5e16a 100644 --- a/api/dicom-web/controller/WADO-RS/bulkdata/instance.js +++ b/api/dicom-web/controller/WADO-RS/bulkdata/instance.js @@ -1,9 +1,6 @@ -const { ApiLogger } = require("../../../../../utils/logs/api-logger"); -const { BulkDataService, InstanceBulkDataFactory } = require("./service/bulkdata"); -const { getInternalServerErrorMessage } = require("../../../../../utils/errorResponse/errorResponseMessage"); -const { InstanceModel } = require("@dbModels/instance.model"); +const { InstanceBulkDataFactory } = require("@bulkdata-service"); const { BaseBulkDataController } = require("./base.controller"); -const { InstanceImagePathFactory } = require("../service/WADO-RS.service"); +const { InstanceImagePathFactory } = require("@wado-rs-service"); class InstanceBulkDataController extends BaseBulkDataController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/bulkdata/series.js b/api/dicom-web/controller/WADO-RS/bulkdata/series.js index ea2d816b..e66164be 100644 --- a/api/dicom-web/controller/WADO-RS/bulkdata/series.js +++ b/api/dicom-web/controller/WADO-RS/bulkdata/series.js @@ -1,6 +1,6 @@ -const { SeriesBulkDataFactory } = require("./service/bulkdata"); +const { SeriesBulkDataFactory } = require("@bulkdata-service"); const { BaseBulkDataController } = require("./base.controller"); -const { SeriesImagePathFactory } = require("../service/WADO-RS.service"); +const { SeriesImagePathFactory } = require("@wado-rs-service"); class SeriesBulkDataController extends BaseBulkDataController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/bulkdata/study.js b/api/dicom-web/controller/WADO-RS/bulkdata/study.js index 9b499e59..01f00204 100644 --- a/api/dicom-web/controller/WADO-RS/bulkdata/study.js +++ b/api/dicom-web/controller/WADO-RS/bulkdata/study.js @@ -1,6 +1,6 @@ -const { StudyBulkDataFactory } = require("./service/bulkdata"); +const { StudyBulkDataFactory } = require("@bulkdata-service"); const { BaseBulkDataController } = require("./base.controller"); -const { StudyImagePathFactory } = require("../service/WADO-RS.service"); +const { StudyImagePathFactory } = require("@wado-rs-service"); class StudyBulkDataController extends BaseBulkDataController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/deletion/base.controller.js b/api/dicom-web/controller/WADO-RS/deletion/base.controller.js index 5d455f6f..6243a569 100644 --- a/api/dicom-web/controller/WADO-RS/deletion/base.controller.js +++ b/api/dicom-web/controller/WADO-RS/deletion/base.controller.js @@ -1,8 +1,6 @@ const { Controller } = require("@root/api/controller.class"); const { ApiLogger } = require("@root/utils/logs/api-logger"); -const { DeleteService } = require("./service/delete"); -const { NotFoundInstanceError } = require("@error/dicom-instance"); -const { getNotFoundErrorMessage, getInternalServerErrorMessage } = require("@root/utils/errorResponse/errorResponseMessage"); +const { DeleteService } = require("@delete-service"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); class BaseDeleteController extends Controller { diff --git a/api/dicom-web/controller/WADO-RS/metadata/base.controller.js b/api/dicom-web/controller/WADO-RS/metadata/base.controller.js index 9c91e8a8..c5cf7f9f 100644 --- a/api/dicom-web/controller/WADO-RS/metadata/base.controller.js +++ b/api/dicom-web/controller/WADO-RS/metadata/base.controller.js @@ -1,6 +1,6 @@ const { Controller } = require("@root/api/controller.class"); const { ApiLogger } = require("@root/utils/logs/api-logger"); -const { StudyImagePathFactory } = require("../service/WADO-RS.service"); +const { StudyImagePathFactory } = require("@wado-rs-service"); const { MetadataService } = require("../service/metadata.service"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); diff --git a/api/dicom-web/controller/WADO-RS/metadata/retrieveInstanceMetadata.js b/api/dicom-web/controller/WADO-RS/metadata/retrieveInstanceMetadata.js index e5945b4a..61f0b21e 100644 --- a/api/dicom-web/controller/WADO-RS/metadata/retrieveInstanceMetadata.js +++ b/api/dicom-web/controller/WADO-RS/metadata/retrieveInstanceMetadata.js @@ -1,5 +1,5 @@ const { BaseRetrieveMetadataController } = require("./base.controller"); -const { InstanceImagePathFactory } = require("../service/WADO-RS.service"); +const { InstanceImagePathFactory } = require("@wado-rs-service"); class RetrieveInstanceMetadataController extends BaseRetrieveMetadataController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/metadata/retrieveSeriesMetadata.js b/api/dicom-web/controller/WADO-RS/metadata/retrieveSeriesMetadata.js index e79c2465..b44a26c5 100644 --- a/api/dicom-web/controller/WADO-RS/metadata/retrieveSeriesMetadata.js +++ b/api/dicom-web/controller/WADO-RS/metadata/retrieveSeriesMetadata.js @@ -1,13 +1,5 @@ -const mongoose = require("mongoose"); -const _ = require("lodash"); -const fs = require("fs"); -const path = require("path"); -const fileExist = require("../../../../../utils/file/fileExist"); -const errorResponse = require("../../../../../utils/errorResponse/errorResponseMessage"); -const { Controller } = require("../../../../controller.class"); -const { ApiLogger } = require("../../../../../utils/logs/api-logger"); const { BaseRetrieveMetadataController } = require("./base.controller"); -const { SeriesImagePathFactory } = require("../service/WADO-RS.service"); +const { SeriesImagePathFactory } = require("@wado-rs-service"); class RetrieveSeriesMetadataController extends BaseRetrieveMetadataController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/metadata/retrieveStudyMetadata.js b/api/dicom-web/controller/WADO-RS/metadata/retrieveStudyMetadata.js index 6c171136..d8658ec9 100644 --- a/api/dicom-web/controller/WADO-RS/metadata/retrieveStudyMetadata.js +++ b/api/dicom-web/controller/WADO-RS/metadata/retrieveStudyMetadata.js @@ -1,4 +1,4 @@ -const { StudyImagePathFactory } = require("../service/WADO-RS.service"); +const { StudyImagePathFactory } = require("@wado-rs-service"); const { BaseRetrieveMetadataController } = require("./base.controller"); class RetrieveStudyMetadataController extends BaseRetrieveMetadataController { diff --git a/api/dicom-web/controller/WADO-RS/rendered/base.controller.js b/api/dicom-web/controller/WADO-RS/rendered/base.controller.js index 5594de04..71f9e6af 100644 --- a/api/dicom-web/controller/WADO-RS/rendered/base.controller.js +++ b/api/dicom-web/controller/WADO-RS/rendered/base.controller.js @@ -1,10 +1,10 @@ const _ = require("lodash"); -const renderedService = require("../service/rendered.service"); +const renderedService = require("@rendered-service"); const { - StudyImagePathFactory, SeriesImagePathFactory, InstanceImagePathFactory -} = require("../service/WADO-RS.service"); -const errorResponse = require("../../../../../utils/errorResponse/errorResponseMessage"); -const { ApiLogger } = require("../../../../../utils/logs/api-logger"); + StudyImagePathFactory +} = require("@wado-rs-service"); +const errorResponse = require("@root/utils/errorResponse/errorResponseMessage"); +const { ApiLogger } = require("@root/utils/logs/api-logger"); const { Controller } = require("../../../../controller.class"); const { ApiErrorArrayHandler } = require("@error/api-errors.handler"); diff --git a/api/dicom-web/controller/WADO-RS/rendered/instanceFrames.js b/api/dicom-web/controller/WADO-RS/rendered/instanceFrames.js index d40604ad..eee25b09 100644 --- a/api/dicom-web/controller/WADO-RS/rendered/instanceFrames.js +++ b/api/dicom-web/controller/WADO-RS/rendered/instanceFrames.js @@ -1,7 +1,7 @@ const _ = require("lodash"); -const { InstanceImagePathFactory } = require("../service/WADO-RS.service"); +const { InstanceImagePathFactory } = require("@wado-rs-service"); const { BaseRetrieveRenderedController } = require("./base.controller"); -const { InstanceFramesListWriter } = require("../service/rendered.service"); +const { InstanceFramesListWriter } = require("@rendered-service"); class RetrieveRenderedInstanceFramesController extends BaseRetrieveRenderedController { constructor(req, res) { diff --git a/api/dicom-web/controller/WADO-RS/rendered/instances.js b/api/dicom-web/controller/WADO-RS/rendered/instances.js index 7405906e..46324f05 100644 --- a/api/dicom-web/controller/WADO-RS/rendered/instances.js +++ b/api/dicom-web/controller/WADO-RS/rendered/instances.js @@ -1,5 +1,5 @@ -const { InstanceImagePathFactory } = require("../service/WADO-RS.service"); -const { InstanceFramesWriter } = require("../service/rendered.service"); +const { InstanceImagePathFactory } = require("@wado-rs-service"); +const { InstanceFramesWriter } = require("@rendered-service"); const { BaseRetrieveRenderedController } = require("./base.controller"); class RetrieveRenderedInstancesController extends BaseRetrieveRenderedController { diff --git a/api/dicom-web/controller/WADO-RS/rendered/series.js b/api/dicom-web/controller/WADO-RS/rendered/series.js index 404c5ef6..92a1e1d2 100644 --- a/api/dicom-web/controller/WADO-RS/rendered/series.js +++ b/api/dicom-web/controller/WADO-RS/rendered/series.js @@ -1,5 +1,5 @@ -const { SeriesImagePathFactory } = require("../service/WADO-RS.service"); -const { SeriesFramesWriter } = require("../service/rendered.service"); +const { SeriesImagePathFactory } = require("@wado-rs-service"); +const { SeriesFramesWriter } = require("@rendered-service"); const { BaseRetrieveRenderedController } = require("./base.controller"); class RetrieveRenderedSeriesController extends BaseRetrieveRenderedController { diff --git a/api/dicom-web/controller/WADO-RS/rendered/study.js b/api/dicom-web/controller/WADO-RS/rendered/study.js index 004b19da..84bc6f36 100644 --- a/api/dicom-web/controller/WADO-RS/rendered/study.js +++ b/api/dicom-web/controller/WADO-RS/rendered/study.js @@ -1,5 +1,5 @@ -const { StudyImagePathFactory } = require("../service/WADO-RS.service"); -const { StudyFramesWriter } = require("../service/rendered.service"); +const { StudyImagePathFactory } = require("@wado-rs-service"); +const { StudyFramesWriter } = require("@rendered-service"); const { BaseRetrieveRenderedController } = require("./base.controller"); class RetrieveRenderedStudyController extends BaseRetrieveRenderedController { diff --git a/api/dicom-web/controller/WADO-RS/service/WADOZip.js b/api/dicom-web/controller/WADO-RS/service/WADOZip.js index ab31df6f..afadc23b 100644 --- a/api/dicom-web/controller/WADO-RS/service/WADOZip.js +++ b/api/dicom-web/controller/WADO-RS/service/WADOZip.js @@ -1,7 +1,7 @@ -const mongoose = require("mongoose"); const archiver = require("archiver"); -const wadoService = require("./WADO-RS.service"); const path = require("path"); +const { StudyModel } = require("@dbModels/study.model"); +const { SeriesModel } = require("@dbModels/series.model"); const { InstanceModel } = require("@dbModels/instance.model"); class WADOZip { constructor(iReq, iRes) { @@ -19,7 +19,7 @@ class WADOZip { } async getZipOfStudyDICOMFiles() { - let imagesPathList = await mongoose.model("dicomStudy").getPathGroupOfInstances(this.requestParams); + let imagesPathList = await StudyModel.getPathGroupOfInstances(this.requestParams); if (imagesPathList.length > 0) { this.setHeaders(this.studyUID); @@ -40,7 +40,7 @@ class WADOZip { } async getZipOfSeriesDICOMFiles() { - let imagesPathList = await mongoose.model("dicomSeries").getPathGroupOfInstances(this.requestParams); + let imagesPathList = await SeriesModel.getPathGroupOfInstances(this.requestParams); if (imagesPathList.length > 0) { this.setHeaders(this.seriesUID); diff --git a/api/dicom-web/controller/WADO-RS/service/thumbnail.service.js b/api/dicom-web/controller/WADO-RS/service/thumbnail.service.js index 4bed2b02..82937497 100644 --- a/api/dicom-web/controller/WADO-RS/service/thumbnail.service.js +++ b/api/dicom-web/controller/WADO-RS/service/thumbnail.service.js @@ -1,6 +1,6 @@ const { InstanceModel } = require("@dbModels/instance.model"); const errorResponse = require("../../../../../utils/errorResponse/errorResponseMessage"); -const renderedService = require("../service/rendered.service"); +const renderedService = require("@rendered-service"); const _ = require("lodash"); const { getUidsString } = require("./WADO-RS.service"); class ThumbnailService { diff --git a/api/dicom-web/controller/WADO-RS/thumbnail/base.controller.js b/api/dicom-web/controller/WADO-RS/thumbnail/base.controller.js index 1c73f9d8..e09c1fd3 100644 --- a/api/dicom-web/controller/WADO-RS/thumbnail/base.controller.js +++ b/api/dicom-web/controller/WADO-RS/thumbnail/base.controller.js @@ -1,5 +1,5 @@ const { Controller } = require("@root/api/controller.class"); -const { StudyImagePathFactory } = require("../service/WADO-RS.service"); +const { StudyImagePathFactory } = require("@wado-rs-service"); const { ThumbnailService } = require("../service/thumbnail.service"); const { ApiLogger } = require("@root/utils/logs/api-logger"); diff --git a/api/dicom-web/mwl-rs.route.js b/api/dicom-web/mwl-rs.route.js new file mode 100644 index 00000000..06e0e0dd --- /dev/null +++ b/api/dicom-web/mwl-rs.route.js @@ -0,0 +1,160 @@ +const express = require("express"); +const Joi = require("joi"); +const { validateParams, intArrayJoi, validateByJoi } = require("@root/api/validator"); +const router = express(); + + +/** + * @openapi + * /dicom-web/mwlitems: + * post: + * tags: + * - MWL-RS + * description: > + * This transaction create or update a Modality WorkList item. + * requestBody: + * content: + * application/dicom+json: + * parameters: + * responses: + * "200": + * description: The workitem create successfully + */ +router.post("/mwlitems", + validateByJoi(Joi.array().items( + Joi.object({ + "00100020": Joi.object({ + vr: Joi.string().valid("LO").required(), + Value: Joi.array().items(Joi.string()).min(1).max(1).required() + }).required(), + "00400100": Joi.object({ + vr: Joi.string().valid("SQ").required(), + Value: Joi.array().items(Joi.object()).min(1).required() + }).required() + }) + ).min(1).max(1), "body", {allowUnknown: true}), + require("./controller/MWL-RS/create-mwlItem") +); + +/** + * @openapi + * /dicom-web/mwlitems: + * get: + * tags: + * - MWL-RS + * description: > + * This transaction search Modality WorkList items. + * parameters: + * - $ref: "#/components/parameters/filter" + * responses: + * "200": + * description: Query successfully + * content: + * "application/dicom+json": + * schema: + * type: array + */ +router.get("/mwlitems",require("./controller/MWL-RS/get-mwlItem")); + +/** + * @openapi + * /dicom-web/mwlitems/count: + * get: + * tags: + * - MWL-RS + * description: > + * This transaction get Modality WorkList items count. + * parameters: + * - $ref: "#/components/parameters/filter" + * responses: + * "200": + * description: Query successfully + * content: + * "application/dicom+json": + * schema: + * properties: + * count: + * type: number + */ +router.get("/mwlitems/count",require("./controller/MWL-RS/count-mwlItem")); + +/** + * @openapi + * /dicom-web/mwlitems/{studyUID}/{spsID}: + * delete: + * tags: + * - MWL-RS + * description: > + * This transaction deletes a Modality WorkList item. + * requestBody: + * content: + * application/dicom+json: + * parameters: + * - $ref: "#/components/parameters/studyUID" + * - $ref: "#/components/parameters/spsID" + * responses: + * "204": + * description: Delete successfully + */ +router.delete("/mwlitems/:studyUID/:spsID", validateParams({ + studyUID: Joi.string().required(), + spsID: Joi.string().required() +}, "params", undefined), require("./controller/MWL-RS/delete-mwlItem")); + + +/** + * @openapi + * /dicom-web/mwlitems/{studyUID}/{spsID}/status/{spsStatus}: + * post: + * tags: + * - MWL-RS + * description: > + * This transaction create or update a Modality WorkList item. + * requestBody: + * content: + * application/dicom+json: + * parameters: + * - $ref: "#/components/parameters/studyUID" + * - $ref: "#/components/parameters/spsID" + * - $ref: "#/components/parameters/spsStatus" + * responses: + * "200": + * description: change status of mwl item successfully + */ +router.post("/mwlitems/:studyUID/:spsID/status/:status", + validateByJoi( + Joi.object({ + studyUID: Joi.string().required(), + spsID: Joi.string().required(), + status: Joi.string().valid("SCHEDULED", "ARRIVED", "READY", "STARTED", "DEPARTED", "CANCELED", "DISCONTINUED", "COMPLETED").required() + }), "params", {allowUnknown: false}), + require("./controller/MWL-RS/change-mwlItem-status") +); + +/** + * @openapi + * /dicom-web/mwlitems/status/{spsStatus}: + * post: + * tags: + * - MWL-RS + * description: > + * This transaction create or update a Modality WorkList item. + * requestBody: + * content: + * application/dicom+json: + * parameters: + * - $ref: "#/components/parameters/spsStatus" + * - $ref: "#/components/parameters/filter" + * responses: + * "200": + * description: change status of mwl items successfully + */ +router.post("/mwlitems/status/:status", + validateByJoi( + Joi.object({ + status: Joi.string().valid("SCHEDULED", "ARRIVED", "READY", "STARTED", "DEPARTED", "CANCELED", "DISCONTINUED", "COMPLETED").required() + }), "params", {allowUnknown: false}), + require("./controller/MWL-RS/change-filtered-mwlItem-status") +); + +module.exports = router; \ No newline at end of file diff --git a/api/dicom-web/service/base-query.service.js b/api/dicom-web/service/base-query.service.js new file mode 100644 index 00000000..3a023e45 --- /dev/null +++ b/api/dicom-web/service/base-query.service.js @@ -0,0 +1,100 @@ +const _ = require("lodash"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); + + +class BaseQueryService { + 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"]; + + /** + * @private + */ + this.includeFields_ = this.request.query["includefield"] || []; + + if (this.includeFields_.includes("all")) { + this.includeFields_ = ["all"]; + } + + delete this.request.query["includefield"]; + + this.initQuery_(); + } + + /** + * @private + */ + 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); + } +} + +/** + * Convert All of name(tags, keyword) of queries to tags number + * @param {Object} iParam The request query. + * @returns + */ +function convertAllQueryToDicomTag(iParam, pushSuffixValue=true) { + let keys = Object.keys(iParam); + let newQS = {}; + for (let i = 0; i < keys.length; i++) { + let keyName = keys[i]; + let keyNameSplit = keyName.split("."); + let newKeyNames = []; + for (let x = 0; x < keyNameSplit.length; x++) { + if (dictionary.keyword[keyNameSplit[x]]) { + newKeyNames.push(dictionary.keyword[keyNameSplit[x]]); + } else if (dictionary.tag[keyNameSplit[x]]) { + newKeyNames.push(keyNameSplit[x]); + } + } + let retKeyName; + if (newKeyNames.length === 0) { + throw new DicomWebServiceError( + DicomWebStatusCodes.InvalidArgumentValue, + `Invalid request query: ${keyNameSplit}`, + 400 + ); + } + + if (pushSuffixValue) { + if (newKeyNames.length >= 2) { + retKeyName = newKeyNames.map(v => v + ".Value").join("."); + } else { + newKeyNames.push("Value"); + retKeyName = newKeyNames.join("."); + } + } else { + retKeyName = newKeyNames.join("."); + } + + newQS[retKeyName] = iParam[keyName]; + } + return newQS; +} +//#endregion + +module.exports.BaseQueryService = BaseQueryService; +module.exports.convertAllQueryToDicomTag = convertAllQueryToDicomTag; \ No newline at end of file diff --git a/config-class.js b/config-class.js index 1d3d61d3..c1de7812 100644 --- a/config-class.js +++ b/config-class.js @@ -24,6 +24,20 @@ function generateUidFromGuid(iGuid) { return `2.25.${bigInteger.toString()}`; //Output the previus parsed integer as string by adding `2.25.` as prefix } +class SqlDbConfig { + constructor() { + this.host = env.get("SQL_HOST").default("127.0.0.1").asString(); + this.port = env.get("SQL_PORT").default("5432").asString(); + this.database = env.get("SQL_DB").default("raccoon").asString(); + this.dialect = env.get("SQL_TYPE").default("postgres").asString(); + this.username = env.get("SQL_USERNAME").default("postgres").asString(); + this.password = env.get("SQL_PASSWORD").default("postgres").asString(); + this.logging = env.get("SQL_LOGGING").default("false").asBool(); + this.forceSync = env.get("SQL_FORCE_SYNC").default("false").asBool(); + this.dbName = this.database; + } +} + class MongoDbConfig { constructor() { this.dbName = env.get("MONGODB_NAME").default("raccoon").asString(); @@ -42,6 +56,7 @@ class ServerConfig { this.host = env.get("SERVER_HOST").default("127.0.0.1").asString(); this.port = env.get("SERVER_PORT").default("8081").asInt(); this.secretKey = env.get("SERVER_SESSION_SECRET_KEY").asString(); + this.dbType = env.get("SERVER_DB_TYPE").default("mongodb").asEnum(["mongodb", "sql"]); } } @@ -65,19 +80,28 @@ class FhirConfig { class RaccoonConfig { constructor() { - this.mongoDbConfig = new MongoDbConfig(); this.serverConfig = new ServerConfig(); + + if (this.serverConfig.dbType === "mongodb") { + this.dbConfig = new MongoDbConfig(); + } else if (this.serverConfig.dbType === "sql") { + this.dbConfig = new SqlDbConfig(); + } + this.dicomWebConfig = new DicomWebConfig(); this.dicomDimseConfig = new DimseConfig(); this.fhirConfig = new FhirConfig(); /** @type {string} */ this.mediaStorageUID = generateUidFromGuid( - uuid.v5(this.mongoDbConfig.dbName, NAME_SPACE) + uuid.v5(this.dbConfig.dbName, NAME_SPACE) ); /** @type {string} */ - this.mediaStorageID = this.mongoDbConfig.dbName; + this.mediaStorageID = this.dbConfig.dbName; + + this.aeTitle = this.dicomWebConfig.aeTitle; + // this.aeTitle = this.dicomDimseConfig.enableDimse ? this.dicomDimseConfig.getAeTitle() : this.dicomWebConfig.aeTitle; this.aeTitle = this.dicomDimseConfig.enableDimse ? this.dicomDimseConfig.aeTitle : this.dicomWebConfig.aeTitle; if (!this.aeTitle) diff --git a/config/jsconfig.mongodb.json b/config/jsconfig.mongodb.json new file mode 100644 index 00000000..199b4dd8 --- /dev/null +++ b/config/jsconfig.mongodb.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "paths": { + "@dcm4che/*": ["./models/DICOM/dcm4che/wrapper/org/dcm4che3/*"], + "@java-wrapper/*": ["./models/DICOM/dcm4che/wrapper/*"], + "@models/*": ["./models/*"], + "@error/*" : ["./error/*"], + "@root/*": ["./*"], + "@chinlinlee/*": ["./models/DICOM/dcm4che/wrapper/org/github/chinlinlee/*"], + "@dbModels/*": ["./models/mongodb/models/*"], + "@dicom-json-model": ["./models/DICOM/dicom-json-model.js"], + "@query-dicom-json-factory": ["./api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js"], + "@stow-rs-service": ["./api/dicom-web/controller/STOW-RS/service/stow-rs.service.js"], + "@qido-rs-service": ["./api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js"], + "@wado-rs-service": ["./api/dicom-web/controller/WADO-RS/service/WADO-RS.service.js"], + "@wado-uri-service": ["./api/WADO-URI/service/WADO-URI.service.js"], + "@bulkdata-service": ["./api/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js"], + "@delete-service": ["./api/dicom-web/controller/WADO-RS/deletion/service/delete.js"], + "@rendered-service": ["./api/dicom-web/controller/WADO-RS/service/rendered.service.js"], + "@thumbnail-service": ["./api/dicom-web/controller/WADO-RS/service/thumbnail.service.js"], + "@ups-service/*": ["./api/dicom-web/controller/UPS-RS/service/*"], + "@mwl-service/*": ["./api/dicom-web/controller/MWL-RS/service/*"], + "@dimse-query-builder": ["./dimse/queryBuilder.js"], + "@dimse-patient-query-task": ["./dimse/patientQueryTask.js"], + "@dimse-study-query-task": ["./dimse/studyQueryTask.js"], + "@dimse-series-query-task": ["./dimse/seriesQueryTask.js"], + "@dimse-instance-query-task": ["./dimse/instanceQueryTask.js"], + "@dimse-utils": ["./dimse/utils.js"], + "@api/*": ["./api/*"] + } + }, + "exclude": [ + "node_modules", "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/config/jsconfig.sql.json b/config/jsconfig.sql.json new file mode 100644 index 00000000..c4d1a146 --- /dev/null +++ b/config/jsconfig.sql.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "paths": { + "@dcm4che/*": ["./models/DICOM/dcm4che/wrapper/org/dcm4che3/*"], + "@java-wrapper/*": ["./models/DICOM/dcm4che/wrapper/*"], + "@models/*": ["./models/*"], + "@error/*" : ["./error/*"], + "@root/*": ["./*"], + "@chinlinlee/*": ["./models/DICOM/dcm4che/wrapper/org/github/chinlinlee/*"], + "@dbModels/*": ["./models/sql/models/*"], + "@dicom-json-model": ["./models/sql/dicom-json-model.js"], + "@query-dicom-json-factory": ["./api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js"], + "@stow-rs-service": ["./api/dicom-web/controller/STOW-RS/service/stow-rs.service.js"], + "@qido-rs-service": ["./api-sql/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js"], + "@wado-rs-service": ["./api-sql/dicom-web/controller/WADO-RS/service/WADO-RS.service.js"], + "@wado-uri-service": ["./api-sql/WADO-URI/service/WADO-URI.service.js"], + "@bulkdata-service": ["./api-sql/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js"], + "@delete-service": ["./api-sql/dicom-web/controller/WADO-RS/deletion/service/delete.js"], + "@rendered-service": ["./api-sql/dicom-web/controller/WADO-RS/service/rendered.service.js"], + "@thumbnail-service": ["./api-sql/dicom-web/controller/WADO-RS/service/thumbnail.service.js"], + "@ups-service/*": ["./api-sql/dicom-web/controller/UPS-RS/service/*"], + "@mwl-service/*": ["./api-sql/dicom-web/controller/MWL-RS/service/*"], + "@dimse-query-builder": ["./dimse-sql/queryBuilder.js"], + "@dimse-patient-query-task": ["./dimse-sql/patientQueryTask.js"], + "@dimse-study-query-task": ["./dimse-sql/studyQueryTask.js"], + "@dimse-series-query-task": ["./dimse-sql/seriesQueryTask.js"], + "@dimse-instance-query-task": ["./dimse-sql/instanceQueryTask.js"], + "@dimse-utils": ["./dimse-sql/utils.js"], + "@api/*": ["./api-sql/*"] + } + }, + "exclude": [ + "node_modules", "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/config/modula-alias/mongodb/package.json b/config/modula-alias/mongodb/package.json new file mode 100644 index 00000000..1ccf6c60 --- /dev/null +++ b/config/modula-alias/mongodb/package.json @@ -0,0 +1,32 @@ +{ + "_moduleAliases": { + "@dcm4che": "../../../models/DICOM/dcm4che/wrapper/org/dcm4che3", + "@java-wrapper": "../../../models/DICOM/dcm4che/wrapper", + "@models": "../../../models", + "@error": "../../../error", + "@root": "../../../", + "@chinlinlee": "../../../models/DICOM/dcm4che/wrapper/org/github/chinlinlee", + "@dbModels": "../../../models/mongodb/models", + "@dbInitializer": "../../../models/mongodb/index.js", + "@dicom-json-model": "../../../models/DICOM/dicom-json-model.js", + "@query-dicom-json-factory": "../../../api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js", + "@stow-rs-service": "../../../api/dicom-web/controller/STOW-RS/service/stow-rs.service.js", + "@qido-rs-service": "../../../api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js", + "@wado-rs-service": "../../../api/dicom-web/controller/WADO-RS/service/WADO-RS.service.js", + "@wado-uri-service": "../../../api/WADO-URI/service/WADO-URI.service.js", + "@bulkdata-service": "../../../api/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js", + "@delete-service": "../../../api/dicom-web/controller/WADO-RS/deletion/service/delete.js", + "@rendered-service": "../../../api/dicom-web/controller/WADO-RS/service/rendered.service.js", + "@thumbnail-service": "../../../api/dicom-web/controller/WADO-RS/service/thumbnail.service.js", + "@ups-service": "../../../api/dicom-web/controller/UPS-RS/service", + "@mwl-service": "../../../api/dicom-web/controller/MWL-RS/service", + "@dimse-query-builder": "../../../dimse/queryBuilder.js", + "@dimse-patient-query-task": "../../../dimse/patientQueryTask.js", + "@dimse-study-query-task": "../../../dimse/studyQueryTask.js", + "@dimse-series-query-task": "../../../dimse/seriesQueryTask.js", + "@dimse-instance-query-task": "../../../dimse/instanceQueryTask.js", + "@dimse-utils": "../../../dimse/utils.js", + "@dimse": "../../../dimse", + "@api": "../../../api" + } +} \ No newline at end of file diff --git a/config/modula-alias/sql/package.json b/config/modula-alias/sql/package.json new file mode 100644 index 00000000..cf5845c7 --- /dev/null +++ b/config/modula-alias/sql/package.json @@ -0,0 +1,32 @@ +{ + "_moduleAliases": { + "@dcm4che": "../../../models/DICOM/dcm4che/wrapper/org/dcm4che3", + "@java-wrapper": "../../../models/DICOM/dcm4che/wrapper", + "@models": "../../../models", + "@error": "../../../error", + "@root": "../../../", + "@chinlinlee": "../../../models/DICOM/dcm4che/wrapper/org/github/chinlinlee", + "@dbModels": "../../../models/sql/models", + "@dbInitializer": "../../../models/sql/initializer.js", + "@dicom-json-model": "../../../models/sql/dicom-json-model.js", + "@query-dicom-json-factory": "../../../api-sql/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js", + "@stow-rs-service": "../../../api/dicom-web/controller/STOW-RS/service/stow-rs.service.js", + "@qido-rs-service": "../../../api-sql/dicom-web/controller/QIDO-RS/service/QIDO-RS.service.js", + "@wado-rs-service": "../../../api-sql/dicom-web/controller/WADO-RS/service/WADO-RS.service.js", + "@wado-uri-service": "../../../api-sql/WADO-URI/service/WADO-URI.service.js", + "@bulkdata-service": "../../../api-sql/dicom-web/controller/WADO-RS/bulkdata/service/bulkdata.js", + "@delete-service": "../../../api-sql/dicom-web/controller/WADO-RS/deletion/service/delete.js", + "@rendered-service": "../../../api-sql/dicom-web/controller/WADO-RS/service/rendered.service.js", + "@thumbnail-service": "../../../api-sql/dicom-web/controller/WADO-RS/service/thumbnail.service.js", + "@ups-service": "../../../api-sql/dicom-web/controller/UPS-RS/service", + "@mwl-service": "../../../api-sql/dicom-web/controller/MWL-RS/service", + "@dimse-query-builder": "../../../dimse-sql/queryBuilder.js", + "@dimse-patient-query-task": "../../../dimse-sql/patientQueryTask.js", + "@dimse-study-query-task": "../../../dimse-sql/studyQueryTask.js", + "@dimse-series-query-task": "../../../dimse-sql/seriesQueryTask.js", + "@dimse-instance-query-task": "../../../dimse-sql/instanceQueryTask.js", + "@dimse-utils": "../../../dimse-sql/utils.js", + "@dimse": "../../../dimse-sql", + "@api": "../../../api-sql" + } +} \ No newline at end of file diff --git a/dimse-sql/index.js b/dimse-sql/index.js new file mode 100644 index 00000000..49196011 --- /dev/null +++ b/dimse-sql/index.js @@ -0,0 +1,53 @@ +const { java } = require("@models/DICOM/dcm4che/java-instance"); + +const { BasicCEchoSCP } = require("@dcm4che/net/service/BasicCEchoSCP"); +const { DicomServiceRegistry } = require("@dcm4che/net/service/DicomServiceRegistry"); +const { JsCStoreScp } = require("../dimse/c-store"); +const { JsCFindScp } = require("../dimse/c-find"); +const { JsCMoveScp } = require("../dimse/c-move"); +const { JsCGetScp } = require("../dimse/c-get"); +const { DcmQrScp } = require("@root/dimse"); + +class SqlDcmQrScp extends DcmQrScp { + + constructor() { + super(); + } + + async createDicomServiceRegistry() { + let dicomServiceRegistry = new DicomServiceRegistry(); + + await dicomServiceRegistry.addDicomService(new BasicCEchoSCP()); + + // await dicomServiceRegistry.addDicomService(new JsStgCmtScp(this).get()); + + // #region C-STORE + let jsCStoreScp = new JsCStoreScp(); + await dicomServiceRegistry.addDicomService(jsCStoreScp.get()); + // #endregion + + // #region C-FIND + await dicomServiceRegistry.addDicomService(new JsCFindScp().getPatientRootLevel()); + await dicomServiceRegistry.addDicomService(new JsCFindScp().getStudyRootLevel()); + await dicomServiceRegistry.addDicomService(new JsCFindScp().getPatientStudyOnlyLevel()); + // #endregion + + // #region C-MOVE + await dicomServiceRegistry.addDicomService(new JsCMoveScp(this).getPatientRootLevel()); + await dicomServiceRegistry.addDicomService(new JsCMoveScp(this).getStudyRootLevel()); + await dicomServiceRegistry.addDicomService(new JsCMoveScp(this).getPatientStudyOnlyLevel()); + // #endregion + + // #region C-GET + await dicomServiceRegistry.addDicomService(new JsCGetScp().getPatientRootLevel()); + await dicomServiceRegistry.addDicomService(new JsCGetScp().getStudyRootLevel()); + await dicomServiceRegistry.addDicomService(new JsCGetScp().getPatientStudyOnlyLevel()); + await dicomServiceRegistry.addDicomService(new JsCGetScp().getCompositeLevel()); + // #endregion + + return dicomServiceRegistry; + } + +} + +module.exports.DcmQrScp = SqlDcmQrScp; \ No newline at end of file diff --git a/dimse-sql/instanceQueryTask.js b/dimse-sql/instanceQueryTask.js new file mode 100644 index 00000000..ec1cf106 --- /dev/null +++ b/dimse-sql/instanceQueryTask.js @@ -0,0 +1,90 @@ +const _ = require("lodash"); + +const { JsSeriesQueryTask } = require("./seriesQueryTask"); +const { InstanceQueryTask } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/InstanceQueryTask"); +const { Attributes } = require("@dcm4che/data/Attributes"); +const { InstanceModel } = require("@models/sql/models/instance.model"); +const { InstanceQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/instanceQueryBuilder"); +const { InstanceQueryTaskInjectProxy, InstanceMatchIteratorProxy } = require("@root/dimse/instanceQueryTask"); +const { QueryTaskUtils } = require("@root/dimse/utils"); + + +class JsInstanceQueryTask extends JsSeriesQueryTask { + constructor(as, pc, rq, keys) { + super(as, pc, rq, keys); + + this.instanceCursor = null; + this.instance = null; + /** @type { Attributes | null } */ + this.instanceAttr = null; + } + + async get() { + let instanceQueryTask = await InstanceQueryTask.newInstanceAsync( + this.as, + this.pc, + this.rq, + this.keys, + this.getQueryTaskInjectProxy(), + this.getPatientQueryTaskInjectProxy(), + this.getStudyQueryTaskInjectProxy(), + this.getSeriesQueryTaskInjectProxy(), + this.getInstanceQueryTaskInjectProxy() + ); + + await super.get(); + await this.instanceQueryTaskInjectProxy.wrappedFindNextInstance(); + + return instanceQueryTask; + } + + getQueryTaskInjectProxy() { + if (!this.matchIteratorProxy) { + this.matchIteratorProxy = new InstanceMatchIteratorProxy(this); + } + return this.matchIteratorProxy.get(); + } + + getInstanceQueryTaskInjectProxy() { + if (!this.instanceQueryTaskInjectProxy) { + this.instanceQueryTaskInjectProxy = new SqlInstanceQueryTaskInjectProxy(this); + } + + return this.instanceQueryTaskInjectProxy.get(); + } + + async getNextInstanceCursor() { + this.instanceOffset = 0; + + let queryAttr = await QueryTaskUtils.getQueryAttribute(this.keys, this.seriesAttr); + let dbQuery = await QueryTaskUtils.getDbQuery(queryAttr, "instance"); + let instanceQueryBuilder = new InstanceQueryBuilder({ + query: { + ...dbQuery + } + }); + let q = instanceQueryBuilder.build(); + this.instanceQuery = { + ...q + }; + } + +} + +class SqlInstanceQueryTaskInjectProxy extends InstanceQueryTaskInjectProxy { + constructor(instanceQueryTask) { + super(instanceQueryTask); + } + + async getInstance() { + this.instanceQueryTask.instance = await InstanceModel.findOne({ + ...this.instanceQueryTask.instanceQuery, + attributes: ["json"], + limit: 1, + offset: this.instanceQueryTask.instanceOffset++ + }); + + this.instanceQueryTask.instanceAttr = this.instanceQueryTask.instance ? await this.instanceQueryTask.instance.getAttributes() : null; + } +} +module.exports.JsInstanceQueryTask = JsInstanceQueryTask; \ No newline at end of file diff --git a/dimse-sql/patientQueryTask.js b/dimse-sql/patientQueryTask.js new file mode 100644 index 00000000..42e4145b --- /dev/null +++ b/dimse-sql/patientQueryTask.js @@ -0,0 +1,83 @@ +const _ = require("lodash"); +const { createPatientQueryTaskInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/PatientQueryTaskInject"); +const { DimseQueryBuilder } = require("@dimse-query-builder"); +const { PatientQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/patientQueryBuilder"); +const { PatientModel } = require("@models/sql/models/patient.model"); +const { JsPatientQueryTask } = require("../dimse/patientQueryTask"); +const { QueryTaskUtils } = require("@root/dimse/utils"); + + +class SqlJsPatientQueryTask extends JsPatientQueryTask { + constructor(as, pc, rq, keys) { + super(as, pc, rq, keys); + + this.offset = 0; + this.query = null; + } + + getPatientQueryTaskInjectProxy() { + if (!this.patientQueryTaskProxy) { + this.patientQueryTaskProxy = new SqlPatientQueryTaskInjectProxy(this); + } + return this.patientQueryTaskProxy.get(); + } + + async initCursor() { + this.offset = 0; + let sqlQuery = await QueryTaskUtils.getDbQuery(this.keys, "patient"); + let patientQueryBuilder = new PatientQueryBuilder({ + query: { + ...sqlQuery + } + }); + let q = patientQueryBuilder.build(); + this.query = { + ...q + }; + } + +} + +class SqlPatientQueryTaskInjectProxy { + constructor(patientQueryTask) { + /** @type {SqlJsPatientQueryTask} */ + this.patientQueryTask = patientQueryTask; + } + + get() { + return createPatientQueryTaskInjectProxy(this.getProxyMethods(), { + keepAsDaemon: true + }); + } + + getProxyMethods() { + return { + wrappedFindNextPatient: this.wrappedFindNextPatient.bind(this), + getPatient: this.getPatient.bind(this), + findNextPatient: this.findNextPatient.bind(this) + }; + } + + async wrappedFindNextPatient() { + await this.findNextPatient(); + } + + async findNextPatient() { + await this.getPatient(); + return !_.isNull(this.patientQueryTask.patientAttr); + } + + async getPatient() { + let patient = await PatientModel.findOne({ + ...this.patientQueryTask.query, + attributes: ["json"], + limit: 1, + offset: this.patientQueryTask.offset++ + }); + + this.patientQueryTask.patient = patient; + this.patientQueryTask.patientAttr = this.patientQueryTask.patient ? await this.patientQueryTask.patient.getAttributes() : null; + } +} + +module.exports.JsPatientQueryTask = SqlJsPatientQueryTask; \ No newline at end of file diff --git a/dimse-sql/queryBuilder.js b/dimse-sql/queryBuilder.js new file mode 100644 index 00000000..abbec7e4 --- /dev/null +++ b/dimse-sql/queryBuilder.js @@ -0,0 +1,27 @@ +const _ = require("lodash"); + +const { DimseQueryBuilder } = require("@root/dimse/queryBuilder"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); + + +class SqlDimseQueryBuilder extends DimseQueryBuilder { + + /** + * + * @param {Attributes} queryKeys + * @param {"patient" | "study" | "series" | "instance"} level + */ + constructor(queryKeys, level="patient") { + super(queryKeys, level); + } + + async build(query) { + return convertAllQueryToDicomTag( + this.cleanEmptyQuery(query), + false + ); + } +} + +module.exports.SqlDimseQueryBuilder = SqlDimseQueryBuilder; +module.exports.DimseQueryBuilder = SqlDimseQueryBuilder; \ No newline at end of file diff --git a/dimse-sql/seriesQueryTask.js b/dimse-sql/seriesQueryTask.js new file mode 100644 index 00000000..bcdcf38d --- /dev/null +++ b/dimse-sql/seriesQueryTask.js @@ -0,0 +1,93 @@ +const _ = require("lodash"); + +const { createQueryTaskInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/QueryTaskInject"); +const { DimseQueryBuilder } = require("@dimse-query-builder"); +const { JsStudyQueryTask } = require("./studyQueryTask"); +const { SeriesQueryTask } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/SeriesQueryTask"); +const { Attributes } = require("@dcm4che/data/Attributes"); +const { SeriesQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/seriesQueryBuilder"); +const { SeriesModel } = require("@models/sql/models/series.model"); +const { Tag } = require("@dcm4che/data/Tag"); +const { SeriesQueryTaskInjectProxy, SeriesMatchIteratorProxy } = require("@root/dimse/seriesQueryTask"); +const { QueryTaskUtils } = require("@root/dimse/utils"); + + +class JsSeriesQueryTask extends JsStudyQueryTask { + constructor(as, pc, rq, keys) { + super(as, pc, rq, keys); + + this.seriesCursor = null; + this.series = null; + /** @type { Attributes | null } */ + this.seriesAttr = null; + } + + async get() { + let seriesQueryTask = await SeriesQueryTask.newInstanceAsync( + this.as, + this.pc, + this.rq, + this.keys, + this.getQueryTaskInjectProxy(), + this.getPatientQueryTaskInjectProxy(), + this.getStudyQueryTaskInjectProxy(), + this.getSeriesQueryTaskInjectProxy() + ); + + await super.get(); + await this.seriesQueryTaskInjectProxy.wrappedFindNextSeries(); + + return seriesQueryTask; + } + + getQueryTaskInjectProxy() { + if (!this.matchIteratorProxy) { + this.matchIteratorProxy = new SeriesMatchIteratorProxy(this); + } + + return this.matchIteratorProxy.get(); + } + + getSeriesQueryTaskInjectProxy() { + if (!this.seriesQueryTaskInjectProxy) { + this.seriesQueryTaskInjectProxy = new SqlSeriesQueryTaskInjectProxy(this); + } + + return this.seriesQueryTaskInjectProxy.get(); + } + + async getNextSeriesCursor() { + this.seriesOffset = 0; + let queryAttr = await QueryTaskUtils.getQueryAttribute(this.keys, this.studyAttr, "series"); + let sqlQuery = await QueryTaskUtils.getDbQuery(queryAttr, "series"); + + let seriesQueryBuilder = new SeriesQueryBuilder({ + query: { + ...sqlQuery + } + }); + let q = seriesQueryBuilder.build(); + this.seriesQuery = { + ...q + }; + } + +} + +class SqlSeriesQueryTaskInjectProxy extends SeriesQueryTaskInjectProxy { + constructor(seriesQueryTask) { + super(seriesQueryTask); + } + async getSeries() { + this.seriesQueryTask.series = await SeriesModel.findOne({ + ...this.seriesQueryTask.seriesQuery, + attributes: ["json"], + limit: 1, + offset: this.seriesQueryTask.seriesOffset++ + }); + + this.seriesQueryTask.seriesAttr = this.seriesQueryTask.series ? await this.seriesQueryTask.series.getAttributes() : null; + } +} + +module.exports.JsSeriesQueryTask = JsSeriesQueryTask; \ No newline at end of file diff --git a/dimse-sql/studyQueryTask.js b/dimse-sql/studyQueryTask.js new file mode 100644 index 00000000..79854318 --- /dev/null +++ b/dimse-sql/studyQueryTask.js @@ -0,0 +1,105 @@ +const _ = require("lodash"); + +const { StudyQueryTask } = require("@chinlinlee/dcm777/net/StudyQueryTask"); +const { JsPatientQueryTask } = require("./patientQueryTask"); +const { createStudyQueryTaskInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/StudyQueryTaskInject"); +const { Attributes } = require("@dcm4che/data/Attributes"); +const { StudyQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/querybuilder"); +const { StudyModel } = require("@models/sql/models/study.model"); +const { StudyQueryTaskInjectProxy, StudyMatchIteratorProxy } = require("@root/dimse/studyQueryTask"); +const { QueryTaskUtils } = require("@root/dimse/utils"); + +class JsStudyQueryTask extends JsPatientQueryTask { + constructor(as, pc, rq, keys) { + super(as, pc, rq, keys); + + this.study = null; + /** @type { Attributes | null } */ + this.studyAttr = null; + } + + async get() { + let studyQueryTask = await StudyQueryTask.newInstanceAsync( + this.as, + this.pc, + this.rq, + this.keys, + this.getQueryTaskInjectProxy(), + this.getPatientQueryTaskInjectProxy(), + this.getStudyQueryTaskInjectProxy() + ); + + await super.get(); + await this.studyQueryTaskInjectProxy.wrappedFindNextStudy(); + + return studyQueryTask; + } + + getQueryTaskInjectProxy() { + if (!this.queryTaskInjectProxy) { + this.queryTaskInjectProxy = new StudyMatchIteratorProxy(this); + } + + return this.queryTaskInjectProxy.get(); + } + + getStudyQueryTaskInjectProxy() { + if (!this.studyQueryTaskInjectProxy) { + this.studyQueryTaskInjectProxy = new SqlStudyQueryTaskInjectProxy(this); + } + + return this.studyQueryTaskInjectProxy.get(); + } + + async getNextStudyCursor() { + this.studyOffset = 0; + let queryAttr = await QueryTaskUtils.getQueryAttribute(this.keys, this.patientAttr, "study"); + let sqlQuery = await QueryTaskUtils.getDbQuery(queryAttr, "study"); + + let studyQueryBuilder = new StudyQueryBuilder({ + query: { + ...sqlQuery + } + }); + let q = studyQueryBuilder.build(); + this.studyQuery = { + ...q + }; + } + + async auditDicomInstancesAccessed() { + if (!this.study) + return; + + let auditManager = await QueryTaskUtils.getAuditManager(this.as); + let studyUID = _.get(this.study, "x0020000D"); + auditManager.onDicomInstancesAccessed([studyUID]); + } +} + +class SqlStudyQueryTaskInjectProxy extends StudyQueryTaskInjectProxy { + constructor(studyQueryTask) { + super(studyQueryTask); + } + + get() { + return createStudyQueryTaskInjectProxy(this.getProxyMethods(), { + keepAsDaemon: true + }); + } + + async getStudy() { + this.studyQueryTask.study = await StudyModel.findOne({ + ...this.studyQueryTask.studyQuery, + attributes: ["json"], + limit: 1, + offset: this.studyQueryTask.studyOffset++ + }); + + this.studyQueryTask.auditDicomInstancesAccessed(); + this.studyQueryTask.studyAttr = this.studyQueryTask.study ? await this.studyQueryTask.study.getAttributes() : null; + } + +} + +module.exports.JsStudyQueryTask = JsStudyQueryTask; \ No newline at end of file diff --git a/dimse-sql/utils.js b/dimse-sql/utils.js new file mode 100644 index 00000000..a090c17c --- /dev/null +++ b/dimse-sql/utils.js @@ -0,0 +1,110 @@ +const _ = require("lodash"); +const path = require("path"); +const { Attributes } = require("@dcm4che/data/Attributes"); +const { importClass } = require("java-bridge"); +const { raccoonConfig } = require("@root/config-class"); +const { InstanceLocator } = require("@dcm4che/net/service/InstanceLocator"); +const { default: File } = require("@java-wrapper/java/io/File"); +const sequenceInstance = require("@models/sql/instance"); +const { InstanceQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/instanceQueryBuilder"); +const { QueryTaskUtils } = require("@root/dimse/utils"); +/** + * + * @param {number} tag + */ +function intTagToString(tag) { + return tag.toString(16).padStart(8, "0").toUpperCase(); +} + +/** + * + * @param {Attributes} keys + * @returns + */ +async function getInstancesFromKeysAttr(keys) { + const { SqlDimseQueryBuilder: DimseQueryBuilder } = require("./queryBuilder"); + let queryBuilder = new DimseQueryBuilder(keys, "instance"); + let normalQuery = await queryBuilder.toNormalQuery(); + let sqlQuery = await queryBuilder.build(normalQuery); + let instanceQueryBuilder = new InstanceQueryBuilder({ + query: { + ...sqlQuery + } + }); + let q = instanceQueryBuilder.build(); + let instanceQuery = { + ...q + }; + + let instances = await sequenceInstance.model("Instance").findAll({ + ...instanceQuery, + attributes: ["json", "instancePath"] + }); + + const JArrayList = await importClass("java.util.ArrayList"); + let list = await JArrayList.newInstanceAsync(); + + for (let instance of instances) { + let instanceFile = await File.newInstanceAsync( + path.join( + raccoonConfig.dicomWebConfig.storeRootPath, + instance.instancePath + ) + ); + + let fileUri = await instanceFile.toURI(); + let fileUriString = await fileUri.toString(); + + let instanceLocator = await InstanceLocator.newInstanceAsync( + _.get(instance.json, "00080016.Value.0"), + _.get(instance.json, "00080018.Value.0"), + _.get(instance.json, "00020010.Value.0"), + fileUriString + ); + + await list.add(instanceLocator); + } + + return list; +} + +/** + * + * @param {Attributes} keys + * @returns + */ +async function findOneInstanceFromKeysAttr(keys) { + const { SqlDimseQueryBuilder: DimseQueryBuilder } = require("./queryBuilder"); + let queryBuilder = new DimseQueryBuilder(keys, "instance"); + let normalQuery = await queryBuilder.toNormalQuery(); + let sqlQuery = await queryBuilder.getMongoQuery(normalQuery); + let instanceQueryBuilder = new InstanceQueryBuilder({ + query: { + ...sqlQuery + } + }); + let q = instanceQueryBuilder.build(); + let instanceQuery = { + ...q + }; + + let instance = await sequenceInstance.model("Instance").findOne({ + ...instanceQuery, + attributes: ["json"] + }); + + return instance.json; +} + +QueryTaskUtils.getDbQuery = async function (queryAttr, level = "patient") { + let queryBuilder = await QueryTaskUtils.getQueryBuilder(queryAttr, level); + let normalQuery = await queryBuilder.toNormalQuery(); + let dbQuery = await queryBuilder.build(normalQuery); + + return dbQuery; +}; + +module.exports.intTagToString = intTagToString; +module.exports.getInstancesFromKeysAttr = getInstancesFromKeysAttr; +module.exports.findOneInstanceFromKeysAttr = findOneInstanceFromKeysAttr; +module.exports.QueryTaskUtils = QueryTaskUtils; \ No newline at end of file diff --git a/dimse/c-find.js b/dimse/c-find.js index c519dfaa..1bd5c061 100644 --- a/dimse/c-find.js +++ b/dimse/c-find.js @@ -7,10 +7,10 @@ const { PresentationContext } = require("@dcm4che/net/pdu/PresentationContext"); const { QueryRetrieveLevel2 } = require("@dcm4che/net/service/QueryRetrieveLevel2"); const { BasicModCFindSCP } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/BasicModCFindSCP"); const { createCFindSCPInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/CFindSCPInject"); -const { JsPatientQueryTask } = require("./patientQueryTask"); -const { JsStudyQueryTask } = require("./studyQueryTask"); -const { JsSeriesQueryTask } = require("./seriesQueryTask"); -const { JsInstanceQueryTask } = require("./instanceQueryTask"); +const { JsPatientQueryTask } = require("@dimse-patient-query-task"); +const { JsStudyQueryTask } = require("@dimse-study-query-task"); +const { JsSeriesQueryTask } = require("@dimse-series-query-task"); +const { JsInstanceQueryTask } = require("@dimse-instance-query-task"); const { PATIENT_ROOT_LEVELS, STUDY_ROOT_LEVELS, PATIENT_STUDY_ONLY_LEVELS } = require("./level"); class JsCFindScp { diff --git a/dimse/c-get.js b/dimse/c-get.js index fbeb0f0a..3b1ff5bc 100644 --- a/dimse/c-get.js +++ b/dimse/c-get.js @@ -2,7 +2,7 @@ const { UID } = require("@dcm4che/data/UID"); const { createCGetSCPInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/CGetSCPInject"); const { SimpleCGetSCP } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/SimpleCGetSCP"); const { PATIENT_ROOT_LEVELS, STUDY_ROOT_LEVELS, PATIENT_STUDY_ONLY_LEVELS } = require("./level"); -const { getInstancesFromKeysAttr } = require("./utils"); +const { getInstancesFromKeysAttr } = require("@dimse-utils"); const { RetrieveTaskImpl } = require("@chinlinlee/dcm777/dcmqrscp/RetrieveTaskImpl"); const { createRetrieveAuditInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/dcmqrscp/RetrieveAuditInject"); const { Dimse } = require("@dcm4che/net/Dimse"); diff --git a/dimse/c-move.js b/dimse/c-move.js index 8a8d0bc3..e56215b2 100644 --- a/dimse/c-move.js +++ b/dimse/c-move.js @@ -13,7 +13,7 @@ const { AAssociateRQ } = require("@dcm4che/net/pdu/AAssociateRQ"); const { Connection } = require("@dcm4che/net/Connection"); const { RetrieveTaskImpl } = require("@chinlinlee/dcm777/dcmqrscp/RetrieveTaskImpl"); const { Dimse } = require("@dcm4che/net/Dimse"); -const { getInstancesFromKeysAttr } = require("./utils"); +const { getInstancesFromKeysAttr } = require("@dimse-utils"); const { createRetrieveAuditInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/dcmqrscp/RetrieveAuditInject"); const { DimseRetrieveAuditService } = require("./service/retrieveAudit.service"); diff --git a/dimse/c-store.js b/dimse/c-store.js index 30e24b45..6461da5c 100644 --- a/dimse/c-store.js +++ b/dimse/c-store.js @@ -1,10 +1,8 @@ -const myMongoDB = require("@models/mongodb"); - const path = require("path"); const { createCStoreSCPInjectProxy } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/CStoreSCPInject"); const { default: SimpleCStoreSCP } = require("@java-wrapper/org/github/chinlinlee/dcm777/net/SimpleCStoreSCP"); const { default: File } = require("@java-wrapper/java/io/File"); -const { StowRsService } = require("@root/api/dicom-web/controller/STOW-RS/service/stow-rs.service"); +const { StowRsService } = require("@stow-rs-service"); const { default: Association } = require("@dcm4che/net/Association"); const { PresentationContext } = require("@dcm4che/net/pdu/PresentationContext"); const { Attributes } = require("@dcm4che/data/Attributes"); diff --git a/dimse/instanceQueryTask.js b/dimse/instanceQueryTask.js index 393147de..39e61cd9 100644 --- a/dimse/instanceQueryTask.js +++ b/dimse/instanceQueryTask.js @@ -177,4 +177,6 @@ class InstanceMatchIteratorProxy { } } -module.exports.JsInstanceQueryTask = JsInstanceQueryTask; \ No newline at end of file +module.exports.JsInstanceQueryTask = JsInstanceQueryTask; +module.exports.InstanceQueryTaskInjectProxy = InstanceQueryTaskInjectProxy; +module.exports.InstanceMatchIteratorProxy = InstanceMatchIteratorProxy; \ No newline at end of file diff --git a/dimse/patientQueryTask.js b/dimse/patientQueryTask.js index b72705f6..f1c74373 100644 --- a/dimse/patientQueryTask.js +++ b/dimse/patientQueryTask.js @@ -191,4 +191,5 @@ class PatientMatchIteratorProxy { } } -module.exports.JsPatientQueryTask = JsPatientQueryTask; \ No newline at end of file +module.exports.JsPatientQueryTask = JsPatientQueryTask; +module.exports.PatientMatchIteratorProxy = PatientMatchIteratorProxy; \ No newline at end of file diff --git a/dimse/seriesQueryTask.js b/dimse/seriesQueryTask.js index 47deb050..7d20e19e 100644 --- a/dimse/seriesQueryTask.js +++ b/dimse/seriesQueryTask.js @@ -171,4 +171,6 @@ class SeriesMatchIteratorProxy { } } -module.exports.JsSeriesQueryTask = JsSeriesQueryTask; \ No newline at end of file +module.exports.JsSeriesQueryTask = JsSeriesQueryTask; +module.exports.SeriesQueryTaskInjectProxy = SeriesQueryTaskInjectProxy; +module.exports.SeriesMatchIteratorProxy = SeriesMatchIteratorProxy; \ No newline at end of file diff --git a/dimse/studyQueryTask.js b/dimse/studyQueryTask.js index cd300a20..c94960ab 100644 --- a/dimse/studyQueryTask.js +++ b/dimse/studyQueryTask.js @@ -178,4 +178,6 @@ class StudyMatchIteratorProxy { } } -module.exports.JsStudyQueryTask = JsStudyQueryTask; \ No newline at end of file +module.exports.JsStudyQueryTask = JsStudyQueryTask; +module.exports.StudyMatchIteratorProxy = StudyMatchIteratorProxy; +module.exports.StudyQueryTaskInjectProxy = StudyQueryTaskInjectProxy; \ No newline at end of file diff --git a/dimse/utils.js b/dimse/utils.js index 33e2c2ec..595af81d 100644 --- a/dimse/utils.js +++ b/dimse/utils.js @@ -106,13 +106,14 @@ class QueryTaskUtils { static async getQueryAttribute(keys, parentAttr, level = "patient") { let queryAttr = await Attributes.newInstanceAsync(); + await Attributes.unifyCharacterSets([keys, parentAttr]); await queryAttr.addAll(keys); await queryAttr.addSelected(parentAttr, QUERY_ATTR_SELECTED_TAGS[level]); return queryAttr; } static async getQueryBuilder(queryAttr, level = "patient") { - const { DimseQueryBuilder } = require("./queryBuilder"); + const { DimseQueryBuilder } = require("@dimse-query-builder"); return new DimseQueryBuilder(queryAttr, level); } diff --git a/docs/swagger/parameters/dicomweb-common.yaml b/docs/swagger/parameters/dicomweb-common.yaml index b2c1448b..2175fd21 100644 --- a/docs/swagger/parameters/dicomweb-common.yaml +++ b/docs/swagger/parameters/dicomweb-common.yaml @@ -28,4 +28,12 @@ components: "image/jpeg": schema: type: string - format: byte \ No newline at end of file + format: byte + parameters: + "filter": + description: "{attributeID}={value}; {attributeID} = {dicomTag} | {dicomKeyword} | {dicomTag}.{attributeID} | {dicomKeyword}.{attributeID}" + in: query + schema: + type: array + items: + type: string \ No newline at end of file diff --git a/docs/swagger/parameters/mwl.yaml b/docs/swagger/parameters/mwl.yaml new file mode 100644 index 00000000..f5459729 --- /dev/null +++ b/docs/swagger/parameters/mwl.yaml @@ -0,0 +1,23 @@ +components: + parameters: + spsID: + in: path + name: spsID + required: true + schema: + type: string + spsStatus: + in: path + name: spsStatus + required: true + enum: + - SCHEDULED + - ARRIVED + - READY + - STARTED + - DEPARTED + - CANCELED + - DISCONTINUED + - COMPLETED + schema: + type: string \ No newline at end of file diff --git a/error/api-errors.handler.js b/error/api-errors.handler.js index eba07b06..bcb2c0b6 100644 --- a/error/api-errors.handler.js +++ b/error/api-errors.handler.js @@ -86,7 +86,7 @@ class ApiErrorArrayHandler { * @param {ApiLogger} apiLogger * @param {Error} e */ - static raiseInternalServerError(response, apiLogger, e) { + static raiseInternalServerError(e, response, apiLogger) { apiLogger.logger.error(e); if (!response.headersSent) { diff --git a/error/dicom-web-service.js b/error/dicom-web-service.js index b2cd090a..1e517517 100644 --- a/error/dicom-web-service.js +++ b/error/dicom-web-service.js @@ -1,6 +1,7 @@ const DicomWebStatusCodes = { "InvalidAttributeValue": "0106", "DuplicateSOPinstance": "0111", + "NoSuchSOPInstance": "0112", "InvalidArgumentValue": "0115", "MissingAttribute": "0120", "ProcessingFailure": "0272", diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 9d8fade8..00000000 --- a/jsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "module": "CommonJS", - "paths": { - "@dcm4che/*": ["./models/DICOM/dcm4che/wrapper/org/dcm4che3/*"], - "@java-wrapper/*": ["./models/DICOM/dcm4che/wrapper/*"], - "@models/*": ["./models/*"], - "@error/*" : ["./error/*"], - "@root/*": ["./*"], - "@chinlinlee/*": ["./models/DICOM/dcm4che/wrapper/org/github/chinlinlee/*"], - "@dbModels/*": ["./models/mongodb/models/*"], - "@dicom-json-model": ["./models/DICOM/dicom-json-model.js"], - "@query-dicom-json-factory": ["./api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js"] - } - }, - "exclude": [ - "node_modules", "**/node_modules/*" - ] -} \ No newline at end of file diff --git a/models/DICOM/audit/auditManager.js b/models/DICOM/audit/auditManager.js index b89b8596..d50fbcae 100644 --- a/models/DICOM/audit/auditManager.js +++ b/models/DICOM/audit/auditManager.js @@ -2,6 +2,10 @@ const _ = require("lodash"); const { AuditMessageFactory } = require("./auditMessageFactory"); const { EventType } = require("./eventType"); +const { AuditMessageModel } = require("@models/db/auditMessage.model"); +const { AuditMessageModelLoggerDbImpl } = require("@models/db/auditMessage.loggerImpl"); +const AuditMessageModelMongodbDbImpl = require("@models/mongodb/models/auditMessage"); +const { raccoonConfig } = require("@root/config-class"); /** * @typedef AuditMessageModel @@ -159,8 +163,9 @@ class AuditManager { } static getAuditMessageModel() { - const mongoose = require("mongoose"); - return mongoose.model("auditMessage"); + if (raccoonConfig.serverConfig.dbType === "sql") + return new AuditMessageModel(new AuditMessageModelLoggerDbImpl()); + return new AuditMessageModel(AuditMessageModelMongodbDbImpl); } } diff --git a/models/DICOM/audit/auditMessageFactory.js b/models/DICOM/audit/auditMessageFactory.js index d9a565ea..87612832 100644 --- a/models/DICOM/audit/auditMessageFactory.js +++ b/models/DICOM/audit/auditMessageFactory.js @@ -24,6 +24,7 @@ const { EventType } = require("./eventType"); const { default: ActiveParticipantBuilder } = require("@dcm4che/audit/ActiveParticipantBuilder"); const { AuditMessages$UserIDTypeCode } = require("@dcm4che/audit/AuditMessages$UserIDTypeCode"); const { ParticipatingObjectFactory } = require("./participatingObjectFactory"); +const { raccoonConfig } = require("@root/config-class"); class AuditMessageFactory { constructor() { } @@ -349,8 +350,13 @@ class AuditMessageFactory { } getInstanceModel() { - const mongoose = require("mongoose"); - return mongoose.model("dicom"); + if (raccoonConfig.serverConfig.dbType === "sql") { + const sequelizeInstance = require("@models/sql/instance"); + return sequelizeInstance.model("Instance"); + } else { + const mongoose = require("mongoose"); + return mongoose.model("dicom"); + } } } diff --git a/models/DICOM/dicom-json-model.js b/models/DICOM/dicom-json-model.js index 233bc8f9..ac773bc7 100644 --- a/models/DICOM/dicom-json-model.js +++ b/models/DICOM/dicom-json-model.js @@ -76,7 +76,8 @@ class BaseDicomJson { } setValue(tag, value) { - let vrOfTag = _.get(dictionary.tagVR, `${tag}.vr`); + let lastTag = tag.split(".").at(-1); + let vrOfTag = _.get(dictionary.tagVR, `${lastTag}.vr`); _.set(this.dicomJson, `${tag}.vr`, vrOfTag); _.set(this.dicomJson, `${tag}.Value`, [value]); } diff --git a/models/DICOM/dicom-tags-mapping.js b/models/DICOM/dicom-tags-mapping.js index d889d338..273867ec 100644 --- a/models/DICOM/dicom-tags-mapping.js +++ b/models/DICOM/dicom-tags-mapping.js @@ -618,5 +618,154 @@ module.exports.tagsNeedStore = { "00741224": { "vr": "SQ" } - } + }, + MWL: { + "00080005": { + "vr": "CS" + }, + "00080050": { + "vr": "SH" + }, + "00080054": { + "vr": "AE" + }, + "00080056": { + "vr": "CS" + }, + "00080090": { + "vr": "PN" + }, + "00080201": { + "vr": "SH" + }, + "00081110": { + "vr": "SQ" + }, + "00081120": { + "vr": "SQ" + }, + "00100010": { + "vr": "PN" + }, + "00100020": { + "vr": "LO" + }, + "00100021": { + "vr": "LO" + }, + "00100024": { + "vr": "SQ" + }, + "00100030": { + "vr": "DA" + }, + "00100040": { + "vr": "CS" + }, + "00100032": { + "vr": "TM" + }, + "00101002": { + "vr": "SQ" + }, + "00101001": { + "vr": "PN" + }, + "00100033": { + "vr": "LO" + }, + "00100034": { + "vr": "LO" + }, + "00100035": { + "vr": "CS" + }, + "00100050": { + "vr": "SQ" + }, + "00100101": { + "vr": "SQ" + }, + "00100200": { + "vr": "CS" + }, + "00100212": { + "vr": "UC" + }, + "00101030": { + "vr": "DS" + }, + "00102000": { + "vr": "LO" + }, + "00102110": { + "vr": "LO" + }, + "001021C0": { + "vr": "US" + }, + "0020000D": { + "vr": "UI" + }, + "00321032": { + "vr": "PN" + }, + "00321060": { + "vr": "LO" + }, + "00321064": { + "vr": "SQ" + }, + "00380010": { + "vr": "LO" + }, + "00380050": { + "vr": "LO" + }, + "00380300": { + "vr": "LO" + }, + "00380500": { + "vr": "LO" + }, + "00400001": { + "vr": "CS" + }, + "00400002": { + "vr": "DA" + }, + "00400003": { + "vr": "TM" + }, + "00400006": { + "vr": "PN" + }, + "00400009": { + "vr": "SH" + }, + "00400010": { + "vr": "SH" + }, + "00400011": { + "vr": "SH" + }, + "00400020": { + "vr": "CS" + }, + "00400100": { + "vr": "SQ" + }, + "00401001": { + "vr": "SH" + }, + "00401003": { + "vr": "SH" + }, + "00401004": { + "vr": "LO" + }, + "00403001": { + "vr": "LO" + } + } }; diff --git a/models/db/auditMessage.loggerImpl.js b/models/db/auditMessage.loggerImpl.js new file mode 100644 index 00000000..3c41e79f --- /dev/null +++ b/models/db/auditMessage.loggerImpl.js @@ -0,0 +1,9 @@ +const { logger } = require("@root/utils/logs/log"); + +class AuditMessageModelLoggerDbImpl { + createMessage(msg) { + logger.info(JSON.stringify(msg)); + } +} + +module.exports.AuditMessageModelLoggerDbImpl = AuditMessageModelLoggerDbImpl; \ No newline at end of file diff --git a/models/db/auditMessage.model.js b/models/db/auditMessage.model.js new file mode 100644 index 00000000..53b64e68 --- /dev/null +++ b/models/db/auditMessage.model.js @@ -0,0 +1,11 @@ +class AuditMessageModel { + constructor(dbModel) { + this.dbModel = dbModel; + } + async createMessage(msg) { + return await this.dbModel.createMessage(msg); + } +} + + +module.exports.AuditMessageModel = AuditMessageModel; \ No newline at end of file diff --git a/models/mongodb/connector.js b/models/mongodb/connector.js index 9383af28..69d52fab 100644 --- a/models/mongodb/connector.js +++ b/models/mongodb/connector.js @@ -14,7 +14,7 @@ const { authSource, isShardingMode, urlOptions -} = raccoonConfig.mongoDbConfig; +} = raccoonConfig.dbConfig; module.exports = exports = function () { const collection = {}; diff --git a/models/mongodb/models/mwlitems.model.js b/models/mongodb/models/mwlitems.model.js new file mode 100644 index 00000000..ca1de43c --- /dev/null +++ b/models/mongodb/models/mwlitems.model.js @@ -0,0 +1,102 @@ +const path = require("path"); +const mongoose = require("mongoose"); +const _ = require("lodash"); +const { tagsNeedStore } = require("../../DICOM/dicom-tags-mapping"); +const { getVRSchema } = require("../schema/dicomJsonAttribute"); +const { IncludeFieldsFactory } = require("../service"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); + +let mwlItemSchema = new mongoose.Schema( + {}, + { + strict: true, + versionKey: false, + toObject: { + getters: true + }, + statics: { + /** + * + * @param {import("../../../utils/typeDef/dicom").DicomJsonMongoQueryOptions} queryOptions + * @returns + */ + getDicomJson: async function (queryOptions) { + let projection = mongoose.model("mwlItems").getDicomJsonProjection(queryOptions.includeFields); + try { + let docs = await mongoose.model("mwlItems").find(queryOptions.query, projection) + .limit(queryOptions.limit) + .skip(queryOptions.skip) + .setOptions({ + strictQuery: false + }) + .exec(); + + + let mwlDicomJson = docs.map((v) => { + let obj = v.toObject(); + delete obj._id; + delete obj.id; + return obj; + }); + + return mwlDicomJson; + + } catch (e) { + throw e; + } + }, + getDicomJsonProjection: function (includeFields) { + let includeFieldsFactory = new IncludeFieldsFactory(includeFields); + return includeFieldsFactory.getMwlLevelFields(); + }, + getCount: async function (query) { + return await mongoose.model("mwlItems").countDocuments(query); + }, + deleteByStudyInstanceUIDAndSpsID: async function(studyUID, spsID) { + return await mongoose.model("mwlItems").deleteMany({ + $and: [ + { + [`${dictionary.keyword.StudyInstanceUID}.Value.0`]: studyUID + }, + { + [`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepID}.Value.0`]: spsID + } + ] + }); + } + }, + methods: { + toDicomJson: function () { + let obj = this.toObject(); + delete obj._id; + delete obj.id; + return obj; + } + } + } +); + +for (let tag in tagsNeedStore.MWL) { + let vr = tagsNeedStore.MWL[tag].vr; + let tagSchema = getVRSchema(vr); + mwlItemSchema.add({ + [tag]: tagSchema + }); +} + +for (let tag in tagsNeedStore.Patient) { + let vr = tagsNeedStore.Patient[tag].vr; + let tagSchema = getVRSchema(vr); + mwlItemSchema.add({ + [tag]: tagSchema + }); +} + +let mwlItemModel = mongoose.model( + "mwlItems", + mwlItemSchema, + "mwlItems" +); + +module.exports = mwlItemModel; +module.exports.MwlItemModel = mwlItemModel; diff --git a/models/mongodb/models/patient.model.js b/models/mongodb/models/patient.model.js index 97e18dfe..78ce4b8d 100644 --- a/models/mongodb/models/patient.model.js +++ b/models/mongodb/models/patient.model.js @@ -31,6 +31,25 @@ let patientSchemaOptions = _.merge( fields[tag] = 1; } return fields; + }, + /** + * + * @param {string} patientId + * @param {any} patient + */ + findOneOrCreatePatient: async function(patientId, patient) { + /** @type {PatientModel | null} */ + let foundPatient = await mongoose.model("patient").findOne({ + "00100020.Value": patientId + }); + + if (!foundPatient) { + /** @type {PatientModel} */ + let patientObj = new mongoose.model("patient")(patient); + patient = await patientObj.save(); + } + + return patient; } } } diff --git a/models/mongodb/models/workItems.js b/models/mongodb/models/workitems.model.js similarity index 91% rename from models/mongodb/models/workItems.js rename to models/mongodb/models/workitems.model.js index bea22c29..35a5ebb8 100644 --- a/models/mongodb/models/workItems.js +++ b/models/mongodb/models/workitems.model.js @@ -4,6 +4,7 @@ const _ = require("lodash"); const { tagsNeedStore } = require("../../DICOM/dicom-tags-mapping"); const { getVRSchema } = require("../schema/dicomJsonAttribute"); const { SUBSCRIPTION_STATE } = require("../../DICOM/ups"); +const { DicomJsonModel } = require("@models/DICOM/dicom-json-model"); let workItemSchema = new mongoose.Schema( { @@ -34,6 +35,11 @@ let workItemSchema = new mongoose.Schema( versionKey: false, toObject: { getters: true + }, + methods: { + toDicomJsonModel: function () { + return new DicomJsonModel(this); + } } } ); @@ -119,3 +125,4 @@ let workItemModel = mongoose.model( /** @type { WorkItemsModel } */ module.exports = workItemModel; +module.exports.WorkItemModel = workItemModel; diff --git a/models/mongodb/service.js b/models/mongodb/service.js index fe7ab819..6e6a98ca 100644 --- a/models/mongodb/service.js +++ b/models/mongodb/service.js @@ -320,6 +320,19 @@ class IncludeFieldsFactory { }; } + getMwlLevelFields() { + if (this.all) { + return {}; + } + + let fields = {}; + for (let tag in tagsOfRequiredMatching.Mwl) { + fields[tag] = 1; + } + + return fields; + } + /** * @private */ diff --git a/models/sql/deleteSchedule.js b/models/sql/deleteSchedule.js new file mode 100644 index 00000000..81cadb73 --- /dev/null +++ b/models/sql/deleteSchedule.js @@ -0,0 +1,113 @@ +const schedule = require("node-schedule"); +const { StudyModel } = require("./models/study.model"); +const { Op } = require("sequelize"); +const moment = require("moment"); +const { logger } = require("@root/utils/logs/log"); +const { InstanceModel } = require("./models/instance.model"); +const { SeriesModel } = require("./models/series.model"); + +// Delete dicom with delete status >= 2 +schedule.scheduleJob("0 0 */1 * * *", async function () { + deleteExpireStudies().catch((e) => { + logger.error(e); + }); + deleteExpireSeries().catch((e) => { + logger.error(e); + }); + deleteExpireInstances().catch((e) => { + logger.error(e); + }); +}); + + +async function deleteExpireStudies() { + let deletedStudies = await StudyModel.findAll({ + where: { + deleteStatus: { + [Op.gte]: 2 + } + } + }); + + for (let deletedStudy of deletedStudies) { + let updateAtDate = moment(deletedStudy.getDataValue("updatedAt")); + let now = moment(); + let diff = now.diff(updateAtDate, "days"); + if (diff >= 30) { + let studyUID = deletedStudy.getDataValue("x0020000D"); + + logger.info("delete expired study: " + studyUID); + await Promise.all([ + InstanceModel.destroy({ + where: { + x0020000D: studyUID + } + }), + SeriesModel.destroy({ + where: { + x0020000D: studyUID + } + }), + deletedStudy.destroy() + ]); + + await deletedStudy.deleteStudyFolder(); + } + } +} + +async function deleteExpireSeries() { + let deletedSeries = await SeriesModel.findAll({ + where: { + deleteStatus: { + [Op.gte]: 2 + } + } + }); + + for (let aDeletedSeries of deletedSeries) { + let updateAtDate = moment(aDeletedSeries.getDataValue("updatedAt")); + let now = moment(); + let diff = now.diff(updateAtDate, "days"); + if (diff >= 30) { + let studyUID = aDeletedSeries.getDataValue("x0020000D"); + let seriesUID = aDeletedSeries.getDataValue("x0020000E"); + + logger.info("delete expired series: " + seriesUID); + await Promise.all([ + InstanceModel.destroy({ + where: { + x0020000D: studyUID, + x0020000E: seriesUID + } + }), + aDeletedSeries.destroy() + ]); + + await aDeletedSeries.deleteSeriesFolder(); + } + } +} + +async function deleteExpireInstances() { + let deletedInstances = await InstanceModel.findAll({ + where: { + deleteStatus: { + [Op.gte]: 2 + } + } + }); + + for (let deletedInstance of deletedInstances) { + let instanceUID = deletedInstance.getDataValue("x00080018"); + + let updateAtDate = moment(deletedInstance.getDataValue("updatedAt")); + let now = moment(); + let diff = now.diff(updateAtDate, "days"); + if (diff >= 30) { + logger.info("delete expired instance: " + instanceUID); + await deletedInstance.destroy(); + await deletedInstance.deleteInstance(); + } + } +} \ No newline at end of file diff --git a/models/sql/dicom-json-model.js b/models/sql/dicom-json-model.js new file mode 100644 index 00000000..4fd110ea --- /dev/null +++ b/models/sql/dicom-json-model.js @@ -0,0 +1,94 @@ +const _ = require("lodash"); +const shortHash = require("shorthash2"); +const fsP = require("fs/promises"); +const path = require("path"); +const mkdirp = require("mkdirp"); + +const { BaseDicomJson, DicomJsonModel, DicomJsonBinaryDataModel } = require("@models/DICOM/dicom-json-model"); +const { PatientPersistentObject } = require("./po/patient.po"); +const { StudyPersistentObject } = require("./po/study.po"); +const { SeriesPersistentObject } = require("./po/series.po"); +const { InstancePersistentObject } = require("./po/instance.po"); +const { StudyModel } = require("./models/study.model"); +const { DicomBulkDataModel } = require("./models/dicomBulkData.model"); + +const { raccoonConfig } = require("@root/config-class"); +const { logger } = require("@root/utils/logs/log"); + +DicomJsonModel.prototype.storeToDb = async function (dicomFileSaveInfo) { + let dbJson = this.getCleanDataBeforeStoringToDb(dicomFileSaveInfo); + + try { + let storedPatient = await this.storePatientCollection(dbJson); + let storedStudy = await this.storeStudyCollection(dbJson, storedPatient); + let storedSeries = await this.storeSeriesCollection(dbJson, storedStudy); + await this.storeInstanceCollection(dbJson, storedSeries); + + await StudyModel.updateModalitiesInStudy(storedStudy); + } catch (e) { + throw e; + } +}; +DicomJsonModel.prototype.storePatientCollection = async function (dicomJson) { + let patientPo = new PatientPersistentObject(dicomJson); + let patient = await patientPo.createPatient(); + return patient; +}; + +DicomJsonModel.prototype.storeStudyCollection = async function(dicomJson, patient) { + let studyPo = new StudyPersistentObject(dicomJson, patient); + let study = await studyPo.createStudy(); + return study; +}; + +DicomJsonModel.prototype.storeSeriesCollection = async function (dicomJson, study) { + let seriesPo = new SeriesPersistentObject(dicomJson, study); + let series = await seriesPo.createSeries(); + return series; +}; + +DicomJsonModel.prototype.storeInstanceCollection = async function(dicomJson, series) { + let instancePo = new InstancePersistentObject(dicomJson, series); + return await instancePo.createInstance(); +}; + +class SqlDicomJsonBinaryDataModel extends DicomJsonBinaryDataModel{ + constructor(dicomJsonModel) { + super(dicomJsonModel); + this.bulkDataModelClass = BulkData; + } +} + +class BulkData { + constructor(uidObj, filename, pathOfBinaryProperty) { + /** @type {import("../../utils/typeDef/dicom").UIDObject} */ + this.uidObj = uidObj; + this.filename = filename; + this.pathOfBinaryProperty = pathOfBinaryProperty; + } + + async storeToDb() { + + let item = { + studyUID: this.uidObj.studyUID, + seriesUID: this.uidObj.seriesUID, + instanceUID: this.uidObj.sopInstanceUID, + filename: this.filename, + binaryValuePath: this.pathOfBinaryProperty + }; + + await DicomBulkDataModel.findOrCreate({ + where: { + instanceUID: this.uidObj.sopInstanceUID, + binaryValuePath: this.pathOfBinaryProperty + }, + defaults: item + }); + + logger.info(`[STOW-RS] [Store bulkdata ${JSON.stringify(item)} successful]`); + } +} + +module.exports.DicomJsonModel = DicomJsonModel; +module.exports.BaseDicomJson = BaseDicomJson; +module.exports.DicomJsonBinaryDataModel = SqlDicomJsonBinaryDataModel; \ No newline at end of file diff --git a/models/sql/generate-erd.js b/models/sql/generate-erd.js new file mode 100644 index 00000000..f747929e --- /dev/null +++ b/models/sql/generate-erd.js @@ -0,0 +1,14 @@ +require("module-alias/register"); +const fsP = require("fs/promises"); +const sequelize = require("./instance"); +const sequelizeErd = require("sequelize-erd"); + + +require("./init").then(async()=> { + const svg = await sequelizeErd({ + source: sequelize + }); // sequelizeErd() returns a Promise + await fsP.writeFile("./erd.svg", svg); +}); + + diff --git a/models/sql/init.js b/models/sql/init.js new file mode 100644 index 00000000..819276df --- /dev/null +++ b/models/sql/init.js @@ -0,0 +1,213 @@ +const { PersonNameModel } = require("./models/personName.model"); +const { PatientModel } = require("./models/patient.model"); +const { StudyModel } = require("./models/study.model"); +const { SeriesModel } = require("./models/series.model"); +const { InstanceModel } = require("./models/instance.model"); +const { DicomBulkDataModel } = require("./models/dicomBulkData.model"); +const { raccoonConfig } = require("@root/config-class"); + +const sequelizeInstance = require("./instance"); +const { SeriesRequestAttributesModel } = require("./models/seriesRequestAttributes.model"); +const { DicomCodeModel } = require("./models/dicomCode.model"); +const { DicomContentSqModel } = require("./models/dicomContentSQ.model"); +const { VerifyIngObserverSqModel } = require("./models/verifyingObserverSQ.model"); +const { WorkItemModel } = require("./models/workitems.model"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { UpsSubscriptionModel } = require("./models/upsSubscription.model"); +const { UpsRequestAttributesModel } = require("./models/upsRequestAttributes.model"); +const { MwlItemModel } = require("./models/mwlitems.model"); + +async function initDatabasePostgres() { + const { Client } = require("pg"); + const client = new Client({ + user: raccoonConfig.dbConfig.username, + password: raccoonConfig.dbConfig.password, + host: raccoonConfig.dbConfig.host, + port: raccoonConfig.dbConfig.port, + database: "postgres", + logging: raccoonConfig.dbConfig.logging + }); + + await client.connect(); + + try { + let result = await client.query(`SELECT 'CREATE DATABASE ${raccoonConfig.dbConfig.database}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${raccoonConfig.dbConfig.database}')`); + if (result.rowCount > 0 ) { + await client.query(`CREATE DATABASE ${raccoonConfig.dbConfig.database}`); + } + } catch(e) { + console.error(e); + process.exit(1); + } finally { + await client.end(); + } +} + +async function init() { + require("./deleteSchedule"); + + if (raccoonConfig.dbConfig.dialect === "postgres") { + await initDatabasePostgres(); + } + + try { + await sequelizeInstance.authenticate(); + + PatientModel.belongsTo(PersonNameModel, { + foreignKey: "x00100010" + }); + + StudyModel.belongsTo(PatientModel, { + foreignKey: "x00100020", + targetKey: "x00100020" + }); + + StudyModel.belongsTo(PersonNameModel, { + foreignKey: "x00080090" + }); + + StudyModel.hasMany(SeriesModel, { + foreignKey: "x0020000D", + sourceKey: "x0020000D" + }); + SeriesModel.belongsTo(StudyModel, { + foreignKey: "x0020000D", + targetKey: "x0020000D" + }); + + // Performing Physician Name many to many + SeriesModel.belongsToMany(PersonNameModel, { + through: "PerformingPhysicianName", + as: "performingPhysicianName", + sourceKey: "x0020000E", + foreignKey: "x0020000E" + }); + PersonNameModel.belongsToMany(SeriesModel, { + through: "PerformingPhysicianName" + }); + + // Operator's Name many to many + SeriesModel.belongsToMany(PersonNameModel, { + through: "OperatorsName", + as: "operatorsName", + sourceKey: "x0020000E", + foreignKey: "x0020000E" + }); + PersonNameModel.belongsToMany(SeriesModel, { + through: "OperatorsName" + }); + + SeriesModel.hasOne(SeriesRequestAttributesModel, { + foreignKey: "x0020000E", + targetKey: "x0020000E" + }); + + InstanceModel.belongsTo(SeriesModel, { + foreignKey: "x0020000E", + targetKey: "x0020000E" + }); + + InstanceModel.hasOne(DicomCodeModel, { + foreignKey: "SOPInstanceUID", + sourceKey: "x00080018" + }); + + InstanceModel.hasOne(VerifyIngObserverSqModel, { + foreignKey: "SOPInstanceUID", + sourceKey: "x00080018" + }); + VerifyIngObserverSqModel.hasOne(DicomCodeModel, { + foreignKey: "x0040A088" + }); + VerifyIngObserverSqModel.belongsTo(PersonNameModel, { + foreignKey: "x0040A075" + }); + + InstanceModel.hasOne(DicomContentSqModel, { + foreignKey: "SOPInstanceUID", + sourceKey: "x00080018" + }); + DicomContentSqModel.hasOne(DicomCodeModel, { + as: "ConceptNameCode" + }); + DicomContentSqModel.hasOne(DicomCodeModel, { + as: "ConceptCode" + }); + + WorkItemModel.belongsTo(PatientModel, { + foreignKey: "x00100020", + targetKey: "x00100020" + }); + + WorkItemModel.belongsTo(DicomCodeModel, { + foreignKey: "x00404025", + as: dictionary.tag["00404025"] + }); + WorkItemModel.belongsTo(DicomCodeModel, { + foreignKey: "x00404026", + as: dictionary.tag["00404026"] + }); + WorkItemModel.belongsTo(DicomCodeModel, { + foreignKey: "x00404027", + as: dictionary.tag["00404027"] + }); + WorkItemModel.belongsTo(DicomCodeModel, { + foreignKey: "x00404009", + as: dictionary.tag["00404009"] + }); + WorkItemModel.belongsTo(DicomCodeModel, { + foreignKey: "x00404018", + as: dictionary.tag["00404018"] + }); + WorkItemModel.belongsTo(DicomCodeModel, { + foreignKey: "x00080082", + as: dictionary.tag["00080082"] + }); + + WorkItemModel.belongsTo(PersonNameModel, { + foreignKey: "x00404037", + as: dictionary.tag["00404037"] + }); + + WorkItemModel.hasMany(UpsSubscriptionModel); + WorkItemModel.hasOne(UpsRequestAttributesModel, { + foreignKey: "upsInstanceUID", + sourceKey: "upsInstanceUID" + }); + + MwlItemModel.belongsTo(PatientModel, { + targetKey: "x00100020", + foreignKey: "patient_id" + }); + + MwlItemModel.belongsTo(DicomCodeModel, { + foreignKey: "protocol_code", + as: dictionary.tag["00400008"] + }); + MwlItemModel.belongsTo(DicomCodeModel, { + foreignKey: "institution_department_type_code", + as: dictionary.tag["00081041"] + }); + MwlItemModel.belongsTo(DicomCodeModel, { + foreignKey: "institution_code", + as: dictionary.tag["00080082"] + }); + MwlItemModel.belongsTo(PersonNameModel, { + foreignKey: "physician_name", + as: dictionary.tag["00400006"] + }); + + //TODO: 設計完畢後要將 force 刪除 + await sequelizeInstance.sync({ + force: raccoonConfig.dbConfig.forceSync + }); + } catch (e) { + console.error('Unable to connect to the database:', e); + process.exit(1); + } + +} + +module.exports = (() => init())(); + + diff --git a/models/sql/initializer.js b/models/sql/initializer.js new file mode 100644 index 00000000..ccb86508 --- /dev/null +++ b/models/sql/initializer.js @@ -0,0 +1 @@ +require("./init").then(()=> console.log("Sequelize initialized")); \ No newline at end of file diff --git a/models/sql/instance.js b/models/sql/instance.js new file mode 100644 index 00000000..e71e1d32 --- /dev/null +++ b/models/sql/instance.js @@ -0,0 +1,9 @@ +const { raccoonConfig } = require("@root/config-class"); +const { Sequelize } = require("sequelize"); + +const sequelize = new Sequelize(raccoonConfig.dbConfig); + +/** + * @type {Sequelize} + */ +module.exports = sequelize; \ No newline at end of file diff --git a/models/sql/models/dicomBulkData.model.js b/models/sql/models/dicomBulkData.model.js new file mode 100644 index 00000000..1ee82028 --- /dev/null +++ b/models/sql/models/dicomBulkData.model.js @@ -0,0 +1,30 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); + +class DicomBulkDataModel extends Model {}; + +DicomBulkDataModel.init({ + studyUID: { + type: vrTypeMapping.UI + }, + seriesUID: { + type: vrTypeMapping.UI + }, + instanceUID: { + type: vrTypeMapping.UI + }, + filename: { + type: DataTypes.TEXT("long") + }, + binaryValuePath: { + type: DataTypes.TEXT("medium") + } +}, { + sequelize: sequelizeInstance, + modelName: "DicomBulkData", + tableName: "DicomBulkData", + freezeTableName: true +}); + +module.exports.DicomBulkDataModel = DicomBulkDataModel; diff --git a/models/sql/models/dicomCode.model.js b/models/sql/models/dicomCode.model.js new file mode 100644 index 00000000..4efb9c8e --- /dev/null +++ b/models/sql/models/dicomCode.model.js @@ -0,0 +1,38 @@ +const { Model } = require("sequelize"); +const sequelizeInstance = require("@root/models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); + +class DicomCodeModel extends Model {}; + +DicomCodeModel.init({ + "x00080100": { + type: vrTypeMapping.SH + }, + "x00080102": { + type: vrTypeMapping.SH + }, + "x00080103": { + type: vrTypeMapping.SH + }, + "x00080104": { + type: vrTypeMapping.LO + } +}, { + sequelize: sequelizeInstance, + modelName: "DicomCode", + tableName: "DicomCode", + freezeTableName: true, + indexes: [ + { + fields: ["x00080100"] + }, + { + fields: ["x00080102"] + }, + { + fields: ["x00080103"] + } + ] +}); + +module.exports.DicomCodeModel = DicomCodeModel; \ No newline at end of file diff --git a/models/sql/models/dicomContentSQ.model.js b/models/sql/models/dicomContentSQ.model.js new file mode 100644 index 00000000..48b9c7d7 --- /dev/null +++ b/models/sql/models/dicomContentSQ.model.js @@ -0,0 +1,42 @@ +const { Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); + +class DicomContentSqModel extends Model {} + +DicomContentSqModel.init({ + "x0040A010": { + // Relationship Type + type: vrTypeMapping.CS + }, + "x0040A040": { + // Value Type + type: vrTypeMapping.CS + }, + "x0040A160": { + // Text Value + type: vrTypeMapping.UT, + validate: { + requiredIfValueType(value) { + if (!value && this.x0040A040 === "TEXT") { + throw new Error("x0040A160 is required if x0040A040 is TEXT"); + } + } + } + } +}, { + sequelize: sequelizeInstance, + modelName: "DicomContentSQ", + tableName: "DicomContentSQ", + freezeTableName: true, + indexes: [ + { + fields: ["x0040A010"] + }, + { + fields: ["x0040A160"] + } + ] +}); + +module.exports.DicomContentSqModel = DicomContentSqModel; \ No newline at end of file diff --git a/models/sql/models/dicomToJpegTask.model.js b/models/sql/models/dicomToJpegTask.model.js new file mode 100644 index 00000000..57c3c03a --- /dev/null +++ b/models/sql/models/dicomToJpegTask.model.js @@ -0,0 +1,61 @@ +const _ = require("lodash"); +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); + +class DicomToJpegTaskModel extends Model {}; + +DicomToJpegTaskModel.init({ + studyUID: { + type: vrTypeMapping.UI + }, + seriesUID: { + type: vrTypeMapping.UI + }, + instanceUID: { + type: vrTypeMapping.UI + }, + message: { + type: DataTypes.TEXT("long") + }, + status: { + type: DataTypes.BOOLEAN + }, + taskTime: { + type: DataTypes.DATE + }, + finishedTime: { + type: DataTypes.DATE + }, + fileSize: { + type: DataTypes.TEXT("medium") + } +}, { + sequelize: sequelizeInstance, + modelName: "DicomToJpegTask", + tableName: "DicomToJpegTask", + freezeTableName: true +}); + +DicomToJpegTaskModel.insertOrUpdate = async (item) => { + try { + let [task, created] = await DicomToJpegTaskModel.findOrCreate({ + where: { + studyUID: item.studyUID, + seriesUID: item.seriesUID, + instanceUID: item.instanceUID + }, + defaults: item + }); + + // update + if (!created) { + _.assign(task, item); + await task.save(); + } + } catch(e) { + throw e; + } +}; + +module.exports.DicomToJpegTaskModel = DicomToJpegTaskModel; diff --git a/models/sql/models/instance.model.js b/models/sql/models/instance.model.js new file mode 100644 index 00000000..251bf99f --- /dev/null +++ b/models/sql/models/instance.model.js @@ -0,0 +1,257 @@ +const fsP = require("fs/promises"); +const path = require("path"); +const { Sequelize, DataTypes, Model, Op } = require("sequelize"); +const _ = require("lodash"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { InstanceQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/instanceQueryBuilder"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { getStoreDicomFullPath } = require("@models/mongodb/service"); +const { logger } = require("@root/utils/logs/log"); +const { raccoonConfig } = require("@root/config-class"); + +let Common; +if (raccoonConfig.dicomDimseConfig.enableDimse) { + require("@models/DICOM/dcm4che/java-instance"); + Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common; +} + +class InstanceModel extends Model { + async incrementDeleteStatus() { + let deleteStatus = this.getDataValue("deleteStatus"); + this.setDataValue("deleteStatus", deleteStatus + 1); + await this.save(); + } + + async deleteInstance() { + let instancePath = this.getDataValue("instancePath"); + logger.warn("Permanently delete instance: " + instancePath); + await fsP.rm(path.join(raccoonConfig.dicomWebConfig.storeRootPath, instancePath), { + force: true, + recursive: true + }); + } + + /** + * + * @param {string} studyUID + */ + static async getAuditInstancesInfoFromStudyUID(studyUID) { + let instanceInfos = { + sopClassUIDs: [], + accessionNumbers: [], + patientID: "", + patientName: "" + }; + if (!studyUID) return instanceInfos; + + let instances = await sequelizeInstance.model("Instance").findAll({ + where: { + x0020000D: studyUID + } + }); + + for (let instance of instances) { + let sopClassUID = instance.x00080016; + let accessionNumber = instance.x00080050; + let patientID = instance.x00100020; + let patientName = _.get(instance.json, "00100010.Value.0.Alphabetic"); + sopClassUID ? instanceInfos.sopClassUIDs.push(sopClassUID) : null; + accessionNumber ? instanceInfos.accessionNumbers.push(accessionNumber) : null; + patientID ? instanceInfos.patientID = patientID : null; + patientName ? instanceInfos.patientName = patientName : null; + } + + instanceInfos.sopClassUIDs = _.uniq(instanceInfos.sopClassUIDs); + instanceInfos.accessionNumbers = _.uniq(instanceInfos.accessionNumbers); + + return instanceInfos; + } +}; + +InstanceModel.init({ + "instancePath": { + type: DataTypes.TEXT("long") + }, + "x00020010": { + // Transfer Syntax UID + type: vrTypeMapping.UI + }, + "x0020000D": { + type: vrTypeMapping.UI, + allowNull: false + }, + "x0020000E": { + type: vrTypeMapping.UI, + allowNull: false + }, + "x00080018": { + type: vrTypeMapping.UI, + allowNull: false, + unique: true, + primaryKey: true + }, + "x00080016": { + type: vrTypeMapping.UI + }, + "x00080022": { + type: vrTypeMapping.DA + }, + "x00080023": { + type: vrTypeMapping.DA + }, + "x0008002A": { + type: vrTypeMapping.DT + }, + "x00080033": { + type: vrTypeMapping.TM + }, + "x00200013": { + type: vrTypeMapping.IS + }, + "x00280008": { + // Number of Frames + type: vrTypeMapping.IS + }, + "x00281050": { + type: vrTypeMapping.DS, + get() { + const rawValue = this.getDataValue("x00281050"); + return rawValue ? rawValue.split("\\") : undefined; + } + }, + "x00281051": { + type: vrTypeMapping.DS, + get() { + const rawValue = this.getDataValue("x00281050"); + return rawValue ? rawValue.split("\\") : undefined; + } + }, + "x0040A491": { + type: vrTypeMapping.CS + }, + "x0040A493": { + type: vrTypeMapping.CS + }, + "json": { + type: vrTypeMapping.JSON + }, + "deleteStatus": { + type: DataTypes.INTEGER, + defaultValue: 0 + } +}, { + sequelize: sequelizeInstance, + modelName: "Instance", + tableName: "Instance", + freezeTableName: true +}); + +InstanceModel.getDicomJson = async function (queryOptions) { + let queryBuilder = new InstanceQueryBuilder(queryOptions); + let q = queryBuilder.build(); + if (q[Op.and]) { + q[Op.and].push( + { + deleteStatus: 0 + } + ); + } else { + q[Op.and] = [ + { + deleteStatus: 0 + } + ]; + } + let seriesArray = await InstanceModel.findAll({ + ...q, + attributes: ["json", "x0020000D", "x0020000E", "x00080018"], + limit: queryOptions.limit, + offset: queryOptions.skip + }); + + return await Promise.all(seriesArray.map(async series => { + let { json } = series.toJSON(); + // Set Retrieve URL + let studyInstanceUID = _.get(json, "0020000D.Value.0"); + let seriesInstanceUID = _.get(json, "0020000E.Value.0"); + let sopInstanceUID = _.get(json, "00080018.Value.0"); + _.set(json, dictionary.keyword.RetrieveURL, { + vr: dictionary.tagVR[dictionary.keyword.RetrieveURL].vr, + Value: [ + `${queryOptions.retrieveBaseUrl}/${studyInstanceUID}/series/${seriesInstanceUID}/instances/${sopInstanceUID}` + ] + }); + return json; + })); +}; + +InstanceModel.getPathOfInstance = async function (iParam) { + let { studyUID, seriesUID, instanceUID } = iParam; + + try { + let instance = await sequelizeInstance.model("Instance").findOne({ + where: { + x0020000D: studyUID, + x0020000E: seriesUID, + x00080018: instanceUID, + deleteStatus: 0 + } + }); + + if (instance) { + let instanceJson = await instance.toJSON(); + + _.set(instanceJson, "instancePath", getStoreDicomFullPath(instanceJson)); + _.set(instanceJson, "studyUID", instanceJson.x0020000D); + _.set(instanceJson, "seriesUID", instanceJson.x0020000E); + _.set(instanceJson, "instanceUID", instanceJson.x00080018); + + return instanceJson; + } + + return undefined; + } catch (e) { + throw e; + } +}; + +InstanceModel.getInstanceOfMedianIndex = async function (query) { + let instanceCountOfStudy = await InstanceModel.count({ + where: { + x0020000D: query.studyUID, + deleteStatus: 0 + } + }); + + let instance = await InstanceModel.findOne({ + where: { + x0020000D: query.studyUID, + deleteStatus: 0 + }, + attributes: ["x0020000D", "x0020000E", "x00080018", "instancePath"], + offset: instanceCountOfStudy >> 1, + limit: 1, + order: [ + ["x0020000D", "ASC"], + ["x0020000E", "ASC"] + ] + }); + + if (instance) { + _.set(instance, "studyUID", instance.x0020000D); + _.set(instance, "seriesUID", instance.x0020000E); + _.set(instance, "instanceUID", instance.x00080018); + } + + return instance; +}; + +InstanceModel.prototype.getAttributes = async function () { + let seriesObj = this.toJSON(); + + let jsonStr = JSON.stringify(seriesObj.json); + return await Common.getAttributesFromJsonString(jsonStr); +}; + +module.exports.InstanceModel = InstanceModel; diff --git a/models/sql/models/mwlitems.model.js b/models/sql/models/mwlitems.model.js new file mode 100644 index 00000000..8757971d --- /dev/null +++ b/models/sql/models/mwlitems.model.js @@ -0,0 +1,195 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { raccoonConfig } = require("@root/config-class"); +const { DicomJsonModel } = require("../dicom-json-model"); +const { MwlQueryBuilder } = require("@root/api-sql/dicom-web/controller/MWL-RS/service/query/mwlQueryBuilder"); + +let Common; +if (raccoonConfig.dicomDimseConfig.enableDimse) { + require("@models/DICOM/dcm4che/java-instance"); + Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common; +} + +class MwlItemModel extends Model { + async getAttributes() { + let obj = this.toJSON(); + let jsonStr = JSON.stringify(obj.json); + return await Common.getAttributesFromJsonString(jsonStr); + } + + toDicomJsonModel() { + return new DicomJsonModel(this.json); + } + static async getDicomJson (queryOptions) { + let queryBuilder = new MwlQueryBuilder(queryOptions); + let q = queryBuilder.build(); + + let mwlItems = await MwlItemModel.findAll({ + ...q, + attributes: ["json"], + limit: queryOptions.limit, + offset: queryOptions.skip + }); + + return await Promise.all(mwlItems.map(async item => { + return item.json; + })); + } + + static async getCount(query) { + let queryBuilder = new MwlQueryBuilder({query}); + let q = queryBuilder.build(); + return await this.count({ + ...q + }); + } + + static async deleteByStudyInstanceUIDAndSpsID(studyUID, spsID) { + let deletedCount = await MwlItemModel.destroy({ + where: { + study_instance_uid: studyUID, + sps_id: spsID + } + }); + return { deletedCount }; + } +}; + +/** @type { import("sequelize").ModelAttributes } */ +const MwlItemSchema = { + // 0020000D + study_instance_uid: { + type: vrTypeMapping.UI, + allowNull: false, + unique: true + }, + // 00100010 + patient_id: { + type: vrTypeMapping.LO, + allowNull: false + }, + // 00800050 + accession_number: { + type: vrTypeMapping.SH + }, + // 00800051.00400031 + accno_local_id: { + type: vrTypeMapping.UT + }, + // 00800051.00400032 + accno_universal_id: { + type: vrTypeMapping.UT + }, + // 00800051.00400033 + accno_universal_id_type: { + type: vrTypeMapping.CS + }, + // 00401001 + requested_procedure_id: { + type: vrTypeMapping.SH + }, + // 00380010 + admission_id: { + type: vrTypeMapping.LO + }, + // 00380014.00400031 + issuer_admission_local_id: { + type: vrTypeMapping.UT + }, + // 00380014.00400032 + issuer_admission_universal_id: { + type: vrTypeMapping.UT + }, + // 00380014.00400033 + issuer_admission_universal_id_type: { + type: vrTypeMapping.CS + }, + // TODO Scheduled Procedure Step Sequence + // 0040,0100.00400001 + station_ae_title: { + type: vrTypeMapping.AE + }, + // 0040,0100.00400010 + station_name: { + type: vrTypeMapping.SH + }, + // 0040,0100.00400002 + start_date: { + type: vrTypeMapping.DA + }, + // 0040,0100.00400004 + end_date: { + type: vrTypeMapping.DA + }, + // 0040,0100.00400003 + start_time: { + type: vrTypeMapping.DT + }, + // 0040,0100.00400005 + end_time: { + type: vrTypeMapping.DT + }, + // 0040,0100.00400006 + physician_name: { + //* must reference to PersonName model + type: vrTypeMapping.PN + }, + // 0040,0100.00400011 + procedure_step_location: { + type: vrTypeMapping.SH + }, + // 0040,0100.00400007 + description: { + type: vrTypeMapping.LO + }, + // 0040,0100.00400008 + protocol_code: { + // reference to dicom code model + type: DataTypes.INTEGER + }, + // 00080080 + institution_name: { + type: vrTypeMapping.LO + }, + // 00081040 + institution_department_name: { + type: vrTypeMapping.LO + }, + // Reference to Dicom Code Model + // 00081041 + institution_department_type_code: { + type: DataTypes.INTEGER + }, + // Reference to Dicom Code Model + // 00080082 + institution_code: { + type: DataTypes.INTEGER + }, + // 00400100.00400009 + sps_id: { + type: vrTypeMapping.SH + }, + // 00400100.00400020 + sps_status: { + type: vrTypeMapping.CS + }, + // 00400100.00080060 + modality: { + type: vrTypeMapping.CS + }, + json: { + type: vrTypeMapping.JSON + } +}; + + +MwlItemModel.init(MwlItemSchema, { + sequelize: sequelizeInstance, + modelName: "mwl_item", + tableName: "mwl_item", + freezeTableName: true +}); + +module.exports.MwlItemModel = MwlItemModel; +module.exports.MwlItemSchema = MwlItemSchema; diff --git a/models/sql/models/patient.model.js b/models/sql/models/patient.model.js new file mode 100644 index 00000000..a262a867 --- /dev/null +++ b/models/sql/models/patient.model.js @@ -0,0 +1,92 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { PatientQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/patientQueryBuilder"); +const { raccoonConfig } = require("@root/config-class"); + +let Common; +if (raccoonConfig.dicomDimseConfig.enableDimse) { + require("@models/DICOM/dcm4che/java-instance"); + Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common; +} + +class PatientModel extends Model { + static async updateOrCreatePatient(patient) { + /** @type {PatientModel | null} */ + const { PatientPersistentObject } = require("../po/patient.po"); + let patientPersistent = new PatientPersistentObject(patient); + let bringPatient = await patientPersistent.createPatient(); + + return bringPatient; + } +}; + +PatientModel.init({ + "x00100010": { + type: DataTypes.INTEGER + }, + "x00100020": { + type: vrTypeMapping.LO, + allowNull: false, + unique: true + }, + "x00100021": { + type: vrTypeMapping.LO + }, + "x00100030": { + type: vrTypeMapping.DA + }, + "x00100032": { + type: vrTypeMapping.TM + }, + "x00100040": { + type: vrTypeMapping.CS + }, + "x00102160": { + type: vrTypeMapping.SH + }, + "x00104000": { + type: vrTypeMapping.LT + }, + "x00880130": { + type: vrTypeMapping.SH + }, + "x00880140": { + type: vrTypeMapping.UI + }, + "json": { + type: vrTypeMapping.JSON + } +}, { + sequelize: sequelizeInstance, + modelName: "Patient", + tableName: "Patient", + freezeTableName: true +}); + +PatientModel.getDicomJson = async function (queryOptions) { + let queryBuilder = new PatientQueryBuilder(queryOptions); + let q = queryBuilder.build(); + let studies = await PatientModel.findAll({ + ...q, + attributes: ["json"], + limit: queryOptions.limit, + offset: queryOptions.skip + }); + + + return await Promise.all(studies.map(async study => { + let { json } = study.toJSON(); + + return json; + })); +}; + +PatientModel.prototype.getAttributes = async function () { + let patientObj = this.toJSON(); + + let jsonStr = JSON.stringify(patientObj.json); + return await Common.getAttributesFromJsonString(jsonStr); +}; + +module.exports.PatientModel = PatientModel; diff --git a/models/sql/models/personName.model.js b/models/sql/models/personName.model.js new file mode 100644 index 00000000..9e0be69b --- /dev/null +++ b/models/sql/models/personName.model.js @@ -0,0 +1,94 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { get } = require("lodash"); + +class PersonNameModel extends Model { + + /** + * + * @param {any} nameObj + * @returns + */ + static async createPersonName(nameObj) { + if (!PersonNameModel.isEmpty(nameObj)) { + return await PersonNameModel.create({ + alphabetic: get(nameObj, "Alphabetic", undefined), + ideographic: get(nameObj, "Ideographic", undefined), + phonetic: get(nameObj, "Phonetic", undefined) + }); + } + return undefined; + } + + /** + * + * @param {any} nameObj + * @param {string} id + * @returns + */ + static async updatePersonNameById(nameObj, id) { + if (!PersonNameModel.isEmpty(nameObj)) { + return await PersonNameModel.update({ + alphabetic: get(nameObj, "Alphabetic", undefined), + ideographic: get(nameObj, "Ideographic", undefined), + phonetic: get(nameObj, "Phonetic", undefined) + }, { + where: { + id: id + } + }); + } + return undefined; + } + + /** + * + * @param {any} item + * @param {string} field + * @returns + */ + static async createPersonNames(item, field) { + let personNames = []; + if (item[field]) { + for (let personName of item[field]) { + let personNameSequelize = await PersonNameModel.create({ + alphabetic: get(personName, "Alphabetic", undefined), + ideographic: get(personName, "Ideographic", undefined), + phonetic: get(personName, "Phonetic", undefined) + }); + personNames.push(personNameSequelize); + } + } + return personNames; + } + + static isEmpty(json) { + return !json && !(json?.Alphabetic || json?.Ideographic || json?.Phonetic); + } +} +PersonNameModel.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + alphabetic: { + type: DataTypes.STRING + }, + ideographic: { + type: DataTypes.STRING, + allowNull: true + }, + phonetic: { + type: DataTypes.STRING, + allowNull: true + } +}, { + sequelize: sequelizeInstance, + modelName: "PersonName", + tableName: "PersonName", + freezeTableName: true +}); + + +module.exports.PersonNameModel = PersonNameModel; diff --git a/models/sql/models/series.model.js b/models/sql/models/series.model.js new file mode 100644 index 00000000..fcb74252 --- /dev/null +++ b/models/sql/models/series.model.js @@ -0,0 +1,185 @@ +const fsP = require("fs/promises"); +const path = require("path"); +const { Sequelize, DataTypes, Model, Op } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { SeriesQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/seriesQueryBuilder"); +const _ = require("lodash"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { getStoreDicomFullPathGroup } = require("@models/mongodb/service"); +const { logger } = require("@root/utils/logs/log"); +const { raccoonConfig } = require("@root/config-class"); + +let Common; +if (raccoonConfig.dicomDimseConfig.enableDimse) { + require("@models/DICOM/dcm4che/java-instance"); + Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common; +} + +class SeriesModel extends Model { + getSeriesPath() { + return this.getDataValue("seriesPath"); + } + + async incrementDeleteStatus() { + let deleteStatus = this.getDataValue("deleteStatus"); + this.setDataValue("deleteStatus", deleteStatus + 1); + await this.save(); + } + + async deleteSeriesFolder() { + let seriesPath = this.getDataValue("seriesPath"); + logger.warn("Permanently delete series folder: " + seriesPath); + await fsP.rm(path.join(raccoonConfig.dicomWebConfig.storeRootPath, seriesPath), { + force: true, + recursive: true + }); + } +}; + +SeriesModel.init({ + "seriesPath": { + type: DataTypes.TEXT("long") + }, + "x0020000D": { + type: vrTypeMapping.UI, + allowNull: false + }, + "x0020000E": { + type: vrTypeMapping.UI, + allowNull: false, + unique: true, + primaryKey: true + }, + "x00080021": { + type: vrTypeMapping.DA + }, + "x00080060": { + type: vrTypeMapping.CS + }, + "x0008103E": { + type: vrTypeMapping.LO + }, + "x0008103F": { + // Temp field for future use + // vr: SQ + // VM: 1 + type: vrTypeMapping.JSON + }, + "x00081052": { + // Temp field for future use + // vr: SQ + // VM: 1 + type: vrTypeMapping.JSON + }, + "x00081072": { + // Temp field for future use + // vr: SQ + // VM: 1 + type: vrTypeMapping.JSON + }, + "x00081250": { + // Temp field for future use + // vr: SQ + // VM: 1 + type: vrTypeMapping.JSON + }, + "x00200011": { + type: vrTypeMapping.IS + }, + "x00400244": { + type: vrTypeMapping.DA + }, + "x00400245": { + type: vrTypeMapping.TM + }, + "x00080031": { + type: vrTypeMapping.TM + }, + "json": { + type: vrTypeMapping.JSON + }, + "deleteStatus": { + type: DataTypes.INTEGER, + defaultValue: 0 + } +}, { + sequelize: sequelizeInstance, + modelName: "Series", + tableName: "Series", + freezeTableName: true +}); + +SeriesModel.getDicomJson = async function(queryOptions) { + let queryBuilder = new SeriesQueryBuilder(queryOptions); + let q = queryBuilder.build(); + if (q[Op.and]) { + q[Op.and].push( + { + deleteStatus: 0 + } + ); + } else { + q[Op.and] = [ + { + deleteStatus: 0 + } + ]; + } + let seriesArray = await SeriesModel.findAll({ + ...q, + attributes: ["json", "x0020000E"], + limit: queryOptions.limit, + offset: queryOptions.skip + }); + + return await Promise.all(seriesArray.map(async series => { + let { json } = series.toJSON(); + // Set Retrieve URL + let studyInstanceUID = _.get(json, "0020000D.Value.0"); + let seriesInstanceUID = _.get(json, "0020000E.Value.0"); + _.set(json, dictionary.keyword.RetrieveURL, { + vr: dictionary.tagVR[dictionary.keyword.RetrieveURL].vr, + Value: [ + `${queryOptions.retrieveBaseUrl}/${studyInstanceUID}/series/${seriesInstanceUID}` + ] + }); + return json; + })); +}; + +SeriesModel.getPathGroupOfInstances = async function(iParam) { + let { studyUID, seriesUID } = iParam; + + try { + let instances = await sequelizeInstance.model("Instance").findAll({ + where: { + x0020000D: studyUID, + x0020000E: seriesUID, + deleteStatus: 0 + }, + attributes: ["instancePath", "x0020000D", "x0020000E", "x00080018"] + }); + + let fullPathGroup = getStoreDicomFullPathGroup(instances); + + return fullPathGroup.map(v=> { + _.set(v, "studyUID", v.x0020000D); + _.set(v, "seriesUID", v.x0020000E); + _.set(v, "instanceUID", v.x00080018); + return v; + }); + + } catch (e) { + throw e; + } +}; + +SeriesModel.prototype.getAttributes = async function () { + let seriesObj = this.toJSON(); + + let jsonStr = JSON.stringify(seriesObj.json); + return await Common.getAttributesFromJsonString(jsonStr); +}; + +module.exports.SeriesModel = SeriesModel; diff --git a/models/sql/models/seriesRequestAttributes.model.js b/models/sql/models/seriesRequestAttributes.model.js new file mode 100644 index 00000000..1c1c1afc --- /dev/null +++ b/models/sql/models/seriesRequestAttributes.model.js @@ -0,0 +1,41 @@ +const { DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const _ = require("lodash"); + +class SeriesRequestAttributesModel extends Model {} + +SeriesRequestAttributesModel.init({ + "x0020000E": { + type: vrTypeMapping.UI, + allowNull: false + }, + "x00080050": { + type: vrTypeMapping.SH + }, + "x00080051_x00400031": { + type: vrTypeMapping.UT + }, + "x00080051_x00400032": { + type: vrTypeMapping.UT + }, + "x00080051_x00400033": { + type: vrTypeMapping.CS + }, + "x00321033": { + type: vrTypeMapping.LO + }, + "x00401001": { + type: vrTypeMapping.SH + }, + "x0020000D": { + type: vrTypeMapping.UI + } +}, { + sequelize: sequelizeInstance, + modelName: "SeriesRequestAttributes", + tableName: "SeriesRequestAttributes", + freezeTableName: true +}); + +module.exports.SeriesRequestAttributesModel = SeriesRequestAttributesModel; \ No newline at end of file diff --git a/models/sql/models/study.model.js b/models/sql/models/study.model.js new file mode 100644 index 00000000..62c623d1 --- /dev/null +++ b/models/sql/models/study.model.js @@ -0,0 +1,228 @@ +const fsP = require("fs/promises"); +const path = require("path"); +const { Sequelize, DataTypes, Model, Op } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { SeriesModel } = require("./series.model"); +const _ = require("lodash"); +const { StudyQueryBuilder } = require("@root/api-sql/dicom-web/controller/QIDO-RS/service/querybuilder"); +const { InstanceModel } = require("./instance.model"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { getStoreDicomFullPathGroup } = require("@models/mongodb/service"); +const { logger } = require("@root/utils/logs/log"); +const { raccoonConfig } = require("@root/config-class"); + +let Common; +if (raccoonConfig.dicomDimseConfig.enableDimse) { + require("@models/DICOM/dcm4che/java-instance"); + Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common; +} + +class StudyModel extends Model { + async getNumberOfStudyRelatedSeries() { + let count = await SeriesModel.count({ + where: { + x0020000D: _.get(this.json, "0020000D.Value.0") + } + }); + return count; + } + + async getNumberOfStudyRelatedInstances() { + let count = await InstanceModel.count({ + where: { + x0020000D: _.get(this.json, "0020000D.Value.0") + } + }); + return count; + } + + async incrementDeleteStatus() { + let deleteStatus = this.getDataValue("deleteStatus"); + this.setDataValue("deleteStatus", deleteStatus + 1); + await this.save(); + } + + async deleteStudyFolder() { + let studyPath = this.getDataValue("studyPath"); + logger.warn("Permanently delete study folder: " + studyPath); + await fsP.rm(path.join(raccoonConfig.dicomWebConfig.storeRootPath, studyPath), { + force: true, + recursive: true + }); + } +}; + +StudyModel.init({ + "studyPath": { + type: DataTypes.TEXT("long") + }, + "x00100020": { + type: vrTypeMapping.LO, + allowNull: false + }, + "x00080005": { + type: vrTypeMapping.JSON + }, + "x00080020": { + type: vrTypeMapping.DA + }, + "x00080030": { + type: vrTypeMapping.TM + }, + "x00080050": { + type: vrTypeMapping.SH + }, + "x00080056": { + type: vrTypeMapping.CS + }, + "x00080090": { + type: vrTypeMapping.PN + }, + "x00080201": { + type: vrTypeMapping.SH + }, + "x0020000D": { + type: vrTypeMapping.UI, + allowNull: false, + unique: true, + primaryKey: true + }, + "x00200010": { + type: vrTypeMapping.SH + }, + "x00201206": { + type: vrTypeMapping.IS + }, + "x00201208": { + type: vrTypeMapping.IS + }, + "json": { + type: vrTypeMapping.JSON + }, + "deleteStatus": { + type: DataTypes.INTEGER, + defaultValue: 0 + } +}, { + sequelize: sequelizeInstance, + modelName: "Study", + tableName: "Study", + freezeTableName: true +}); + +StudyModel.updateModalitiesInStudy = async function (study) { + let seriesArray = await SeriesModel.findAll({ + where: { + x0020000D: study.x0020000D, + deleteStatus: 0 + }, + attributes: [ + [Sequelize.fn("DISTINCT", Sequelize.col("x00080060")), "modality"] + ] + }); + + let modalities = []; + for (let item of seriesArray) { + if (_.get(item, "dataValues.modality")) + modalities.push(item.dataValues.modality); + } + + study.json = { + ...study.json, + "00080061": { + vr: "CS", + Value: modalities + } + }; + await study.save(); +}; + +StudyModel.getDicomJson = async function (queryOptions) { + let queryBuilder = new StudyQueryBuilder(queryOptions); + let q = queryBuilder.build(); + if (q[Op.and]) { + q[Op.and].push( + { + deleteStatus: 0 + } + ); + } else { + q[Op.and] = [ + { + deleteStatus: 0 + } + ]; + } + let studies = await StudyModel.findAll({ + ...q, + attributes: ["json"], + limit: queryOptions.limit, + offset: queryOptions.skip + }); + + + return await Promise.all(studies.map(async study => { + let numberOfStudyRelatedSeries = await study.getNumberOfStudyRelatedSeries(); + let numberOfStudyRelatedInstances = await study.getNumberOfStudyRelatedInstances(); + let { json } = study.toJSON(); + // Set Retrieve URL + _.set(json, dictionary.keyword.RetrieveURL, { + vr: dictionary.tagVR[dictionary.keyword.RetrieveURL].vr, + Value: [`${queryOptions.retrieveBaseUrl}/${_.get(json, "0020000D.Value.0")}`] + }); + + // Set number of Study related series + _.set(json, dictionary.keyword.NumberOfStudyRelatedSeries, { + vr: dictionary.tagVR[dictionary.keyword.NumberOfStudyRelatedSeries].vr, + Value: [ + numberOfStudyRelatedSeries.toString() + ] + }); + + // Set number of Study related instances + _.set(json, dictionary.keyword.NumberOfStudyRelatedInstances, { + vr: dictionary.tagVR[dictionary.keyword.NumberOfStudyRelatedInstances].vr, + Value: [ + numberOfStudyRelatedInstances.toString() + ] + }); + + return json; + })); +}; + +StudyModel.getPathGroupOfInstances = async function (iParam) { + let { studyUID } = iParam; + + try { + let instances = await sequelizeInstance.model("Instance").findAll({ + where: { + x0020000D: studyUID, + deleteStatus: 0 + }, + attributes: ["instancePath", "x0020000D", "x0020000E", "x00080018"] + }); + + let fullPathGroup = getStoreDicomFullPathGroup(instances); + + return fullPathGroup.map(v => { + _.set(v, "studyUID", v.x0020000D); + _.set(v, "seriesUID", v.x0020000E); + _.set(v, "instanceUID", v.x00080018); + return v; + }); + + } catch (e) { + throw e; + } +}; + +StudyModel.prototype.getAttributes = async function () { + let studyObj = this.toJSON(); + + let jsonStr = JSON.stringify(studyObj.json); + return await Common.getAttributesFromJsonString(jsonStr); +}; + +module.exports.StudyModel = StudyModel; diff --git a/models/sql/models/upsGlobalSubscription.model.js b/models/sql/models/upsGlobalSubscription.model.js new file mode 100644 index 00000000..f391f062 --- /dev/null +++ b/models/sql/models/upsGlobalSubscription.model.js @@ -0,0 +1,56 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { SUBSCRIPTION_STATE } = require("@models/DICOM/ups"); + +class UpsGlobalSubscriptionModel extends Model { + static async cursor(query) { + return new UpsGlobalSubscriptionModelCursor(query); + } +}; + +UpsGlobalSubscriptionModel.init({ + aeTitle: { + type: DataTypes.TEXT + }, + subscribed: { + type: DataTypes.INTEGER, + defaultValue: SUBSCRIPTION_STATE.NOT_SUBSCRIBED + }, + queryKeys: { + type: vrTypeMapping.JSON, + allowNull: true + }, + isDeletionLock: { + type: DataTypes.BOOLEAN, + defaultValue: false + } +}, { + sequelize: sequelizeInstance, + modelName: "UpsGlobalSubscription", + tableName: "UpsGlobalSubscription", + freezeTableName: true +}); + +class UpsGlobalSubscriptionModelCursor { + /** + * + * @param {import("sequelize").FindOptions} query + */ + constructor(query) { + /** @type { import("sequelize").FindOptions } */ + this.query = query; + this.offset = 0; + this.item = undefined; + } + + async next() { + return await UpsGlobalSubscriptionModel.findOne({ + ...this.query, + offset: this.offset++ + }); + } +} + + +module.exports.UpsGlobalSubscriptionModel = UpsGlobalSubscriptionModel; diff --git a/models/sql/models/upsRequestAttributes.model.js b/models/sql/models/upsRequestAttributes.model.js new file mode 100644 index 00000000..da0795cb --- /dev/null +++ b/models/sql/models/upsRequestAttributes.model.js @@ -0,0 +1,41 @@ +const { DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const _ = require("lodash"); + +class UpsRequestAttributesModel extends Model {} + +UpsRequestAttributesModel.init({ + "upsInstanceUID": { + type: DataTypes.STRING, + allowNull: false + }, + "x00080050": { + type: vrTypeMapping.SH + }, + "x00080051_x00400031": { + type: vrTypeMapping.UT + }, + "x00080051_x00400032": { + type: vrTypeMapping.UT + }, + "x00080051_x00400033": { + type: vrTypeMapping.CS + }, + "x00321033": { + type: vrTypeMapping.LO + }, + "x00401001": { + type: vrTypeMapping.SH + }, + "x0020000D": { + type: vrTypeMapping.UI + } +}, { + sequelize: sequelizeInstance, + modelName: "UpsRequestAttributesModel", + tableName: "UpsRequestAttributesModel", + freezeTableName: true +}); + +module.exports.UpsRequestAttributesModel = UpsRequestAttributesModel; \ No newline at end of file diff --git a/models/sql/models/upsSubscription.model.js b/models/sql/models/upsSubscription.model.js new file mode 100644 index 00000000..f4a78de2 --- /dev/null +++ b/models/sql/models/upsSubscription.model.js @@ -0,0 +1,27 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { SUBSCRIPTION_STATE } = require("@models/DICOM/ups"); + +class UpsSubscriptionModel extends Model { +}; + +UpsSubscriptionModel.init({ + aeTitle: { + type: DataTypes.TEXT + }, + subscribed: { + type: DataTypes.INTEGER, + defaultValue: SUBSCRIPTION_STATE.NOT_SUBSCRIBED + }, + isDeletionLock: { + type: DataTypes.BOOLEAN, + defaultValue: false + } +}, { + sequelize: sequelizeInstance, + modelName: "UpsSubscription", + tableName: "UpsSubscription", + freezeTableName: true +}); + +module.exports.UpsSubscriptionModel = UpsSubscriptionModel; diff --git a/models/sql/models/verifyingObserverSQ.model.js b/models/sql/models/verifyingObserverSQ.model.js new file mode 100644 index 00000000..c6e29124 --- /dev/null +++ b/models/sql/models/verifyingObserverSQ.model.js @@ -0,0 +1,34 @@ +/** + * This Tag(Verifying Observer SQ, 0040,A073) only used in SR Document + */ +const { Model, DataTypes } = require("sequelize"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const sequelizeInstance = require("../instance"); + +class VerifyIngObserverSqModel extends Model {} + +VerifyIngObserverSqModel.init({ + "x0040A027": { + // Verifying Organization + type: vrTypeMapping.LO + }, + "x0040A030": { + // Verification DateTime + type: vrTypeMapping.DT + }, + "x0040A075": { + // Verifying Observer Name + type: vrTypeMapping.PN + }, + "x0040A088": { + // Verifying Observer Identification Code Sequence (foreign key) + type: DataTypes.INTEGER + } +}, { + sequelize: sequelizeInstance, + modelName: "VerifyingObserverSQ", + tableName: "VerifyingObserverSQ", + freezeTableName: true +}); + +module.exports.VerifyIngObserverSqModel = VerifyIngObserverSqModel; \ No newline at end of file diff --git a/models/sql/models/workitems.model.js b/models/sql/models/workitems.model.js new file mode 100644 index 00000000..c462b0a8 --- /dev/null +++ b/models/sql/models/workitems.model.js @@ -0,0 +1,204 @@ +const { Sequelize, DataTypes, Model } = require("sequelize"); +const sequelizeInstance = require("@models/sql/instance"); +const { vrTypeMapping } = require("../vrTypeMapping"); +const { raccoonConfig } = require("@root/config-class"); +const { SUBSCRIPTION_STATE } = require("@models/DICOM/ups"); +const { UpsQueryBuilder } = require("@root/api-sql/dicom-web/controller/UPS-RS/service/query/upsQueryBuilder"); +const { DicomJsonModel } = require("../dicom-json-model"); +const { DicomWebServiceError, DicomWebStatusCodes } = require("@error/dicom-web-service"); +const { merge } = require("lodash"); + +let Common; +if (raccoonConfig.dicomDimseConfig.enableDimse) { + require("@models/DICOM/dcm4che/java-instance"); + Common = require("@java-wrapper/org/github/chinlinlee/dcm777/net/common/Common").Common; +} + +class WorkItemModel extends Model { + + + async getAttributes() { + let obj = this.toJSON(); + let jsonStr = JSON.stringify(obj.json); + return await Common.getAttributesFromJsonString(jsonStr); + } + + toDicomJsonModel() { + return new DicomJsonModel(this.json); + } + + /** + * + * @param {DicomJsonModel} changedStateWorkItemDicomJsonModel + */ + async changeWorkItemState(changedStateWorkItemDicomJsonModel) { + let changedWorkItemJson = merge(this.json, changedStateWorkItemDicomJsonModel.dicomJson); + this.transactionUID = changedStateWorkItemDicomJsonModel.getString("00081195"); + this.x00741000 = changedStateWorkItemDicomJsonModel.getString("00741000"); + this.json = { + ...this.json, + ...changedWorkItemJson + }; + // Let sequelize know json is changed + this.changed("json", true); + await this.save(); + } + + static async getDicomJson (queryOptions) { + let queryBuilder = new UpsQueryBuilder(queryOptions); + let q = queryBuilder.build(); + + let upsArray = await WorkItemModel.findAll({ + ...q, + attributes: ["json"], + limit: queryOptions.limit, + offset: queryOptions.skip + }); + + return await Promise.all(upsArray.map(async ups => { + let { json } = ups.toJSON(); + return json; + })); + } + + static async findOneWorkItemDicomJsonModel(upsInstanceUID) { + let workItemObj = await WorkItemModel.findOne({ + where: { + upsInstanceUID: upsInstanceUID + } + }); + if (workItemObj) { + let {json} = workItemObj.toJSON(); + return new DicomJsonModel(json); + } else { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSDoesNotExist, + "The UPS instance not exist", + 404 + ); + } + } + + static async findOneWorkItemByUpsInstanceUID(upsInstanceUID) { + let workItemObj = await WorkItemModel.findOne({ + where: { + upsInstanceUID: upsInstanceUID + } + }); + if (!workItemObj) { + throw new DicomWebServiceError( + DicomWebStatusCodes.UPSDoesNotExist, + "The UPS instance not exist", + 404 + ); + } + return workItemObj; + } +}; + +/** @type { import("sequelize").ModelAttributes } */ +const WorkItemSchema = { + upsInstanceUID: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + patientID: { + type: DataTypes.STRING, + allowNull: false + }, + transactionUID: { + type: DataTypes.STRING + }, + subscribed: { + type: DataTypes.INTEGER, + defaultValue: SUBSCRIPTION_STATE.NOT_SUBSCRIBED + }, + //#region patient level + "x00100020": { + type: vrTypeMapping.LO, + allowNull: false + }, + //#endregion + "x00741200": { + type: vrTypeMapping.CS + }, + "x00404010": { + type: vrTypeMapping.DT + }, + "x00741204": { + type: vrTypeMapping.LO + }, + "x00741202": { + type: vrTypeMapping.LO + }, + "x00404025": { + // DICOM Code + type: DataTypes.INTEGER + }, + "x00404026": { + // DICOM Code + type: DataTypes.INTEGER + }, + "x00404027": { + // DICOM Code + type: DataTypes.INTEGER + }, + "x00404034": { + // DICOM Code + type: DataTypes.INTEGER + }, + "x00404005": { + type: vrTypeMapping.DT + }, + "x00404011": { + type: vrTypeMapping.DT + }, + "x00404018": { + // DICOM Code + type: DataTypes.INTEGER + }, + "x00380010": { + type: vrTypeMapping.LO + }, + "x00380014_x00400031": { + type: vrTypeMapping.UT + }, + "x00380014_x00400032": { + type: vrTypeMapping.UT + }, + "x00380014_x00400033": { + type: vrTypeMapping.CS + }, + "x00741000": { + type: vrTypeMapping.CS + }, + "x00080082": { + type: DataTypes.INTEGER + }, + // #region Scheduled Human Performers Sequence + "x00404009": { + // DICOM Code + type: DataTypes.INTEGER + }, + "x00404036": { + type: vrTypeMapping.LO + }, + "x00404037": { + type: DataTypes.INTEGER + }, + // #endregion + "json": { + type: vrTypeMapping.JSON + } +}; + +WorkItemModel.init(WorkItemSchema, { + sequelize: sequelizeInstance, + modelName: "UPSWorkItem", + tableName: "UPSWorkItem", + freezeTableName: true +}); + +module.exports.WorkItemModel = WorkItemModel; +module.exports.WorkItemSchema = WorkItemSchema; diff --git a/models/sql/po/instance.po.js b/models/sql/po/instance.po.js new file mode 100644 index 00000000..2609e969 --- /dev/null +++ b/models/sql/po/instance.po.js @@ -0,0 +1,512 @@ +const moment = require("moment"); +const _ = require("lodash"); +const { PersonNameModel } = require("../models/personName.model"); +const { InstanceModel } = require("../models/instance.model"); + +const { tagsNeedStore } = require("@models/DICOM/dicom-tags-mapping"); +const { DicomCodeModel } = require("../models/dicomCode.model"); +const { DicomContentSqModel } = require("../models/dicomContentSQ.model"); +const { VerifyIngObserverSqModel } = require("../models/verifyingObserverSQ.model"); + +const INSTANCE_STORE_TAGS = { + "00020010": true, + "00080016": true, + "00080018": true, + "00080022": true, + "00080023": true, + "0008002A": true, + "00080033": true, + "00200013": true, + "0040A043": true, + "0040A073": true, + "0040A491": true, + "0040A493": true, + "0040A730": true, + "00080008": true, + "0040A032": true, + "00081115": true, + "00280008": true, + "00280010": true, + "00280011": true, + "00280100": true, + "0040A370": true, + "0040A375": true, + "0040A504": true, + "0040A525": true, + "00420010": true, + "00420012": true, + "00700080": true, + "00700081": true, + "00700082": true, + "00700083": true, + "00700084": true, + "00080005": true, + "00081190": true, + "00080054": true, + "00080056": true, + ...tagsNeedStore.Patient, + ...tagsNeedStore.Study, + ...tagsNeedStore.Series +}; + +class InstancePersistentObject { + constructor(dicomJson, series) { + this.json = {}; + Object.keys(INSTANCE_STORE_TAGS).forEach(key => { + let value = _.get(dicomJson, key); + value ? _.set(this.json, key, value) : undefined; + }); + this.series = series; + this.instancePath = _.get(dicomJson, "instancePath", ""); + + this.x00020010 = _.get(dicomJson, "00020010.Value.0", undefined); + this.x0020000D = this.series.x0020000D; + this.x0020000E = this.series.x0020000E; + this.x00080018 = _.get(dicomJson, "00080018.Value.0", undefined); + this.x00080016 = _.get(dicomJson, "00080016.Value.0", undefined); + this.x00080022 = _.get(dicomJson, "00080022.Value.0", undefined); + this.x00080023 = _.get(dicomJson, "00080023.Value.0", undefined); + this.x0008002A = _.get(dicomJson, "0008002A.Value.0", undefined); + this.x00080033 = _.get(dicomJson, "00080033.Value.0", undefined); + this.x00200013 = _.get(dicomJson, "00200013.Value.0", undefined); + this.x00280008 = _.get(dicomJson, "00280008.Value.0", undefined); + this.x00281050 = _.get(dicomJson, "00281050.Value", undefined); + this.x00281051 = _.get(dicomJson, "00281051.Value", undefined); + this.x0040A043 = _.get(dicomJson, "0040A043.Value.0", undefined); + this.x0040A073 = _.get(dicomJson, "0040A073.Value.0", undefined); + this.x0040A491 = _.get(dicomJson, "0040A491.Value.0", undefined); + this.x0040A493 = _.get(dicomJson, "0040A493.Value.0", undefined); + this.x0040A730 = _.get(dicomJson, "0040A730.Value.0", undefined); + } + + async createConceptNameCodeSq(instance) { + if (this.x0040A043) { + let nameCodeSq = { + "x00080100": _.get(this.x0040A043, "00080100.Value.0", undefined), + "x00080102": _.get(this.x0040A043, "00080102.Value.0", undefined), + "x00080103": _.get(this.x0040A043, "00080103.Value.0", undefined), + "x00080104": _.get(this.x0040A043, "00080104.Value.0", undefined) + }; + await instance.createDicomCode(nameCodeSq); + } + } + + /** + * + * @param {InstanceModel} instance + */ + async createOrUpdateConceptNameCode(instance) { + let instanceConceptNameCode = await instance.getDicomCode(); + if (this.x0040A043) { + let nameCodeSq = { + "x00080100": _.get(this.x0040A043, "00080100.Value.0", undefined), + "x00080102": _.get(this.x0040A043, "00080102.Value.0", undefined), + "x00080103": _.get(this.x0040A043, "00080103.Value.0", undefined), + "x00080104": _.get(this.x0040A043, "00080104.Value.0", undefined) + }; + if (!instanceConceptNameCode) { + // Create + await instance.createDicomCode(nameCodeSq); + } else { + // Update + await DicomCodeModel.update(nameCodeSq, { + where: { + SOPInstanceUID: instance.dataValues.x00080018 + } + }); + } + } else { + // Delete when no concept name code + await DicomCodeModel.destroy({ + where: { + SOPInstanceUID: instance.dataValues.x00080018 + } + }); + } + } + + async createContentItem(instance) { + if (this.x0040A730) { + let contentItem = { + "x0040A040": _.get(this.x0040A730, "0040A040.Value.0", undefined), + "x0040A010": _.get(this.x0040A730, "0040A010.Value.0", undefined), + "x0040A160": _.get(this.x0040A730, "0040A160.Value.0", undefined) + }; + let nameCodeSq = { + "x00080100": _.get(this.x0040A730, "0040A043.Value.0.00080100.Value.0", undefined), + "x00080102": _.get(this.x0040A730, "0040A043.Value.0.00080102.Value.0", undefined), + "x00080103": _.get(this.x0040A730, "0040A043.Value.0.00080103.Value.0", undefined), + "x00080104": _.get(this.x0040A730, "0040A043.Value.0.00080104.Value.0", undefined) + }; + let conceptCodeSq = { + "x00080100": _.get(this.x0040A730, "0040A168.Value.0.00080100.Value.0", undefined), + "x00080102": _.get(this.x0040A730, "0040A168.Value.0.00080102.Value.0", undefined), + "x00080103": _.get(this.x0040A730, "0040A168.Value.0.00080103.Value.0", undefined), + "x00080104": _.get(this.x0040A730, "0040A168.Value.0.00080104.Value.0", undefined) + }; + + if (Object.values(contentItem).some(v => v)) { + let createdContentItem = await instance.createDicomContentSQ(contentItem); + + if (Object.values(nameCodeSq).some(v => v)) { + await createdContentItem.createConceptNameCode(nameCodeSq); + } + + if (Object.values(conceptCodeSq).some(v => v)) { + await createdContentItem.createConceptCode(conceptCodeSq); + } + } + + } + } + + async createOrUpdateContentItem(instance) { + let contentItemPo = new ContentItemPersistentObject(this.x0040A730, instance); + // Create or Update + await contentItemPo.createOrUpdateContentItem(); + + } + + async createOrUpdateVerifyingObserverSq(instance) { + let verifyingObserverSqPo = new VerifyingObserverSqPersistentObject(this.x0040A073, this.x0040A493, instance); + await verifyingObserverSqPo.createOrUpdate(); + } + + + async createInstance() { + + let item = { + json: this.json, + x00020010: this.x00020010, + x0020000D: this.x0020000D, + x0020000E: this.x0020000E, + x00080018: this.x00080018, + x00080016: this.x00080016, + x00080022: this.x00080022 ? this.x00080022 : undefined, + x00080023: this.x00080023, + x0008002A: this.x0008002A ? moment(this.x0008002A, "YYYYMMDDhhmmss.SSSSSSZZ").toISOString(): undefined, + x00080033: this.x00080033 ? Number(this.x00080033) : undefined, + x00200013: this.x00200013, + x00280008: this.x00280008, + x00281050: this.x00281050 ? this.x00281050.join("\\"): undefined, + x00281051: this.x00281051 ? this.x00281051.join("\\"): undefined, + x0040A073: this.x0040A073, + x0040A491: this.x0040A491, + x0040A493: this.x0040A493, + x0040A730: this.x0040A730, + instancePath: this.instancePath, + deleteStatus: 0 + }; + + let [instance, created] = await InstanceModel.findOrCreate({ + where: { + x0020000D: this.x0020000D, + x0020000E: this.x0020000E, + x00080018: this.x00080018 + }, + defaults: item + }); + + if (created) { + // do nothing + await this.createContentItem(instance); + } else { + await InstanceModel.update(item, { + where: { + x00080018: instance.dataValues.x00080018 + } + }); + } + await this.createOrUpdateConceptNameCode(instance); + await this.createOrUpdateContentItem(instance); + await this.createOrUpdateVerifyingObserverSq(instance); + + return instance; + } + +} + +class ContentItemPersistentObject { + /** + * + * @param {any} contentItem + * @param {InstanceModel} instance + */ + constructor(contentItem, instance) { + this.contentSq = { + "x0040A040": _.get(contentItem, "0040A040.Value.0", undefined), + "x0040A010": _.get(contentItem, "0040A010.Value.0", undefined), + "x0040A160": _.get(contentItem, "0040A160.Value.0", undefined) + }; + this.nameCodeSq = { + "x00080100": _.get(contentItem, "0040A043.Value.0.00080100.Value.0", undefined), + "x00080102": _.get(contentItem, "0040A043.Value.0.00080102.Value.0", undefined), + "x00080103": _.get(contentItem, "0040A043.Value.0.00080103.Value.0", undefined), + "x00080104": _.get(contentItem, "0040A043.Value.0.00080104.Value.0", undefined) + }; + this.conceptCodeSq = { + "x00080100": _.get(contentItem, "0040A168.Value.0.00080100.Value.0", undefined), + "x00080102": _.get(contentItem, "0040A168.Value.0.00080102.Value.0", undefined), + "x00080103": _.get(contentItem, "0040A168.Value.0.00080103.Value.0", undefined), + "x00080104": _.get(contentItem, "0040A168.Value.0.00080104.Value.0", undefined) + }; + this.instance = instance; + } + + async getExistContentItem() { + return await this.instance.getDicomContentSQ(); + } + + async createOrUpdateContentItem() { + if (!await this.getExistContentItem()) { + // Create + await this.createDicomContentSq(); + await this.createConceptNameCodeInContentItem(); + await this.createConceptCodeInContentItem(); + } else { + // Update + await this.updateConceptNameCodeInContentItem(); + await this.updateConceptCodeInContentItem(); + await this.updateDicomContentSq(); + } + } + + async createDicomContentSq() { + if (Object.values(this.contentSq).some(v => v)) { + await this.instance.createDicomContentSQ(this.contentSq); + } + } + + async updateDicomContentSq() { + if (Object.values(this.contentSq).some(v => v)) { + // Update value + await DicomContentSqModel.update(this.contentSq, { + where: { + SOPInstanceUID: this.instance.dataValues.x00080018 + } + }); + } else { + // Remove item because of given item does not exist + await DicomContentSqModel.destroy({ + where: { + SOPInstanceUID: this.instance.dataValues.x00080018 + } + }); + } + } + + async createConceptNameCodeInContentItem() { + if (Object.values(this.nameCodeSq).some(v => v)) { + if (this.createdContentSq) + await this.createdContentSq.createConceptNameCode(this.nameCodeSq); + } + } + + async updateConceptNameCodeInContentItem() { + let contentItemInInstance = await this.getExistContentItem(); + if (Object.values(this.nameCodeSq).some(v => v)) { + if (contentItemInInstance) { + let nameCode = await contentItemInInstance.getConceptNameCode(); + if (nameCode) { + await DicomCodeModel.update(this.nameCodeSq, { + where: { + ConceptNameCodeId: contentItemInInstance.dataValues.id + } + }); + } else { + await contentItemInInstance.createConceptNameCode(this.nameCodeSq); + } + } + } else { + if (contentItemInInstance) { + await DicomCodeModel.destroy({ + where: { + ConceptNameCodeId: contentItemInInstance.dataValues.id + } + }); + } + } + } + + async createConceptCodeInContentItem() { + if (Object.values(this.conceptCodeSq).some(v => v)) { + if (this.createdContentSq) + await this.createdContentSq.createConceptCode(this.conceptCodeSq); + } + } + + async updateConceptCodeInContentItem() { + let contentItemInInstance = await this.getExistContentItem(); + if (Object.values(this.nameCodeSq).some(v => v)) { + if (contentItemInInstance) { + let conceptCode = await contentItemInInstance.getConceptCode(); + if (conceptCode) { + await DicomCodeModel.update(this.conceptCodeSq, { + where: { + ConceptCodeId: contentItemInInstance.dataValues.id + } + }); + } else { + await contentItemInInstance.createConceptCode(this.nameCodeSq); + } + } + } else { + if (contentItemInInstance) { + await DicomCodeModel.destroy({ + where: { + ConceptCodeId: contentItemInInstance.dataValues.id + } + }); + } + } + } +} + +class VerifyingObserverSqPersistentObject { + constructor(verifyingObserverSq, verificationFlag, instance) { + if (verificationFlag === "VERIFIED" && !verifyingObserverSq) { + throw new Error("Verifying observer is required when Verification Flag (0040,A493) is VERIFIED"); + } + this.verifyingObserverSq = verifyingObserverSq; + this.instance = instance; + } + + async getExistItem() { + return await this.instance.getVerifyingObserverSQ(); + } + + async createOrUpdate() { + let verifyingObserverSq = { + "x0040A027": _.get(this.verifyingObserverSq, "0040A027.Value.0", undefined), + "x0040A030": _.get(this.verifyingObserverSq, "0040A030.Value.0", undefined) + }; + + + verifyingObserverSq.x0040A030 = verifyingObserverSq.x0040A030 ? + moment(verifyingObserverSq.x0040A030, "YYYYMMDDhhmmss.SSSSSSZZ").toISOString() : + undefined; + + if (!await this.getExistItem()) { + // create + let name = await this.createName(); + let personNameId = name ? name.dataValues.id : undefined; + + let identificationCode = await this.createIdentificationCode(); + let identificationCodeId = identificationCode ? identificationCode.dataValues.id : undefined; + + await this.instance.createVerifyingObserverSQ({ + ...verifyingObserverSq, + x0040A088: identificationCodeId, + x0040A075: personNameId + }); + } else { + let name = await this.updateName(); + let personNameId = name ? name.dataValues.id : undefined; + + let identificationCode = await this.updateIdentificationCode(); + let identificationCodeId = identificationCode ? identificationCode.dataValues.id : undefined; + + await VerifyIngObserverSqModel.update({ + ...verifyingObserverSq, + x0040A088: identificationCodeId, + x0040A075: personNameId + }, { + where: { + SOPInstanceUID: this.instance.dataValues.x00080018 + } + }); + } + + } + + /** + * create Verifying Observer Name + */ + async createName() { + let nameItem = _.get(this.verifyingObserverSq, "0040A075.Value.0"); + return await PersonNameModel.createPersonName(nameItem); + } + + async updateName() { + let verifyingObserverSq = await this.getExistItem(); + /** @type { PersonNameModel | undefined } */ + let name = await verifyingObserverSq.getPersonName(); + let nameItem = _.get(this.verifyingObserverSq, "0040A075.Value.0"); + + if (name) { + let updatedName = await PersonNameModel.updatePersonNameById(nameItem, name.getDataValue("id")); + if (!updatedName) { + await VerifyIngObserverSqModel.update({ + x0040A075: null + }, { + where: { + SOPInstanceUID: this.instance.dataValues.x00080018 + } + }); + await PersonNameModel.destroy({ + where: { + id: name.dataValues.id + } + }); + } else { + return name; + } + } else { + return await this.createName(); + } + return undefined; + } + + async createIdentificationCode() { + let codeItem = _.get(this.verifyingObserverSq, "0040A088.Value.0"); + if (codeItem && Object.values(codeItem).some(v => v)) { + return await DicomCodeModel.create({ + "x00080100": _.get(codeItem, "00080100.Value.0", undefined), + "x00080102": _.get(codeItem, "00080102.Value.0", undefined), + "x00080103": _.get(codeItem, "00080103.Value.0", undefined), + "x00080104": _.get(codeItem, "00080104.Value.0", undefined) + }); + } + return undefined; + } + + async updateIdentificationCode() { + let verifyingObserverSq = await this.getExistItem(); + let code = await verifyingObserverSq.getDicomCode(); + let newCodeItem = _.get(this.verifyingObserverSq, "0040A088.Value.0"); + + if (code) { + if (newCodeItem && Object.values(newCodeItem).some(v => v)) { + await DicomCodeModel.update({ + "x00080100": _.get(newCodeItem, "00080100.Value.0", undefined), + "x00080102": _.get(newCodeItem, "00080102.Value.0", undefined), + "x00080103": _.get(newCodeItem, "00080103.Value.0", undefined), + "x00080104": _.get(newCodeItem, "00080104.Value.0", undefined) + }, { + where: { + id: code.dataValues.id + } + }); + return code; + } + + // delete when empty code + await VerifyIngObserverSqModel.update({ + x0040A088: null + }, { + where: { + SOPInstanceUID: this.instance.dataValues.x00080018 + } + }); + await DicomCodeModel.destroy({ + where: { + id: code.dataValues.id + } + }); + } else { + return await this.createIdentificationCode(); + } + } +} + +module.exports.InstancePersistentObject = InstancePersistentObject; \ No newline at end of file diff --git a/models/sql/po/mwlItem.po.js b/models/sql/po/mwlItem.po.js new file mode 100644 index 00000000..c377429a --- /dev/null +++ b/models/sql/po/mwlItem.po.js @@ -0,0 +1,164 @@ +const { get, set, cloneDeep, unset } = require("lodash"); +const { PersonNameModel } = require("../models/personName.model"); +const { tagsNeedStore } = require("@models/DICOM/dicom-tags-mapping"); +const { BaseDicomJson } = require("@models/DICOM/dicom-json-model"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { vrValueTransform } = require("./utils"); +const { DicomCodeModel } = require("../models/dicomCode.model"); +const { MwlItemModel } = require("../models/mwlitems.model"); +const { Op } = require("sequelize"); + + +class MwlItemPersistentObject { + constructor(dicomJson, patient) { + + this.json = {}; + this.initJsonProperties(dicomJson); + + this.patient = patient; + + let dicomJsonObj = new BaseDicomJson(dicomJson); + this.study_instance_uid = dicomJsonObj.getValue("0020000D"); + this.accession_number = dicomJsonObj.getValue(dictionary.keyword.AccessionNumber); + this.accno_local_id = dicomJsonObj.getValue(`${dictionary.keyword.IssuerOfAccessionNumberSequence}.Value.0.${dictionary.keyword.LocalNamespaceEntityID}`); + this.accno_universal_id = dicomJsonObj.getValue(`${dictionary.keyword.IssuerOfAccessionNumberSequence}.Value.0.${dictionary.keyword.UniversalEntityID}`); + this.accno_universal_id_type = dicomJsonObj.getValue(`${dictionary.keyword.IssuerOfAccessionNumberSequence}.Value.0.${dictionary.keyword.UniversalEntityIDType}`); + this.requested_procedure_id = dicomJsonObj.getValue(dictionary.keyword.RequestedProcedureID); + this.admission_id = dicomJsonObj.getValue(dictionary.keyword.AdmissionID); + this.issuer_admission_local_id = dicomJsonObj.getValue(`${dictionary.keyword.IssuerOfAdmissionIDSequence}.Value.0.${dictionary.keyword.LocalNamespaceEntityID}`); + this.issuer_admission_universal_id = dicomJsonObj.getValue(`${dictionary.keyword.IssuerOfAdmissionIDSequence}.Value.0.${dictionary.keyword.UniversalEntityID}`); + this.issuer_admission_universal_id_type = dicomJsonObj.getValue(`${dictionary.keyword.IssuerOfAdmissionIDSequence}.Value.0.${dictionary.keyword.UniversalEntityIDType}`); + // #region sps (Scheduled Procedure Step) + this.station_ae_title = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.StationAETitle}`); + this.station_name = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.StationName}`); + this.start_date = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStartDate}`); + this.end_date = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepEndDate}`); + this.start_time = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStartTime}`); + this.end_time = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepEndTime}`); + this.physician_name = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledPerformingPhysicianName}`); + this.procedure_step_location = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepLocation}`); + this.description = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepDescription}`); + this.protocol_code = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProtocolCodeSequence}`); + this.institution_name = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.InstitutionName}`); + this.institution_department_name = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.InstitutionalDepartmentName}`); + this.institution_department_type_code = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.InstitutionalDepartmentTypeCodeSequence}`); + this.institution_code = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.InstitutionCodeSequence}`); + this.sps_id = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepID}`); + this.sps_status = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.ScheduledProcedureStepStatus}`); + this.modality = dicomJsonObj.getValue(`${dictionary.keyword.ScheduledProcedureStepSequence}.Value.0.${dictionary.keyword.Modality}`); + // #endregion + } + + initJsonProperties(dicomJson) { + Object.keys({ + ...tagsNeedStore.MWL, + ...tagsNeedStore.Patient + }).forEach(key => { + let value = get(dicomJson, key); + value ? set(this.json, key, value) : undefined; + }); + } + + async save() { + let mwlItemObj = MwlItemModel.build(this.getPersistentObject()); + mwlItemObj.patient_id = this.patient.x00100020; + let [mwlItem, created] = await MwlItemModel.findOrCreate({ + where: { + [Op.and]: [ + { + sps_id: this.sps_id + }, + { + study_instance_uid: this.study_instance_uid + } + ] + }, + defaults: mwlItemObj.toJSON() + }); + let tempClonedMwlItem = cloneDeep(mwlItem); + + await this.setGeneralCode(mwlItem, dictionary.keyword.InstitutionalDepartmentTypeCodeSequence); + await this.setGeneralCode(mwlItem, dictionary.keyword.InstitutionCodeSequence); + await this.setGeneralCode(mwlItem, dictionary.keyword.ScheduledProtocolCodeSequence); + await this.setPhysicianName(mwlItem); + if (!created) { + await this.removeAllAssociationItems(tempClonedMwlItem); + mwlItem.json = { + ...mwlItem.json, + ...this.json + }; + mwlItem.changed("json", true); + } + await mwlItem.save(); + + return mwlItem; + } + + async setGeneralCode(item, tag) { + if (this[`x${tag}`]) { + let code = await DicomCodeModel.create({ + "x00080100": get(this[`x${tag}`], "00080100.Value.0", undefined), + "x00080102": get(this[`x${tag}`], "00080102.Value.0", undefined), + "x00080103": get(this[`x${tag}`], "00080103.Value.0", undefined), + "x00080104": get(this[`x${tag}`], "00080104.Value.0", undefined) + }); + let keyword = dictionary.tag[tag]; + await item[`set${keyword}`](code); + } + } + + async setPhysicianName(mwlItem) { + if (this.physician_name) { + let nameOfPhysician = await PersonNameModel.createPersonName(this.physician_name); + await mwlItem[`set${dictionary.tag["00400006"]}`](nameOfPhysician); + } + } + + async removeAllAssociationItems(upsWorkItem) { + const associationItemsNames = [ + "InstitutionalDepartmentTypeCodeSequence", + "InstitutionCodeSequence", + "ScheduledProtocolCodeSequence", + "ScheduledPerformingPhysicianName" + ]; + + for (let i = 0; i < associationItemsNames.length; i++) { + let associationItemName = associationItemsNames[i]; + let associationItem = await upsWorkItem[`get${associationItemName}`](); + if (associationItem) { + await associationItem.destroy(); + } + } + } + + getPersistentObject() { + return { + json: this.json, + study_instance_uid: this.study_instance_uid, + accession_number: this.accession_number, + accno_local_id: this.accno_local_id, + accno_universal_id: this.accno_universal_id, + accno_universal_id_type: this.accno_universal_id_type, + requested_procedure_id: this.requested_procedure_id, + admission_id: this.admission_id, + issuer_admission_local_id: this.issuer_admission_local_id, + issuer_admission_universal_id: this.issuer_admission_universal_id, + issuer_admission_universal_id_type: this.issuer_admission_universal_id_type, + station_ae_title: this.station_ae_title, + station_name: this.station_name, + start_date: this.start_date, + end_date: this.end_date, + start_time: vrValueTransform.DT(this.start_time), + end_time: vrValueTransform.DT(this.end_time), + procedure_step_location: this.procedure_step_location, + description: this.description, + institution_name: this.institution_name, + institution_department_name: this.institution_department_name, + sps_id: this.sps_id, + sps_status: this.sps_status, + modality: this.modality + }; + } +} + +module.exports.MwlItemPersistentObject = MwlItemPersistentObject; \ No newline at end of file diff --git a/models/sql/po/patient.po.js b/models/sql/po/patient.po.js new file mode 100644 index 00000000..67412da0 --- /dev/null +++ b/models/sql/po/patient.po.js @@ -0,0 +1,80 @@ +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", undefined); + 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() { + return await PersonNameModel.createPersonName(this.x00100010); + } + + /** + * + * @param {PatientModel} patient + * @returns + */ + async updatePersonName(patient) { + return await PersonNameModel.updatePersonNameById(this.x00100010, patient.getDataValue("x00100010")); + } + + async createPatient() { + + let item = { + 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 + }; + + let [patient, created] = await PatientModel.findOrCreate({ + where: { + x00100020: this.x00100020 + }, + defaults: item + }); + + if (created) { + let personName = await this.createPersonName(); + patient.x00100010 = personName ? personName.id : undefined; + await patient.save(); + } else { + await PatientModel.update(item, { + where: { + id: patient.dataValues.id + } + }); + await this.updatePersonName(patient); + } + + return patient; + } + +} + +module.exports.PatientPersistentObject = PatientPersistentObject; \ No newline at end of file diff --git a/models/sql/po/series.po.js b/models/sql/po/series.po.js new file mode 100644 index 00000000..53b8a127 --- /dev/null +++ b/models/sql/po/series.po.js @@ -0,0 +1,186 @@ +const moment = require("moment"); +const _ = require("lodash"); +const { PersonNameModel } = require("../models/personName.model"); +const { SeriesModel } = require("../models/series.model"); + +const { tagsNeedStore } = require("@models/DICOM/dicom-tags-mapping"); +const sequelize = require("../instance"); +const { SeriesRequestAttributesModel } = require("../models/seriesRequestAttributes.model"); + +class SeriesPersistentObject { + constructor(dicomJson, study) { + + this.json = {}; + Object.keys({ + ...tagsNeedStore.Study, + ...tagsNeedStore.Series + }).forEach(key => { + let value = _.get(dicomJson, key); + value ? _.set(this.json, key, value) : undefined; + }); + this.study = study; + this.seriesPath = _.get(dicomJson, "seriesPath", ""); + + this.x0020000D = this.study.x0020000D; + this.x0020000E = _.get(dicomJson, "0020000E.Value.0", ""); + this.x00080021 = _.get(dicomJson, "00080021.Value.0", undefined); + this.x00080060 = _.get(dicomJson, "00080060.Value.0", ""); + this.x0008103E = _.get(dicomJson, "0008103E.Value.0", ""); + this.x0008103F = _.get(dicomJson, "0008103F.Value", undefined); + this.x00081050 = _.get(dicomJson, "00081050.Value", ""); + this.x00081052 = _.get(dicomJson, "00081052.Value.0", ""); + this.x00081070 = _.get(dicomJson, "00081070.Value", ""); + this.x00081072 = _.get(dicomJson, "00081072.Value", ""); + this.x00081250 = _.get(dicomJson, "00081250.Value", ""); + this.x00200011 = _.get(dicomJson, "00200011.Value.0", ""); + this.x00400244 = _.get(dicomJson, "00400244.Value.0", undefined); + this.x00400245 = _.get(dicomJson, "00400245.Value.0", ""); + this.x00400275 = _.get(dicomJson, "00400275.Value.0", ""); + this.x00080031 = _.get(dicomJson, "00080031.Value.0", ""); + } + + async createReferringPhysicianName() { + return await PersonNameModel.createPersonName(this.x00080090); + } + + async addPerformingPhysicianNames(series) { + let performingPhysicianNames = await PersonNameModel.createPersonNames(series, "x00081050"); + for (let performingPhysicianName of performingPhysicianNames) { + await series.addPerformingPhysicianName(performingPhysicianName); + } + } + + async updatePerformingPhysicianNames(series) { + // The value multiplicity of PerformingPhysicianName is 1-n + // We cannot sure the length of data is changed + // So, destroy all and recreate + for(let personName of series.performingPhysicianName) { + await personName.destroy(); + } + await this.addPerformingPhysicianNames(series); + } + + async addOperatorsNames(series) { + let operationsNames = await PersonNameModel.createPersonNames(series, "x00081070"); + for (let operationsName of operationsNames) { + await series.addOperatorsName(operationsName); + } + } + + async updateOperatorsNames(series) { + for(let personName of series.operatorsName) { + await personName.destroy(); + } + await this.addOperatorsNames(series); + } + + getRequestAttributesInJson() { + if (this.x00400275) { + return { + x0020000E: this.x0020000E, + x00080050: _.get(this.x00400275, "00080050.Value.0"), + x00080051_x00400031: _.get(this.x00400275, "00080051.Value.0.00400031.Value.0"), + x00080051_x00400032: _.get(this.x00400275, "00080051.Value.0.00400032.Value.0"), + x00080051_x00400033: _.get(this.x00400275, "00080051.Value.0.00400033.Value.0"), + x00321033: _.get(this.x00400275, "00321033.Value.0"), + x00401001: _.get(this.x00400275, "00401001.Value.0"), + x0020000D: _.get(this.x00400275, "0020000D.Value.0") + }; + } + return undefined; + } + + /** + * + * @param {SeriesModel} series + */ + async updateRequestAttribute(series) { + let requestAttributes = this.getRequestAttributesInJson(); + if (requestAttributes) { + if (await series.getSeriesRequestAttribute()) { + await SeriesRequestAttributesModel.update(requestAttributes, { + where: { + x0020000E: series.dataValues.x0020000E + } + }); + } else { + await series.createSeriesRequestAttribute(requestAttributes); + } + } else { + await SeriesRequestAttributesModel.destroy({ + where: { + x0020000E: series.dataValues.x0020000E + } + }); + } + } + + async createSeries() { + let item = { + json: this.json, + x0020000D: this.x0020000D, + x0020000E: this.x0020000E, + x00080021: this.x00080021, + x00080060: this.x00080060, + x0008103E: this.x0008103E, + x0008103F: this.x0008103F, + x00081052: this.x00081052, + x00081072: this.x00081072, + x00081250: this.x00081250, + x00200011: this.x00200011, + x00400244: this.x00400244, + x00400245: this.x00400245 ? Number(this.x00400245) : undefined, + x00080031: this.x00080031 ? Number(this.x00080031) : undefined, + seriesPath: this.seriesPath, + deleteStatus: 0 + }; + + let [series, created] = await SeriesModel.findOrCreate({ + where: { + x0020000D: this.x0020000D, + x0020000E: this.x0020000E + }, + defaults: item + }); + + if (created) { + await this.addPerformingPhysicianNames(series); + await this.addOperatorsNames(series); + let requestAttributes = this.getRequestAttributesInJson(); + if (requestAttributes) { + await series.createSeriesRequestAttribute(requestAttributes); + } + + await series.save(); + } else { + await SeriesModel.update(item, { + where: { + x0020000E: series.dataValues.x0020000E + } + }); + await this.updateRequestAttribute(series); + + let seriesWithIncludeItem = await SeriesModel.findByPk(series.dataValues.x0020000E, { + include: [ + { + model: sequelize.model("PersonName"), + as: "performingPhysicianName", + attributes: ["id"] + }, + { + model: sequelize.model("PersonName"), + as: "operatorsName", + attributes: ["id"] + } + ], + attributes: ["x0020000E"] + }); + await this.updatePerformingPhysicianNames(seriesWithIncludeItem); + } + + return series; + } + +} + +module.exports.SeriesPersistentObject = SeriesPersistentObject; \ No newline at end of file diff --git a/models/sql/po/study.po.js b/models/sql/po/study.po.js new file mode 100644 index 00000000..ef9f050a --- /dev/null +++ b/models/sql/po/study.po.js @@ -0,0 +1,84 @@ +const moment = require("moment"); +const _ = require("lodash"); +const { PersonNameModel } = require("../models/personName.model"); +const { StudyModel } = require("../models/study.model"); +const { tagsNeedStore } = require("@models/DICOM/dicom-tags-mapping"); + + + +class StudyPersistentObject { + constructor(dicomJson, patient) { + + this.json = {}; + Object.keys(tagsNeedStore.Study).forEach(key => { + let value = _.get(dicomJson, key); + value ? _.set(this.json, key, value) : undefined; + }); + this.patient = patient; + this.studyPath = _.get(dicomJson, "studyPath", ""); + + this.x00080005 = _.get(dicomJson, "00080005.Value", undefined); + this.x00080020 = _.get(dicomJson, "00080020.Value.0", ""); + this.x00080030 = _.get(dicomJson, "00080030.Value.0", ""); + this.x00080050 = _.get(dicomJson, "00080050.Value.0", ""); + this.x00080056 = _.get(dicomJson, "00080056.Value.0", ""); + this.x00080090 = _.get(dicomJson, "00080090.Value.0", ""); + this.x00080201 = _.get(dicomJson, "00080201.Value.0", ""); + this.x0020000D = _.get(dicomJson, "0020000D.Value.0", ""); + this.x00200010 = _.get(dicomJson, "00200010.Value.0", ""); + this.x00201206 = _.get(dicomJson, "00201206.Value.0", ""); + this.x00201208 = _.get(dicomJson, "00201208.Value.0", ""); + } + + async createReferringPhysicianName() { + return await PersonNameModel.createPersonName(this.x00080090); + } + + async updateReferringPhysicianName(study) { + return await PersonNameModel.updatePersonNameById(this.x00080090, study.getDataValue("x00080090")); + } + + async createStudy() { + let item = { + json: this.json, + x00100020: this.patient.x00100020, + x00080005 : this.x00080005, + x00080020 : this.x00080020, + x00080030 : this.x00080030 ? Number(this.x00080030) : undefined, + x00080050 : this.x00080050, + x00080056 : this.x00080056, + x00080201 : this.x00080201, + x0020000D : this.x0020000D, + x00200010 : this.x00200010, + x00201206 : this.x00201206, + x00201208 : this.x00201208, + studyPath: this.studyPath, + deleteStatus: 0 + }; + + let [study, created] = await StudyModel.findOrCreate({ + where: { + x0020000D: this.x0020000D + }, + defaults: item + }); + + if (created) { + let referringPhysicianName = await this.createReferringPhysicianName(); + study.x00080090 = referringPhysicianName ? referringPhysicianName.id : undefined; + await study.save(); + } else { + await StudyModel.update(item, { + where: { + x0020000D: study.dataValues.x0020000D + } + }); + await this.updateReferringPhysicianName(study); + } + + return study; + } + +} + +module.exports.StudyPersistentObject = StudyPersistentObject; \ No newline at end of file diff --git a/models/sql/po/upsWorkItem.po.js b/models/sql/po/upsWorkItem.po.js new file mode 100644 index 00000000..928f268f --- /dev/null +++ b/models/sql/po/upsWorkItem.po.js @@ -0,0 +1,222 @@ +const { get, set, cloneDeep, unset } = require("lodash"); +const { PersonNameModel } = require("../models/personName.model"); +const { tagsNeedStore } = require("@models/DICOM/dicom-tags-mapping"); +const { BaseDicomJson } = require("@models/DICOM/dicom-json-model"); +const { dictionary } = require("@models/DICOM/dicom-tags-dic"); +const { vrValueTransform } = require("./utils"); +const { WorkItemModel } = require("../models/workitems.model"); +const { DicomCodeModel } = require("../models/dicomCode.model"); +const { UpsRequestAttributesModel } = require("../models/upsRequestAttributes.model"); +const { UpdateWorkItemService } = require("@root/api/dicom-web/controller/UPS-RS/service/update-workItem.service"); + + +class UpsWorkItemPersistentObject { + constructor(dicomJson, patient) { + + this.json = {}; + this.initJsonProperties(dicomJson); + + this.patient = patient; + + let dicomJsonObj = new BaseDicomJson(dicomJson); + this.upsInstanceUID = get(dicomJson, "upsInstanceUID", ""); + this.patientID = get(dicomJson, "patientID", ""); + this.transactionUID = get(dicomJson, "transactionUID", ""); + + this.x00100020 = dicomJsonObj.getValue("00100020"); + this.x00080018 = dicomJsonObj.getValue("00080018"); + this.x00741200 = dicomJsonObj.getValue("00741200"); + this.x00404010 = dicomJsonObj.getValue("00404010"); + this.x00741204 = dicomJsonObj.getValue("00741204"); + this.x00741202 = dicomJsonObj.getValue("00741202"); + this.x00404025 = dicomJsonObj.getValue("00404025"); + this.x00404026 = dicomJsonObj.getValue("00404026"); + this.x00404027 = dicomJsonObj.getValue("00404027"); + this.x00404005 = dicomJsonObj.getValue("00404005"); + let scheduledHumanPerformerSequence = new BaseDicomJson(dicomJsonObj.getValue("00404034")); + this.x00404009 = scheduledHumanPerformerSequence.getValue("00404009"); + this.x00404037 = scheduledHumanPerformerSequence.getValue("00404037"); + this.x00404036 = scheduledHumanPerformerSequence.getValue("00404036"); + this.x00404011 = dicomJsonObj.getValue("00404011"); + this.x00400418 = dicomJsonObj.getValue("00400418"); + this.x00380010 = dicomJsonObj.getValue("00380010"); + this.x00741000 = dicomJsonObj.getValue("00741000"); + this.x00080082 = dicomJsonObj.getValue("00080082"); + + // issuer of Admission ID + let issuerOfAdmissionIdSequence = new BaseDicomJson(dicomJsonObj.getValue("00380014")); + this.admissionLocalEntityId = issuerOfAdmissionIdSequence.getValue("00400031"); + this.admissionUniversalEntityId = issuerOfAdmissionIdSequence.getValue("00400032"); + this.admissionUniversalEntityIdType = issuerOfAdmissionIdSequence.getValue("00400033"); + } + + initJsonProperties(dicomJson) { + Object.keys({ + ...tagsNeedStore.UPS, + ...tagsNeedStore.Patient + }).forEach(key => { + let value = get(dicomJson, key); + value ? set(this.json, key, value) : undefined; + }); + } + + async save() { + let upsWorkItemObj = WorkItemModel.build(this.getPersistentObject()); + let [upsWorkItem, created] = await WorkItemModel.findOrCreate({ + where: { + upsInstanceUID: this.upsInstanceUID + }, + defaults: upsWorkItemObj.toJSON() + }); + let tempUpsWorkItem = cloneDeep(upsWorkItem); + + + await upsWorkItem.setPatient(this.patient); + await this.setGeneralCode(upsWorkItem, dictionary.keyword.ScheduledStationNameCodeSequence); + await this.setGeneralCode(upsWorkItem, dictionary.keyword.ScheduledStationClassCodeSequence); + await this.setGeneralCode(upsWorkItem, dictionary.keyword.ScheduledStationGeographicLocationCodeSequence); + await this.setGeneralCode(upsWorkItem, dictionary.keyword.HumanPerformerCodeSequence); + await this.setGeneralCode(upsWorkItem, dictionary.keyword.ScheduledWorkitemCodeSequence); + await this.setGeneralCode(upsWorkItem, dictionary.keyword.InstitutionCodeSequence); + await this.setHumanPerformerName(upsWorkItem); + + let requestAttributeDAO = new UpsWorkItemRequestAttributeDAO(this.upsInstanceUID, this.json); + await requestAttributeDAO.update(upsWorkItem); + + if (!created) { + await this.removeAllAssociationItems(tempUpsWorkItem); + this.adjustUpdateWorkItem(); + upsWorkItem.json = { + ...upsWorkItem.json, + ...this.json + }; + upsWorkItem.changed("json", true); + } + await upsWorkItem.save(); + + return upsWorkItem; + } + + async setGeneralCode(item, tag) { + if (this[`x${tag}`]) { + let code = await DicomCodeModel.create({ + "x00080100": get(this[`x${tag}`], "00080100.Value.0", undefined), + "x00080102": get(this[`x${tag}`], "00080102.Value.0", undefined), + "x00080103": get(this[`x${tag}`], "00080103.Value.0", undefined), + "x00080104": get(this[`x${tag}`], "00080104.Value.0", undefined) + }); + let keyword = dictionary.tag[tag]; + await item[`set${keyword}`](code); + } + } + + async setHumanPerformerName(upsWorkItem) { + if (this.x00404037) { + let nameOfHumanPerformer = await PersonNameModel.createPersonName(this.x00404037); + await upsWorkItem[`set${dictionary.tag["00404037"]}`](nameOfHumanPerformer); + } + } + + async removeAllAssociationItems(upsWorkItem) { + const associationItemsNames = [ + "ScheduledStationNameCodeSequence", + "ScheduledStationClassCodeSequence", + "ScheduledStationGeographicLocationCodeSequence", + "HumanPerformerCodeSequence", + "ScheduledWorkitemCodeSequence", + "InstitutionCodeSequence", + "HumanPerformerName" + ]; + + for (let i = 0 ; i < associationItemsNames.length ; i++) { + let associationItemName = associationItemsNames[i]; + let associationItem = await upsWorkItem[`get${associationItemName}`](); + if (associationItem) { + await associationItem.destroy(); + } + } + } + + getPersistentObject() { + return { + json: this.json, + upsInstanceUID : this.upsInstanceUID, + patientID : this.patientID, + transactionUID : this.transactionUID, + x00100020: this.x00100020, + x00741200: this.x00741200, + x00404010: vrValueTransform.DT(this.x00404010), + x00741204: this.x00741204, + x00741202: this.x00741202, + x00404036: this.x00404036, + x00404005: vrValueTransform.DT(this.x00404005), + x00404011: vrValueTransform.DT(this.x00404011), + x00380010: this.x00380010, + x00741000: this.x00741000, + x00380014_x00400031: this.admissionLocalEntityId, + x00380014_x00400032: this.admissionUniversalEntityId, + x00380014_x00400033: this.admissionUniversalEntityIdType + }; + } + + adjustUpdateWorkItem() { + for (let i = 0; i < UpdateWorkItemService.notAllowedAttributes.length; i++) { + let notAllowedAttr = UpdateWorkItemService.notAllowedAttributes[i]; + unset(this.json, notAllowedAttr); + } + } +} + +class UpsWorkItemRequestAttributeDAO { + constructor(upsInstanceUID, dicomJson) { + this.dicomJson = dicomJson; + this.dicomJsonObj = new BaseDicomJson(this.dicomJson); + this.upsInstanceUID = upsInstanceUID; + } + + async getRequestAttribute() { + let requestAttribute = this.dicomJsonObj.getValue(dictionary.keyword.ReferencedRequestSequence); + if (requestAttribute) { + return { + upsInstanceUID: this.upsInstanceUID, + x00080050: get(requestAttribute, "00080050.Value.0"), + x00080051_x00400031: get(requestAttribute, "00080051.Value.0.00400031.Value.0"), + x00080051_x00400032: get(requestAttribute, "00080051.Value.0.00400032.Value.0"), + x00080051_x00400033: get(requestAttribute, "00080051.Value.0.00400033.Value.0"), + x00321033: get(requestAttribute, "00321033.Value.0"), + x00401001: get(requestAttribute, "00401001.Value.0"), + x0020000D: get(requestAttribute, "0020000D.Value.0") + }; + } + + return undefined; + } + + + /** + * + * @param {WorkItemModel} workItem + */ + async update(workItem) { + let requestAttributes = await this.getRequestAttribute(); + if (requestAttributes) { + if (await workItem.getUpsRequestAttributesModel()) { + await UpsRequestAttributesModel.update(requestAttributes, { + where: { + upsInstanceUID: workItem.upsInstanceUID + } + }); + } else { + await workItem.createUpsRequestAttributesModel(requestAttributes); + } + } else { + await UpsRequestAttributesModel.destroy({ + where: { + upsInstanceUID: workItem.upsInstanceUID + } + }); + } + } +} + +module.exports.UpsWorkItemPersistentObject = UpsWorkItemPersistentObject; \ No newline at end of file diff --git a/models/sql/po/utils.js b/models/sql/po/utils.js new file mode 100644 index 00000000..0edd878a --- /dev/null +++ b/models/sql/po/utils.js @@ -0,0 +1,7 @@ +const moment = require("moment"); + +const vrValueTransform = { + "DT": (v) => v ? moment(v, "YYYYMMDDhhmmss.SSSSSSZZ").toISOString(): undefined +}; + +module.exports.vrValueTransform = vrValueTransform; \ No newline at end of file diff --git a/models/sql/vrTypeMapping.js b/models/sql/vrTypeMapping.js new file mode 100644 index 00000000..9ecd7349 --- /dev/null +++ b/models/sql/vrTypeMapping.js @@ -0,0 +1,40 @@ +const { raccoonConfig } = require("@root/config-class"); +const { DataTypes } = require("sequelize"); + +const vrTypeMapping = { + "AE": DataTypes.STRING, + "AS": DataTypes.STRING, + "CS": DataTypes.STRING, + "DA": DataTypes.DATEONLY, + "DS": DataTypes.STRING, + "DT": DataTypes.DATE, + "FT": DataTypes.FLOAT, + "FD": DataTypes.DOUBLE, + "IS": DataTypes.STRING, + "LO": DataTypes.STRING, + "LT": DataTypes.STRING(10240+1), + "OB": DataTypes.TEXT, + "OD": DataTypes.TEXT, + "OF": DataTypes.TEXT, + "OL": DataTypes.TEXT, + "OV": DataTypes.TEXT, + "OW": DataTypes.TEXT, + "PN": DataTypes.INTEGER, // foreign key + "SH": DataTypes.STRING, + "SL": DataTypes.INTEGER, + "SS": DataTypes.SMALLINT, + "ST": DataTypes.STRING(1024+1), + "SV": DataTypes.BIGINT, + "TM": DataTypes.DECIMAL, + "UC": DataTypes.TEXT("long"), + "UI": DataTypes.STRING, + "UL": DataTypes.INTEGER.UNSIGNED, + "UR": DataTypes.TEXT("long"), + "US": DataTypes.SMALLINT.UNSIGNED, + "UT": DataTypes.TEXT("long"), + "UV": DataTypes.BIGINT.UNSIGNED, + "JSON": raccoonConfig.dbConfig.dialect === "postgres" ? DataTypes.JSONB : DataTypes.JSON // For Array or SQ data +}; + + +module.exports.vrTypeMapping = vrTypeMapping; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cd354ef9..f41e40f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "raccoon-only-dicom", - "version": "1.0.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "raccoon-only-dicom", - "version": "1.0.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "@jorgeferrero/stream-to-buffer": "^2.0.6", @@ -18,6 +18,7 @@ "commander": "^10.0.1", "compression": "^1.7.4", "connect-mongo": "^4.6.0", + "connect-session-sequelize": "^7.1.7", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dicom-parser": "^1.8.13", @@ -44,10 +45,13 @@ "passport": "^0.6.0", "passport-local": "^1.0.0", "path-match": "^1.2.4", + "pg": "^8.11.1", + "pg-hstore": "^2.3.4", "regexparam": "^2.0.1", "request-compose": "^2.1.6", "request-multipart": "^1.0.0", "run-script-os": "^1.1.6", + "sequelize": "^6.32.1", "sharp": "^0.30.4", "shorthash2": "^1.0.3", "uuid": "^9.0.0", @@ -58,6 +62,7 @@ "eslint-config-prettier": "^8.5.0", "mocha": "^10.2.0", "mongodb-memory-server": "^8.12.2", + "sequelize-erd": "^1.3.1", "standard-version": "^9.5.0", "swagger-jsdoc": "^6.2.8" } @@ -831,6 +836,95 @@ "node": ">=6.9.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jorgeferrero/stream-to-buffer": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@jorgeferrero/stream-to-buffer/-/stream-to-buffer-2.0.6.tgz", @@ -1411,6 +1505,14 @@ "node": ">=14.0.0" } }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -1423,6 +1525,11 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "node_modules/@types/node": { "version": "17.0.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", @@ -1434,6 +1541,11 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.7.17", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", + "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -1945,6 +2057,14 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g==" }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", @@ -2587,6 +2707,20 @@ "mongodb": "^4.1.0" } }, + "node_modules/connect-session-sequelize": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-7.1.7.tgz", + "integrity": "sha512-Wqq7rg0w+9bOVs6jC0nhZnssXJ3+iKNlDVWn2JfBuBPoY7oYaxzxfBKeUYrX6dHt3OWEWbZV6LJvapwi76iBQQ==", + "dependencies": { + "debug": "^4.1.1" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "sequelize": ">= 6.1.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -3271,6 +3405,11 @@ "node": ">=4" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -3306,6 +3445,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3926,9 +4070,9 @@ } }, "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz", - "integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "engines": { "node": ">=14" }, @@ -4564,6 +4708,14 @@ "node": ">=8" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4722,11 +4874,11 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/jackspeak": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.0.3.tgz", - "integrity": "sha512-0Jud3OMUdMbrlr3PyUMKESq51LXVAB+a239Ywdvd+Kgxj3MaBRml/nVRxf8tQFyfthMjuRkxkv7Vg58pmIMfuQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.0.tgz", + "integrity": "sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg==", "dependencies": { - "cliui": "^7.0.4" + "@isaacs/cliui": "^8.0.2" }, "engines": { "node": ">=14" @@ -4738,55 +4890,29 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jackspeak/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/jackspeak/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/java-bridge": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge/-/java-bridge-2.3.0.tgz", - "integrity": "sha512-qQqooQMY+dyWKbQp67pfc1WZncVNSZFYKf7RQOY2tWYQNBrA4xGsQ8G5VbffXcuW61/Ezja/XrbiHpd3a22Oaw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge/-/java-bridge-2.4.0.tgz", + "integrity": "sha512-KxJCs1DnmxPVol8N6+N7y89u9wfLG5oSkm0xoBBJ7CLEhuuTc/1hiFITjrEEub66AwNZDr2byYB5N2lbUQhrkg==", "dependencies": { - "glob": "^10.0.0" + "glob": "^10.3.3" }, "engines": { "node": ">= 15" }, "optionalDependencies": { - "java-bridge-darwin-arm64": "2.3.0", - "java-bridge-darwin-x64": "2.3.0", - "java-bridge-linux-arm64-gnu": "2.3.0", - "java-bridge-linux-x64-gnu": "2.3.0", - "java-bridge-win32-ia32-msvc": "2.3.0", - "java-bridge-win32-x64-msvc": "2.3.0" + "java-bridge-darwin-arm64": "2.4.0", + "java-bridge-darwin-x64": "2.4.0", + "java-bridge-linux-arm64-gnu": "2.4.0", + "java-bridge-linux-x64-gnu": "2.4.0", + "java-bridge-win32-ia32-msvc": "2.4.0", + "java-bridge-win32-x64-msvc": "2.4.0" } }, "node_modules/java-bridge-darwin-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-darwin-arm64/-/java-bridge-darwin-arm64-2.3.0.tgz", - "integrity": "sha512-jIEly/4h0zZtRv1KA95kg8H4RVp7KUa+rn6bpLupfB/9uVtcM8uf1bFurvZRkGM41JYbM+y7KcotoKTFUgUKXA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-darwin-arm64/-/java-bridge-darwin-arm64-2.4.0.tgz", + "integrity": "sha512-dIFMV6w+lXnOU6xaeOmKsUOloIAzm577mkmi7Cim3Lue2nLZUFhDbwLiHTcnUTbq5+d7Qb8JbkdNTE2AjaPLmg==", "cpu": [ "arm64" ], @@ -4799,9 +4925,9 @@ } }, "node_modules/java-bridge-darwin-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-darwin-x64/-/java-bridge-darwin-x64-2.3.0.tgz", - "integrity": "sha512-ukhU321HSsofgEfnLm9QUbG09U3shwxulRVqIpKIalB+GB+HRF1Op8FzyRJOkA0Jzc0O36uq7sVQ7HBGEtF76A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-darwin-x64/-/java-bridge-darwin-x64-2.4.0.tgz", + "integrity": "sha512-2Fj9DUynyP6Wt6ceAmeCgkSaUkW20bOtRN/0JTqQe2wwFVGafFxzUjWUYygHlGG26Gtik3kJzic+c7Sxr1lFUQ==", "cpu": [ "x64" ], @@ -4814,9 +4940,9 @@ } }, "node_modules/java-bridge-linux-arm64-gnu": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-linux-arm64-gnu/-/java-bridge-linux-arm64-gnu-2.3.0.tgz", - "integrity": "sha512-ITV+7aFle1ucNhoREOGpem/ez8sCjePKsd+GC/JQ4wBLqNuPhfbvu7qCSHJiB9c/FeRUiLTh9NaBfHyuip92ng==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-linux-arm64-gnu/-/java-bridge-linux-arm64-gnu-2.4.0.tgz", + "integrity": "sha512-5IyZ5t6JDxEgjsOazhS+ZyBFjpSKN2agNFuCuW+R8wQmZMj459kXhsIZEHujtp7MobmMXc1dTcwaV8n3mqaR1w==", "cpu": [ "arm64" ], @@ -4829,9 +4955,9 @@ } }, "node_modules/java-bridge-linux-x64-gnu": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-linux-x64-gnu/-/java-bridge-linux-x64-gnu-2.3.0.tgz", - "integrity": "sha512-Nzk97+9sOR4gMJJPYZd/o9OgkkhuWW5BaxSOmBxAhyZk4O6hha5T2w3QtvrqcfkbpyOY8yeaZdGq3k0n86ALnA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-linux-x64-gnu/-/java-bridge-linux-x64-gnu-2.4.0.tgz", + "integrity": "sha512-9V6kRmEbX1FFGqzRWr+NK9jxf9otiw9wCuLBoLBj/iErAjQpia6l061Z7vydt5c4Yu/lk107oBUo/Hsbtc/syQ==", "cpu": [ "x64" ], @@ -4844,9 +4970,9 @@ } }, "node_modules/java-bridge-win32-ia32-msvc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-win32-ia32-msvc/-/java-bridge-win32-ia32-msvc-2.3.0.tgz", - "integrity": "sha512-MG4R1OSiCq20a1a/w4Hf5NtnNIulHhc0DnqomS/s09nH+xcVcDV9eQQ12pa7FoCZtK7nkVRwV/xhLQ8wtcK6zg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-win32-ia32-msvc/-/java-bridge-win32-ia32-msvc-2.4.0.tgz", + "integrity": "sha512-D+Im+AAiPgbT0BVWXbuSVQcbek4+VK6SHUVCXF4Gi02Yew/SJ+aNgIIkjP0TMVeYrxG1AlwcYQ80ahuzOm1acA==", "cpu": [ "ia32" ], @@ -4859,9 +4985,9 @@ } }, "node_modules/java-bridge-win32-x64-msvc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-win32-x64-msvc/-/java-bridge-win32-x64-msvc-2.3.0.tgz", - "integrity": "sha512-dDDD/+plvden+VHA2Zr5DebGDFnO15wpp+Udhn/XZjUBmfDto03BI814IsfTziDv0h/GDZIXVxvAnjtuQyBkGw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-win32-x64-msvc/-/java-bridge-win32-x64-msvc-2.4.0.tgz", + "integrity": "sha512-gK43rdXS6w7pNkz7DDHFnpj4A9v5yu5HPdFU4PankuQzlvLC5r4/S1hBXR4jvI5+md9c8LnvLaxLQwQ+OUfE8A==", "cpu": [ "x64" ], @@ -4882,16 +5008,15 @@ } }, "node_modules/java-bridge/node_modules/glob": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.1.tgz", - "integrity": "sha512-ngom3wq2UhjdbmRE/krgkD8BQyi1KZ5l+D2dVm4+Yj+jJIBp74/ZGunL6gNGc/CYuQmvUBiavWEXIotRiv5R6A==", + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", "dependencies": { "foreground-child": "^3.1.0", - "fs.realpath": "^1.0.0", "jackspeak": "^2.0.3", - "minimatch": "^9.0.0", - "minipass": "^5.0.0", - "path-scurry": "^1.7.0" + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" @@ -4904,9 +5029,9 @@ } }, "node_modules/java-bridge/node_modules/minimatch": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", - "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4918,11 +5043,11 @@ } }, "node_modules/java-bridge/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/joi": { @@ -6420,6 +6545,11 @@ "node": ">=6" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6569,12 +6699,12 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz", - "integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dependencies": { - "lru-cache": "^9.0.0", - "minipass": "^5.0.0" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6584,19 +6714,19 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.0.tgz", - "integrity": "sha512-qFXQEwchrZcMVen2uIDceR8Tii6kCJak5rzDStfEM0qA3YLMswaxIEZO0DhIbJ3aqaJiDjt+3crlplOb0tDtKQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", "engines": { "node": "14 || >=16.14" } }, "node_modules/path-scurry/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/path-to-regexp": { @@ -6645,6 +6775,108 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "node_modules/pg": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz", + "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.1", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgpass/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -6678,6 +6910,41 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", @@ -7231,6 +7498,11 @@ "node": ">=4" } }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, "node_modules/rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -7350,6 +7622,102 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/sequelize": { + "version": "6.32.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.32.1.tgz", + "integrity": "sha512-3Iv0jruv57Y0YvcxQW7BE56O7DC1BojcfIrqh6my+IQwde+9u/YnuYHzK+8kmZLhLvaziRT1eWu38nh9yVwn/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.4", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.0", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.1", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-erd": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/sequelize-erd/-/sequelize-erd-1.3.1.tgz", + "integrity": "sha512-w5/gNkj0WTp80KMvMxrrTp3HyIn1B8F5XmTXP6PXQNoWi1HKazXe9IvlbmGVP2yxx/YTtX+QWatcwkDk8HLK9Q==", + "dev": true, + "dependencies": { + "commander": "^2.9.0", + "lodash": "^4.17.15" + }, + "bin": { + "sequelize-erd": "bin/generate" + } + }, + "node_modules/sequelize-erd/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -7886,6 +8254,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-package": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", @@ -7904,6 +8286,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -8133,6 +8527,11 @@ "node": ">=0.6" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8249,6 +8648,11 @@ "node": ">= 0.8" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -8367,7 +8771,6 @@ "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", - "dev": true, "engines": { "node": ">= 0.10" } @@ -8424,6 +8827,14 @@ "node": ">= 0.10.0" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8458,6 +8869,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -8530,7 +8958,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "engines": { "node": ">=0.4" } @@ -9405,6 +9832,64 @@ "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@jorgeferrero/stream-to-buffer": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@jorgeferrero/stream-to-buffer/-/stream-to-buffer-2.0.6.tgz", @@ -9879,6 +10364,14 @@ "tslib": "^2.5.0" } }, + "@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "requires": { + "@types/ms": "*" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -9891,6 +10384,11 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "@types/node": { "version": "17.0.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", @@ -9902,6 +10400,11 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "@types/validator": { + "version": "13.7.17", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.17.tgz", + "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==" + }, "@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10309,6 +10812,11 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g==" }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, "buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", @@ -10839,6 +11347,14 @@ "kruptein": "^3.0.0" } }, + "connect-session-sequelize": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-7.1.7.tgz", + "integrity": "sha512-Wqq7rg0w+9bOVs6jC0nhZnssXJ3+iKNlDVWn2JfBuBPoY7oYaxzxfBKeUYrX6dHt3OWEWbZV6LJvapwi76iBQQ==", + "requires": { + "debug": "^4.1.1" + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -11352,6 +11868,11 @@ } } }, + "dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -11389,6 +11910,11 @@ } } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -11868,9 +12394,9 @@ }, "dependencies": { "signal-exit": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz", - "integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" } } }, @@ -12341,6 +12867,11 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -12466,48 +12997,26 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "jackspeak": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.0.3.tgz", - "integrity": "sha512-0Jud3OMUdMbrlr3PyUMKESq51LXVAB+a239Ywdvd+Kgxj3MaBRml/nVRxf8tQFyfthMjuRkxkv7Vg58pmIMfuQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.0.tgz", + "integrity": "sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg==", "requires": { - "@pkgjs/parseargs": "^0.11.0", - "cliui": "^7.0.4" - }, - "dependencies": { - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" } }, "java-bridge": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge/-/java-bridge-2.3.0.tgz", - "integrity": "sha512-qQqooQMY+dyWKbQp67pfc1WZncVNSZFYKf7RQOY2tWYQNBrA4xGsQ8G5VbffXcuW61/Ezja/XrbiHpd3a22Oaw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge/-/java-bridge-2.4.0.tgz", + "integrity": "sha512-KxJCs1DnmxPVol8N6+N7y89u9wfLG5oSkm0xoBBJ7CLEhuuTc/1hiFITjrEEub66AwNZDr2byYB5N2lbUQhrkg==", "requires": { - "glob": "^10.0.0", - "java-bridge-darwin-arm64": "2.3.0", - "java-bridge-darwin-x64": "2.3.0", - "java-bridge-linux-arm64-gnu": "2.3.0", - "java-bridge-linux-x64-gnu": "2.3.0", - "java-bridge-win32-ia32-msvc": "2.3.0", - "java-bridge-win32-x64-msvc": "2.3.0" + "glob": "^10.3.3", + "java-bridge-darwin-arm64": "2.4.0", + "java-bridge-darwin-x64": "2.4.0", + "java-bridge-linux-arm64-gnu": "2.4.0", + "java-bridge-linux-x64-gnu": "2.4.0", + "java-bridge-win32-ia32-msvc": "2.4.0", + "java-bridge-win32-x64-msvc": "2.4.0" }, "dependencies": { "brace-expansion": { @@ -12519,67 +13028,66 @@ } }, "glob": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.1.tgz", - "integrity": "sha512-ngom3wq2UhjdbmRE/krgkD8BQyi1KZ5l+D2dVm4+Yj+jJIBp74/ZGunL6gNGc/CYuQmvUBiavWEXIotRiv5R6A==", + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", + "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", "requires": { "foreground-child": "^3.1.0", - "fs.realpath": "^1.0.0", "jackspeak": "^2.0.3", - "minimatch": "^9.0.0", - "minipass": "^5.0.0", - "path-scurry": "^1.7.0" + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" } }, "minimatch": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", - "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "requires": { "brace-expansion": "^2.0.1" } }, "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==" } } }, "java-bridge-darwin-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-darwin-arm64/-/java-bridge-darwin-arm64-2.3.0.tgz", - "integrity": "sha512-jIEly/4h0zZtRv1KA95kg8H4RVp7KUa+rn6bpLupfB/9uVtcM8uf1bFurvZRkGM41JYbM+y7KcotoKTFUgUKXA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-darwin-arm64/-/java-bridge-darwin-arm64-2.4.0.tgz", + "integrity": "sha512-dIFMV6w+lXnOU6xaeOmKsUOloIAzm577mkmi7Cim3Lue2nLZUFhDbwLiHTcnUTbq5+d7Qb8JbkdNTE2AjaPLmg==", "optional": true }, "java-bridge-darwin-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-darwin-x64/-/java-bridge-darwin-x64-2.3.0.tgz", - "integrity": "sha512-ukhU321HSsofgEfnLm9QUbG09U3shwxulRVqIpKIalB+GB+HRF1Op8FzyRJOkA0Jzc0O36uq7sVQ7HBGEtF76A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-darwin-x64/-/java-bridge-darwin-x64-2.4.0.tgz", + "integrity": "sha512-2Fj9DUynyP6Wt6ceAmeCgkSaUkW20bOtRN/0JTqQe2wwFVGafFxzUjWUYygHlGG26Gtik3kJzic+c7Sxr1lFUQ==", "optional": true }, "java-bridge-linux-arm64-gnu": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-linux-arm64-gnu/-/java-bridge-linux-arm64-gnu-2.3.0.tgz", - "integrity": "sha512-ITV+7aFle1ucNhoREOGpem/ez8sCjePKsd+GC/JQ4wBLqNuPhfbvu7qCSHJiB9c/FeRUiLTh9NaBfHyuip92ng==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-linux-arm64-gnu/-/java-bridge-linux-arm64-gnu-2.4.0.tgz", + "integrity": "sha512-5IyZ5t6JDxEgjsOazhS+ZyBFjpSKN2agNFuCuW+R8wQmZMj459kXhsIZEHujtp7MobmMXc1dTcwaV8n3mqaR1w==", "optional": true }, "java-bridge-linux-x64-gnu": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-linux-x64-gnu/-/java-bridge-linux-x64-gnu-2.3.0.tgz", - "integrity": "sha512-Nzk97+9sOR4gMJJPYZd/o9OgkkhuWW5BaxSOmBxAhyZk4O6hha5T2w3QtvrqcfkbpyOY8yeaZdGq3k0n86ALnA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-linux-x64-gnu/-/java-bridge-linux-x64-gnu-2.4.0.tgz", + "integrity": "sha512-9V6kRmEbX1FFGqzRWr+NK9jxf9otiw9wCuLBoLBj/iErAjQpia6l061Z7vydt5c4Yu/lk107oBUo/Hsbtc/syQ==", "optional": true }, "java-bridge-win32-ia32-msvc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-win32-ia32-msvc/-/java-bridge-win32-ia32-msvc-2.3.0.tgz", - "integrity": "sha512-MG4R1OSiCq20a1a/w4Hf5NtnNIulHhc0DnqomS/s09nH+xcVcDV9eQQ12pa7FoCZtK7nkVRwV/xhLQ8wtcK6zg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-win32-ia32-msvc/-/java-bridge-win32-ia32-msvc-2.4.0.tgz", + "integrity": "sha512-D+Im+AAiPgbT0BVWXbuSVQcbek4+VK6SHUVCXF4Gi02Yew/SJ+aNgIIkjP0TMVeYrxG1AlwcYQ80ahuzOm1acA==", "optional": true }, "java-bridge-win32-x64-msvc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/java-bridge-win32-x64-msvc/-/java-bridge-win32-x64-msvc-2.3.0.tgz", - "integrity": "sha512-dDDD/+plvden+VHA2Zr5DebGDFnO15wpp+Udhn/XZjUBmfDto03BI814IsfTziDv0h/GDZIXVxvAnjtuQyBkGw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/java-bridge-win32-x64-msvc/-/java-bridge-win32-x64-msvc-2.4.0.tgz", + "integrity": "sha512-gK43rdXS6w7pNkz7DDHFnpj4A9v5yu5HPdFU4PankuQzlvLC5r4/S1hBXR4jvI5+md9c8LnvLaxLQwQ+OUfE8A==", "optional": true }, "joi": { @@ -13726,6 +14234,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13840,23 +14353,23 @@ "dev": true }, "path-scurry": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz", - "integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "requires": { - "lru-cache": "^9.0.0", - "minipass": "^5.0.0" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "dependencies": { "lru-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.0.tgz", - "integrity": "sha512-qFXQEwchrZcMVen2uIDceR8Tii6kCJak5rzDStfEM0qA3YLMswaxIEZO0DhIbJ3aqaJiDjt+3crlplOb0tDtKQ==" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==" }, "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==" } } }, @@ -13899,6 +14412,83 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "pg": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz", + "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.1", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "pg-connection-string": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" + }, + "pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "requires": { + "underscore": "^1.13.1" + } + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "requires": {} + }, + "pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + }, + "dependencies": { + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + } + } + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -13920,6 +14510,29 @@ "find-up": "^4.0.0" } }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, "prebuild-install": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", @@ -14356,6 +14969,11 @@ "dev": true, "peer": true }, + "retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" + }, "rfdc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", @@ -14443,6 +15061,59 @@ } } }, + "sequelize": { + "version": "6.32.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.32.1.tgz", + "integrity": "sha512-3Iv0jruv57Y0YvcxQW7BE56O7DC1BojcfIrqh6my+IQwde+9u/YnuYHzK+8kmZLhLvaziRT1eWu38nh9yVwn/g==", + "requires": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.4", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.0", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.1", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "sequelize-erd": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/sequelize-erd/-/sequelize-erd-1.3.1.tgz", + "integrity": "sha512-w5/gNkj0WTp80KMvMxrrTp3HyIn1B8F5XmTXP6PXQNoWi1HKazXe9IvlbmGVP2yxx/YTtX+QWatcwkDk8HLK9Q==", + "dev": true, + "requires": { + "commander": "^2.9.0", + "lodash": "^4.17.15" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==" + }, "serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -14849,6 +15520,16 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "stringify-package": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz", @@ -14863,6 +15544,14 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -15047,6 +15736,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -15130,6 +15824,11 @@ "random-bytes": "~1.0.0" } }, + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -15237,8 +15936,7 @@ "validator": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz", - "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==", - "dev": true + "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==" }, "vary": { "version": "1.1.2", @@ -15280,6 +15978,14 @@ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" }, + "wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "requires": { + "@types/node": "*" + } + }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -15341,6 +16047,16 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -15355,8 +16071,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "3.2.2", diff --git a/package.json b/package.json index 20a14d0e..bfef97f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "raccoon-only-dicom", - "version": "1.0.0", + "version": "1.2.0", "description": "", "main": "index.js", "scripts": { @@ -54,18 +54,6 @@ "keywords": [], "author": "chinlinlee", "license": "MIT", - "_moduleAliases": { - "@dcm4che": "./models/DICOM/dcm4che/wrapper/org/dcm4che3", - "@java-wrapper": "./models/DICOM/dcm4che/wrapper", - "@models": "./models", - "@error": "./error", - "@root": "./", - "@chinlinlee": "./models/DICOM/dcm4che/wrapper/org/github/chinlinlee", - "@dbModels": "./models/mongodb/models", - "@dbInitializer": "./models/mongodb/index.js", - "@dicom-json-model": "./models/DICOM/dicom-json-model.js", - "@query-dicom-json-factory": "./api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory.js" - }, "dependencies": { "@jorgeferrero/stream-to-buffer": "^2.0.6", "archiver": "^5.3.1", @@ -76,6 +64,7 @@ "commander": "^10.0.1", "compression": "^1.7.4", "connect-mongo": "^4.6.0", + "connect-session-sequelize": "^7.1.7", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dicom-parser": "^1.8.13", @@ -102,10 +91,13 @@ "passport": "^0.6.0", "passport-local": "^1.0.0", "path-match": "^1.2.4", + "pg": "^8.11.1", + "pg-hstore": "^2.3.4", "regexparam": "^2.0.1", "request-compose": "^2.1.6", "request-multipart": "^1.0.0", "run-script-os": "^1.1.6", + "sequelize": "^6.32.1", "sharp": "^0.30.4", "shorthash2": "^1.0.3", "uuid": "^9.0.0", @@ -116,6 +108,7 @@ "eslint-config-prettier": "^8.5.0", "mocha": "^10.2.0", "mongodb-memory-server": "^8.12.2", + "sequelize-erd": "^1.3.1", "standard-version": "^9.5.0", "swagger-jsdoc": "^6.2.8" } diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000..3a91ad40 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,10 @@ +# Plugins Configuration +The every plugin(middleware) must have a properties listed below: + +property name | type | description +---------|----------|--------- + enable | boolean | enabled/disable the plugin + before | boolean | Use middleware before/after the router + routers | object[] | Array of routers that you want to add middleware + routers.path | string | The path of the router that you want to add middleware + routers.method | string | The API method of this router \ No newline at end of file diff --git a/routes.js b/routes.js index a994920c..60587fb4 100644 --- a/routes.js +++ b/routes.js @@ -28,6 +28,7 @@ module.exports = function (app) { 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("/dicom-web", require("./api/dicom-web/mwl-rs.route")); app.use("/wado", require("./api/WADO-URI")); }; diff --git a/server.js b/server.js index 0fb6fc1b..cc601547 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,11 @@ RegExp.prototype.toJSON = RegExp.prototype.toString; -require('module-alias')(__dirname); +const { raccoonConfig } = require("./config-class"); + +if (raccoonConfig.serverConfig.dbType === "mongodb") { + require('module-alias')(__dirname + "/config/modula-alias/mongodb"); +} else if (raccoonConfig.serverConfig.dbType === "sql") { + require('module-alias')(__dirname + "/config/modula-alias/sql"); +} const { app, server } = require("./app"); const bodyParser = require("body-parser"); @@ -8,12 +14,30 @@ const cookieParser = require("cookie-parser"); const compress = require("compression"); const cors = require("cors"); const os = require("os"); -const mongoose = require("mongoose"); -const MongoStore = require("connect-mongo"); + +let sessionStore; +let dbInstance; +let sessionStoreOption; +if (raccoonConfig.serverConfig.dbType === "mongodb") { + sessionStore = require("connect-mongo"); + dbInstance = require("mongoose"); + + sessionStoreOption = sessionStore.create({ + client: dbInstance.connection.getClient(), + dbName: raccoonConfig.dbConfig.dbName + }); + +} else if (raccoonConfig.serverConfig.dbType === "sql") { + sessionStore = require("connect-session-sequelize")(session.Store); + dbInstance = require("./models/sql/instance"); + + sessionStoreOption = new sessionStore({ + db: dbInstance + }); +} const passport = require("passport"); -const { raccoonConfig } = require("./config-class"); -const { DcmQrScp } = require('./dimse'); +const { DcmQrScp } = require('@dimse'); require("dotenv"); require("./websocket"); @@ -42,6 +66,7 @@ app.use( //#region session + app.use( session({ secret: raccoonConfig.serverConfig.secretKey || "secretKey", @@ -51,10 +76,7 @@ app.use( httpOnly: true, maxAge: 60 * 60 * 1000 }, - store: MongoStore.create({ - client: mongoose.connection.getClient(), - dbName: raccoonConfig.mongoDbConfig.dbName - }) + store: sessionStoreOption }) ); diff --git a/test/QIDO-RS-Service/common.test.js b/test/QIDO-RS-Service/common.test.js index 3f7e9888..52604525 100644 --- a/test/QIDO-RS-Service/common.test.js +++ b/test/QIDO-RS-Service/common.test.js @@ -3,11 +3,9 @@ const patientModel = require("../../models/mongodb/models/patient.model"); const { DicomJsonModel } = require("../../models/DICOM/dicom-json-model"); const { expect } = require("chai"); const _ = require("lodash"); -const { - convertAllQueryToDICOMTag -} = require("../../api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service"); const { convertRequestQueryToMongoQuery } = require("../../api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory"); const moment = require("moment"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); describe("QIDO-RS Service Common Function", () => { @@ -42,7 +40,7 @@ describe("QIDO-RS Service Common Function", () => { */ it("Should convert `00100010=foobar1234`, VR: `PN` to Mongo query", async ()=> { - let query = convertAllQueryToDICOMTag({ + let query = convertAllQueryToDicomTag({ "00100010": "foobar1234" }); @@ -82,7 +80,7 @@ describe("QIDO-RS Service Common Function", () => { describe("Convert `Date` VR: `DA` query", ()=> { it("Should convert `00100030=19991111` to Mongo query", async ()=> { - let query = convertAllQueryToDICOMTag({ + let query = convertAllQueryToDicomTag({ "00100030": "19991111" }); @@ -109,7 +107,7 @@ describe("QIDO-RS Service Common Function", () => { }); it("Should convert `00100030=19991111-` to Mongo query", async ()=> { - let query = convertAllQueryToDICOMTag({ + let query = convertAllQueryToDicomTag({ "00100030": "19991111-" }); @@ -135,7 +133,7 @@ describe("QIDO-RS Service Common Function", () => { }); it("Should convert `00100030=-19991111` to Mongo query", async ()=> { - let query = convertAllQueryToDICOMTag({ + let query = convertAllQueryToDicomTag({ "00100030": "-19991111" }); @@ -161,7 +159,7 @@ describe("QIDO-RS Service Common Function", () => { }); it("Should convert `00100030=19900101-19991111` to Mongo query", async ()=> { - let query = convertAllQueryToDICOMTag({ + let query = convertAllQueryToDicomTag({ "00100030": "19900101-19991111" }); @@ -191,7 +189,7 @@ describe("QIDO-RS Service Common Function", () => { describe("Convert string `00100020=foobar` VR: `LO`", () => { it("Should convert string completely", async ()=> { - let query = convertAllQueryToDICOMTag({ + let query = convertAllQueryToDicomTag({ "00100020": "foobar" }); diff --git a/test/QIDO-RS-Service/patient.test.js b/test/QIDO-RS-Service/patient.test.js index d2fc319e..0caca936 100644 --- a/test/QIDO-RS-Service/patient.test.js +++ b/test/QIDO-RS-Service/patient.test.js @@ -3,8 +3,8 @@ const patientModel = require("../../models/mongodb/models/patient.model"); const { DicomJsonModel } = require("../../models/DICOM/dicom-json-model"); const { expect } = require("chai"); const _ = require("lodash"); -const { convertAllQueryToDICOMTag } = require("../../api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service"); const { QueryPatientDicomJsonFactory } = require("../../api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); describe("Patient QIDO-RS Service", async () => { let fakePatientData = { @@ -97,7 +97,7 @@ describe("Patient QIDO-RS Service", async () => { let q = { "00100020": "foobar123456" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryPatientDicomJsonFactory({ query: { @@ -114,7 +114,7 @@ describe("Patient QIDO-RS Service", async () => { let q = { "00100020": "foobar123" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryPatientDicomJsonFactory({ query: { @@ -134,7 +134,7 @@ describe("Patient QIDO-RS Service", async () => { let q = { "00100010": "John*" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryPatientDicomJsonFactory({ query: { @@ -151,7 +151,7 @@ describe("Patient QIDO-RS Service", async () => { let q = { "00100010": "John Doe" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryPatientDicomJsonFactory({ query: { diff --git a/test/query.test.js b/test/query.test.js index 062a89fd..6b329aef 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -9,8 +9,8 @@ const dicomStudyModel = require("../models/mongodb/models/study.model"); const dicomSeriesModel = require("../models/mongodb/models/series.model"); const dicomModel = require("../models/mongodb/models/instance.model"); const { expect } = require("chai"); -const { convertAllQueryToDICOMTag } = require("../api/dicom-web/controller/QIDO-RS/service/QIDO-RS.service"); const { QueryStudyDicomJsonFactory, QuerySeriesDicomJsonFactory, QueryInstanceDicomJsonFactory } = require("../api/dicom-web/controller/QIDO-RS/service/query-dicom-json-factory"); +const { convertAllQueryToDicomTag } = require("@root/api/dicom-web/service/base-query.service"); describe("Query DICOM of study, series, and instance level", async () => { @@ -26,7 +26,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "StudyDate": "19990101-19991231" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -45,7 +45,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "StudyDate": "20220101-20221231" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -66,7 +66,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "PatientID": "TCGA-G4-6304" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -89,7 +89,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "StudyDate": "20100101-20101231" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -109,7 +109,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "StudyDate": "19990101-19991231" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -131,7 +131,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "PatientBirthDate": "19590101" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -151,7 +151,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "PatientBirthDate": "19601218" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -173,7 +173,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "AccessionNumber": "4444" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: { @@ -193,7 +193,7 @@ describe("Query DICOM of study, series, and instance level", async () => { "AccessionNumber": "2794663908550664" }; - q = convertAllQueryToDICOMTag(q); + q = convertAllQueryToDicomTag(q); let dicomJsonFactory = new QueryStudyDicomJsonFactory({ query: {