diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..e69de29b diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..e63c9dd8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,14 @@ +{ + "extends": [ + "prettier" + ], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + "semi": ["error", "always"], + "quotes": ["error", "single", { "avoidEscape": true }], + + } +} \ No newline at end of file diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml new file mode 100644 index 00000000..a0ea4b92 --- /dev/null +++ b/.github/workflows/ci-workflow.yml @@ -0,0 +1,36 @@ +name: Lint and Test + +on: [push, pull_request] + +jobs: + lint: + name: Check tsc, lint, and prettier + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: '12.x' + - run: npm install + - run: npm run lint + - run: npm run prettier + env: + CI: true + test: + name: Test on node ${{ matrix.node-version }} and ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [12] + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test + env: + CI: true \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..912f7bb8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto", + "printWidth": 100, + "tabWidth": 2, + "singleQuote": true +} \ No newline at end of file diff --git a/package.json b/package.json index afbe9902..1dfb74e6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "REMS Administrator", + "name": "rems-administrator", "version": "1.0.0", "description": "A starter project emphasizing best practices for creating CDS Services", "main": "main.js", @@ -14,8 +14,11 @@ "scripts": { "develop": "node src/scripts/nodemon.js", "start": "node src/scripts/serve.js", - "test": "jest", - "prettier": "prettier \"src/**/*.js\" --write", + "test": "jest --maxWorkers=4 --coverage --detectOpenHandles", + "lint": "eslint \"**/*.{js,ts}\"", + "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", + "prettier": "prettier --check \"**/*.{js,ts}\"", + "prettier:fix": "prettier --write \"**/*.{js,ts}\"", "changelog": "conventional-changelog -p angular -s -i CHANGELOG.md" }, "repository": { @@ -51,8 +54,14 @@ "winston-daily-rotate-file": "^4.2.1" }, "devDependencies": { - "eslint": "^7.0.0", - "jest": "^26.0.1", - "prettier": "^2.0.5" + "prettier": "^2.0.5", + "eslint": "^8.5.0", + "eslint-config-prettier": "^6.10.1", + "jest": "^27.4.5", + "jest-extended": "^1.2.0", + "json-diff": "^0.9.0", + "ts-jest": "^27.1.2", + "ts-node": "^9.1.1", + "typescript": "^4.8.4" } } diff --git a/src/config.js b/src/config.js index d999de4a..560c91d1 100644 --- a/src/config.js +++ b/src/config.js @@ -1,10 +1,10 @@ module.exports = { server: { port: 8090, - discoveryEndpoint: '/cds-services', + discoveryEndpoint: '/cds-services' }, logging: { - level: 'info', - }, + level: 'info' + } }; diff --git a/src/hooks/rems.hook.js b/src/hooks/rems.hook.js index 1728ed6e..6789f43c 100644 --- a/src/hooks/rems.hook.js +++ b/src/hooks/rems.hook.js @@ -1,4 +1,4 @@ -const { kebabCase } = require("lodash"); +const { kebabCase } = require('lodash'); const definition = { hook: 'order-sign', @@ -7,34 +7,34 @@ const definition = { id: 'rems-order-sign', prefetch: { patient: 'Patient/{{context.patientId}}', - request:'MedicationRequest?_id={{context.draftOrders.MedicationRequest.id}}', + request: 'MedicationRequest?_id={{context.draftOrders.MedicationRequest.id}}', practitioner: 'Practitioner/{{context.userId}}' - }, + } }; -const sourceLabel = "MCODE REMS Administrator Prototype"; -const sourceUrl = "https://github.com/mcode/REMS"; +const sourceLabel = 'MCODE REMS Administrator Prototype'; +const sourceUrl = 'https://github.com/mcode/REMS'; function buildErrorCard(reason) { console.log(reason); let cards = { cards: [ { - summary: "Bad Request", + summary: 'Bad Request', detail: reason, source: { label: sourceLabel, url: sourceUrl }, - indicator: 'warning', - }, - ], + indicator: 'warning' + } + ] }; return cards; -}; +} const handler = (req, res) => { - console.log("REMS order-sign hook") + console.log('REMS order-sign hook'); try { const context = req.body.context; const contextRequest = context.draftOrders.entry[0]; @@ -44,27 +44,32 @@ const handler = (req, res) => { const practitioner = prefetch.practitioner; const npi = practitioner.identifier[0].value; - console.log(" MedicationRequest: " + prefetchRequest.id); - console.log(" Practitioner: " + practitioner.id + " NPI: " + npi); - console.log(" Patient: " + patient.id); + console.log(' MedicationRequest: ' + prefetchRequest.id); + console.log(' Practitioner: ' + practitioner.id + ' NPI: ' + npi); + console.log(' Patient: ' + patient.id); // verify a MedicationRequest was sent - if (contextRequest.resourceType !== "MedicationRequest") { - res.json(buildErrorCard("DraftOrders does not contain a MedicationRequest")); + if (contextRequest.resourceType !== 'MedicationRequest') { + res.json(buildErrorCard('DraftOrders does not contain a MedicationRequest')); return; } // verify ids - if (patient.id.replace('Patient/','') !== context.patientId.replace('Patient/','')) { - res.json(buildErrorCard("Context patientId does not match prefetch Patient ID")); + if (patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '')) { + res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); return; } - if (practitioner.id.replace('Practitioner/','') !== context.userId.replace('Practitioner/','')) { - res.json(buildErrorCard("Context userId does not match prefetch Practitioner ID")); + if ( + practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') + ) { + res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); return; } - if (prefetchRequest.id.replace('MedicationRequest/','') !== contextRequest.id.replace('MedicationRequest/','')) { - res.json(buildErrorCard("Context draftOrder does not match prefetch MedicationRequest ID")); + if ( + prefetchRequest.id.replace('MedicationRequest/', '') !== + contextRequest.id.replace('MedicationRequest/', '') + ) { + res.json(buildErrorCard('Context draftOrder does not match prefetch MedicationRequest ID')); return; } @@ -77,17 +82,16 @@ const handler = (req, res) => { detail: `Detail: ${text}`, source: { label: sourceLabel, - url: sourceUrl, + url: sourceUrl }, - indicator: 'info', - }, - ], + indicator: 'info' + } + ] }; res.json(cards); - } catch (error) { console.log(error); - res.json(buildErrorCard("Unknown Error")); + res.json(buildErrorCard('Unknown Error')); } }; diff --git a/src/hooks/rems.hook.test.js b/src/hooks/rems.hook.test.js index 2a298ef0..6118871c 100644 --- a/src/hooks/rems.hook.test.js +++ b/src/hooks/rems.hook.test.js @@ -9,9 +9,9 @@ describe('hook: test rems', () => { id: 'rems-order-sign', prefetch: { patient: 'Patient/{{context.patientId}}', - request:'MedicationRequest?_id={{context.draftOrders.MedicationRequest.id}}', + request: 'MedicationRequest?_id={{context.draftOrders.MedicationRequest.id}}', practitioner: 'Practitioner/{{context.userId}}' - }, + } }; expect(getREMSHook).toHaveProperty('definition'); expect(getREMSHook).toHaveProperty('handler'); diff --git a/src/lib/winston.js b/src/lib/winston.js index a62583c9..fb27408b 100644 --- a/src/lib/winston.js +++ b/src/lib/winston.js @@ -13,7 +13,7 @@ let applicationTransports = []; let transportConsole = new transports.Console({ level: logging.level, timestamp: true, - colorize: true, + colorize: true }); applicationTransports.push(transportConsole); @@ -25,7 +25,7 @@ if (logging.directory) { datePattern: 'YYYY-MM-DD-HH', level: logging.level, zippedArchive: true, - maxSize: '20m', + maxSize: '20m' }); applicationTransports.push(transportDailyFile); @@ -34,7 +34,7 @@ if (logging.directory) { // Add a default application logger container.add('application', { format: format.combine(format.timestamp(), format.logstash()), - transports: applicationTransports, + transports: applicationTransports }); /** diff --git a/src/lib/winston.test.js b/src/lib/winston.test.js index eb2c0c34..ebd4522f 100644 --- a/src/lib/winston.test.js +++ b/src/lib/winston.test.js @@ -24,8 +24,8 @@ describe('Logger Class', () => { jest.mock('../config', () => ({ logging: { level: 'debug', - directory: 'logs', - }, + directory: 'logs' + } })); const config = require('../config'); diff --git a/src/main.js b/src/main.js index f1a4d234..b04c7e92 100644 --- a/src/main.js +++ b/src/main.js @@ -16,8 +16,7 @@ module.exports = async function main() { // Build our server logger.info('Initializing REMS Administrator'); - const app = initialize(config) - .registerService(remsService); + const app = initialize(config).registerService(remsService); const { server: serverConfig } = config; diff --git a/src/scripts/develop.js b/src/scripts/develop.js index 7aedd887..0c51d7c3 100644 --- a/src/scripts/develop.js +++ b/src/scripts/develop.js @@ -8,7 +8,7 @@ nodemon({ ext: 'js json', verbose: true, watch: ['src/**/*.json', 'src/**/*.js'], - ignore: ['node_modules'], + ignore: ['node_modules'] }); nodemon diff --git a/src/server.js b/src/server.js index d05e82b4..fa053253 100644 --- a/src/server.js +++ b/src/server.js @@ -7,7 +7,7 @@ const _ = require('lodash'); let logger = container.get('application'); -const initialize = (config) => { +const initialize = config => { const logLevel = _.get(config, 'logging.level'); return new REMSServer().configureLogstream(logLevel).configureMiddleware(); }; @@ -54,7 +54,7 @@ class REMSServer { log ? log : morgan('combined', { - stream: { write: (message) => logger[level](message) }, + stream: { write: message => logger[level](message) } }) ); @@ -81,9 +81,7 @@ class REMSServer { listen({ port, discoveryEndpoint = '/cds-services' }, callback) { this.app.get(discoveryEndpoint, (req, res) => res.json({ services: this.services })); this.app.get('/', (req, res) => res.send('Welcome to the REMS Administrator')); - this.app.listen(port, callback); - - return this; + return this.app.listen(port, callback); } } diff --git a/src/server.test.js b/src/server.test.js index 665d3b11..0636a2a0 100644 --- a/src/server.test.js +++ b/src/server.test.js @@ -11,7 +11,7 @@ describe('REMSServer class', () => { // Mock express and body parser jest.mock('body-parser', () => ({ urlencoded: jest.fn(), - json: jest.fn(), + json: jest.fn() })); jest.mock('express', () => { @@ -21,12 +21,13 @@ describe('REMSServer class', () => { get: jest.fn(), listen: jest.fn(), options: jest.fn(), - post: jest.fn(), + post: jest.fn() })); // Mock the static directory function mock.static = jest.fn(); return mock; }); + server = new REMSServer(); }); @@ -39,6 +40,7 @@ describe('REMSServer class', () => { expect(server).toHaveProperty('app'); expect(server).toHaveProperty('listen'); }); + test('method: configureMiddleware', () => { let set = jest.spyOn(server.app, 'set'); let use = jest.spyOn(server.app, 'use'); @@ -53,6 +55,7 @@ describe('REMSServer class', () => { expect(use).toHaveBeenCalledTimes(3); }); + test('method: configureLogstream', () => { let use = jest.spyOn(server.app, 'use'); @@ -67,11 +70,11 @@ describe('REMSServer class', () => { hook: 'patient-view', name: 'foo', description: 'bar', - id: 'foobar', + id: 'foobar' }, handler: (req, res) => { res.json('hello world'); - }, + } }; server.registerService(mockService); @@ -81,22 +84,24 @@ describe('REMSServer class', () => { hook: 'patient-view', name: 'foo', description: 'bar', - id: 'foobar', - }, + id: 'foobar' + } ]); }); + test('Method: listen', () => { let listen = jest.spyOn(server.app, 'listen'); let callback = jest.fn(); // Start listening on a port and pass the callback through - server.listen({ port: 3000 }, callback); + let serverListen = server.listen({ port: 3000 }, callback); expect(listen).toHaveBeenCalledTimes(1); expect(listen.mock.calls[0][0]).toBe(3000); expect(listen.mock.calls[0][1]).toBe(callback); + serverListen.close(); }); + test('should be able to initilize a server', () => { const newServer = initialize(); - expect(newServer).toBeInstanceOf(REMSServer); expect(newServer).toHaveProperty('app'); expect(newServer).toHaveProperty('listen');