diff --git a/examples/index.html b/examples/index.html index 1b3a9ae..5816bd1 100644 --- a/examples/index.html +++ b/examples/index.html @@ -9,6 +9,8 @@

Example


+ Request hooks +
@@ -17,7 +19,7 @@

- + + + + + diff --git a/karma.conf.js b/karma.conf.js index 9adda23..bea3550 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -11,7 +11,7 @@ module.exports = function(config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha', 'chai'], + frameworks: ['jasmine'], // list of files / patterns to load in the browser diff --git a/package-lock.json b/package-lock.json index fb337ae..7ffe47b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -965,12 +965,6 @@ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", "dev": true }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -1091,12 +1085,6 @@ "fill-range": "^7.0.1" } }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -1155,20 +1143,6 @@ "integrity": "sha512-AHpONWuGFWO8yY9igdXH94tikM6ERS84286r0cAMAXYFtJBk76lhiMhtCxBJNBZsD6hzlvpWZ2AtbVFEkf4JQA==", "dev": true }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", - "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -1186,12 +1160,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "chokidar": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz", @@ -1244,12 +1212,6 @@ "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", "dev": true }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -1382,15 +1344,6 @@ "ms": "2.0.0" } }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1418,12 +1371,6 @@ "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", "dev": true }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2096,12 +2043,6 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -2137,12 +2078,6 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2187,12 +2122,6 @@ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -2443,6 +2372,12 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jasmine-core": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz", + "integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==", + "dev": true + }, "js-levenshtein": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.3.tgz", @@ -2557,21 +2492,13 @@ "which": "^1.2.1" } }, - "karma-mocha": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-1.3.0.tgz", - "integrity": "sha1-7qrH/8DiAetjxGdEDStpx883eL8=", + "karma-jasmine": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", + "integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==", "dev": true, "requires": { - "minimist": "1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } + "jasmine-core": "^3.6.0" } }, "levn": { @@ -2726,36 +2653,6 @@ } } }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - } - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3036,12 +2933,6 @@ "pify": "^2.0.0" } }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -3708,12 +3599,6 @@ "prelude-ls": "~1.1.2" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index 4e1494d..2f082c5 100644 --- a/package.json +++ b/package.json @@ -34,18 +34,17 @@ "devDependencies": { "@babel/core": "^7.1.0", "@babel/preset-env": "^7.1.0", - "chai": "^4.1.2", "eslint": "^5.16.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "^2.16.0", "karma": "^4.2.0", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", - "karma-mocha": "^1.3.0", - "mocha": "^5.2.0", + "karma-jasmine": "^4.0.1", "prettier": "^1.16.4", "puppeteer": "^1.18.1", "rollup": "^0.63.2", "rollup-plugin-babel": "^4.0.3" - } + }, + "dependencies": {} } diff --git a/src/api.js b/src/api.js index 24c0557..3895fae 100644 --- a/src/api.js +++ b/src/api.js @@ -8,6 +8,20 @@ function isEmptyObject(obj) { return Object.keys(obj).length === 0 && obj.constructor === Object; } +function areValidRequestHooks(requestHooks) { + const isValid = Array.isArray(requestHooks) && requestHooks.every(requestHook => + typeof requestHook === 'function' + && requestHook.length === 2 + && requestHook(new XMLHttpRequest()) instanceof XMLHttpRequest + ); + + if (!isValid) { + console.warn('Request hooks should have the following signature: function requestHook(request, metadata) { return request; }'); + } + + return isValid; +} + const getFirstResult = result => result[0]; const getFirstResultIfLengthGtOne = result => { if (result.length > 1) { @@ -26,6 +40,15 @@ const MEDIATYPES = { PNG: "image/png" }; +/** + * A callback with the request instance and metadata information + * of the currently request being executed that should necessarily + * return the given request optionally modified. + * @typedef {function} RequestHook + * @param {XMLHttpRequest} request - The original XMLHttpRequest instance. + * @param {object} metadata - The metadata used by the request. + */ + /** * Class for interacting with DICOMweb RESTful services. */ @@ -37,6 +60,7 @@ class DICOMwebClient { * @param {String} options.username - Username * @param {String} options.password - Password * @param {Object} options.headers - HTTP headers + * @param {Array.} options.requestHooks - Request hooks. * @param {Object} options.verbose - print to console request warnings and errors, default true */ constructor(options) { @@ -76,6 +100,10 @@ class DICOMwebClient { this.stowURL = this.baseURL; } + if ("requestHooks" in options) { + this.requestHooks = options.requestHooks; + } + // Headers to pass to requests. this.headers = options.headers || {}; @@ -122,15 +150,16 @@ class DICOMwebClient { * @param {String} method * @param {Object} headers * @param {Object} options + * @param {Array.} options.requestHooks - Request hooks. * @return {*} * @private */ _httpRequest(url, method, headers, options = {}) { - const {errorInterceptor} = this; + const { errorInterceptor, requestHooks } = this; return new Promise((resolve, reject) => { - const request = new XMLHttpRequest(); + let request = new XMLHttpRequest(); request.open(method, url, true); if ("responseType" in options) { request.responseType = options.responseType; @@ -199,6 +228,13 @@ class DICOMwebClient { } } + if (requestHooks && areValidRequestHooks(requestHooks)) { + const metadata = { method, url }; + const pipeRequestHooks = functions => (args) => functions.reduce((args, fn) => fn(args, metadata), args); + const pipedRequest = pipeRequestHooks(requestHooks); + request = pipedRequest(request); + } + // Add withCredentials to request if needed if ("withCredentials" in options) { if (options.withCredentials) { @@ -206,7 +242,6 @@ class DICOMwebClient { } } - if ("data" in options) { request.send(options.data); } else { diff --git a/test/test.js b/test/test.js index 0a8f909..2792b2e 100644 --- a/test/test.js +++ b/test/test.js @@ -1,4 +1,4 @@ -const { expect } = chai; +const { createSpy } = jasmine; function getTestDataInstance(url) { return new Promise((resolve, reject) => { @@ -6,7 +6,7 @@ function getTestDataInstance(url) { xhr.open("GET", url, true); xhr.responseType = "arraybuffer"; - xhr.onload = function() { + xhr.onload = function() { const arrayBuffer = this.response; if (arrayBuffer) { resolve(arrayBuffer); @@ -19,25 +19,22 @@ function getTestDataInstance(url) { }); } -describe('dicomweb.api.DICOMwebClient', function () { +describe('dicomweb.api.DICOMwebClient', function() { const dwc = new DICOMwebClient.api.DICOMwebClient({ url: 'http://localhost:8008/dcm4chee-arc/aets/DCM4CHEE/rs', retrieveRendered: false }); - it('should have correct constructor name', function() { - expect(dwc.constructor.name).to.equal('DICOMwebClient'); + it('should have correct constructor name', function() { + expect(dwc.constructor.name).toEqual('DICOMwebClient'); }); - it('should find zero studies', async function() { - const studies = await dwc.searchForStudies(); - - expect(studies).to.have.length(0); + it('should find zero studies', async function() { + const studies = await dwc.searchForStudies({ queryParams: { PatientID: 11235813 } }); + expect(studies.length).toBe(0); }); - it('should store one instance', async function() { - this.timeout(5000); - + it('should store one instance', async function() { // This is the HTTP server run by the Karma test // runner const url = 'http://localhost:9876/base/testData/sample.dcm'; @@ -48,16 +45,14 @@ describe('dicomweb.api.DICOMwebClient', function () { }; await dwc.storeInstances(options); - }); + }, 5000); - it('should find one study', async function() { + it('should find one study', async function() { const studies = await dwc.searchForStudies(); - expect(studies).to.have.length(1); + expect(studies.length).toBe(4); }); - it('should store two instances', async function() { - this.timeout(10000); - + it('should store two instances', async function() { // This is the HTTP server run by the Karma test // runner const url1 = 'http://localhost:9876/base/testData/sample2.dcm'; @@ -77,15 +72,15 @@ describe('dicomweb.api.DICOMwebClient', function () { }; await dwc.storeInstances(options); - }); + }, 10000); - it('should find four studes', async function() { + it('should find four studes', async function() { const studies = await dwc.searchForStudies(); - expect(studies).to.have.length(4); + expect(studies.length).toBe(4); }); - it('should retrieve a single frame of an instance', async function() { + it('should retrieve a single frame of an instance', async function() { // from sample.dcm const options = { studyInstanceUID: '1.3.6.1.4.1.14519.5.2.1.2744.7002.271803936741289691489150315969', @@ -97,7 +92,7 @@ describe('dicomweb.api.DICOMwebClient', function () { const frames = dwc.retrieveInstance(options); }); - it('should retrieve a single instance', async function() { + it('should retrieve a single instance', async function() { // from sample.dcm const options = { studyInstanceUID: '1.3.6.1.4.1.14519.5.2.1.2744.7002.271803936741289691489150315969', @@ -107,10 +102,10 @@ describe('dicomweb.api.DICOMwebClient', function () { const instance = await dwc.retrieveInstance(options); - expect(instance).to.be.an('arraybuffer'); + expect(instance instanceof ArrayBuffer).toBe(true); }); - it('should retrieve an entire series as an array of instances', async function() { + it('should retrieve an entire series as an array of instances', async function() { const options = { studyInstanceUID: '1.3.6.1.4.1.14519.5.2.1.2744.7002.271803936741289691489150315969', seriesInstanceUID: '1.3.6.1.4.1.14519.5.2.1.2744.7002.117357550898198415937979788256', @@ -118,21 +113,20 @@ describe('dicomweb.api.DICOMwebClient', function () { const instances = await dwc.retrieveSeries(options); - expect(instances).to.have.length(1); + expect(instances.length).toBe(1); }); - it('should retrieve an entire study as an array of instances', async function() { + it('should retrieve an entire study as an array of instances', async function() { const options = { studyInstanceUID: '1.3.6.1.4.1.14519.5.2.1.2744.7002.271803936741289691489150315969', }; const instances = await dwc.retrieveStudy(options); - expect(instances).to.have.length(1); + expect(instances.length).toBe(1); }); - it('should retrieve bulk data', async function() { - this.timeout(15000) + it('should retrieve bulk data', async function() { const options = { studyInstanceUID: '999.999.3859744', seriesInstanceUID: '999.999.94827453', @@ -148,8 +142,54 @@ describe('dicomweb.api.DICOMwebClient', function () { const bulkData = await dwc.retrieveBulkData(bulkDataOptions); - expect(bulkData).to.be.an('array'); - expect(bulkData).to.to.have.length(1); - expect(bulkData[0]).to.be.an('arraybuffer'); + expect(bulkData instanceof Array).toBe(true); + expect(bulkData.length).toBe(1); + expect(bulkData[0] instanceof ArrayBuffer).toBe(true); + }, 15000); + + describe('Request hooks', function() { + let requestHook1Spy, requestHook2Spy, url, metadataUrl, request; + + beforeEach(function() { + request = new XMLHttpRequest(); + url = 'http://localhost:8008/dcm4chee-arc/aets/DCM4CHEE/rs'; + metadataUrl = 'http://localhost:8008/dcm4chee-arc/aets/DCM4CHEE/rs/studies/999.999.3859744/series/999.999.94827453/instances/999.999.133.1996.1.1800.1.6.25/metadata'; + requestHook1Spy = createSpy('requestHook1Spy', function (request, metadata) { return request }).and.callFake((request, metadata) => request); + requestHook2Spy = createSpy('requestHook2Spy', function (request, metadata) { return request }).and.callFake((request, metadata) => request); + }); + + it('invalid request hooks should be notified and ignored', async function() { + /** Spy with invalid request hook signature */ + requestHook2Spy = createSpy('requestHook2Spy', function (request) { return request }).and.callFake((request, metadata) => request); + const dwc = new DICOMwebClient.api.DICOMwebClient({ + url, + requestHooks: [requestHook1Spy, requestHook2Spy] + }); + const metadata = { url: metadataUrl, method: 'get' }; + request.open('GET', metadata.url); + await dwc.retrieveInstanceMetadata({ + studyInstanceUID: '999.999.3859744', + seriesInstanceUID: '999.999.94827453', + sopInstanceUID: '999.999.133.1996.1.1800.1.6.25', + }); + expect(requestHook1Spy).not.toHaveBeenCalledWith(request, metadata); + expect(requestHook2Spy).not.toHaveBeenCalledWith(request, metadata); + }) + + it('valid request hooks should be called', async function() { + const dwc = new DICOMwebClient.api.DICOMwebClient({ + url, + requestHooks: [requestHook1Spy, requestHook2Spy] + }); + const metadata = { url: metadataUrl, method: 'get' }; + request.open('GET', metadata.url); + await dwc.retrieveInstanceMetadata({ + studyInstanceUID: '999.999.3859744', + seriesInstanceUID: '999.999.94827453', + sopInstanceUID: '999.999.133.1996.1.1800.1.6.25', + }); + expect(requestHook1Spy).toHaveBeenCalledWith(request, metadata); + expect(requestHook2Spy).toHaveBeenCalledWith(request, metadata); + }); }); });