diff --git a/package-lock.json b/package-lock.json index 40658d10f43..d10037a926c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "medic", - "version": "4.15.0", + "version": "4.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "medic", - "version": "4.15.0", + "version": "4.16.0", "hasInstallScript": true, "license": "AGPL-3.0-only", "workspaces": [ diff --git a/package.json b/package.json index 70cd07cc139..8a0f873c4d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "medic", - "version": "4.15.0", + "version": "4.16.0", "private": true, "license": "AGPL-3.0-only", "repository": { diff --git a/scripts/build/index.js b/scripts/build/index.js index 66eb5474803..44c188118d0 100644 --- a/scripts/build/index.js +++ b/scripts/build/index.js @@ -1,7 +1,6 @@ const fs = require('fs'); const spawn = require('child_process').spawn; const path = require('path'); -const rpn = require('request-promise-native'); const mustache = require('mustache'); const packageJson = require('../../package.json'); @@ -34,8 +33,18 @@ const getApiUrl = (pathname = '') => { const apiUrl = new URL(COUCH_URL); apiUrl.port = API_PORT || DEFAULT_API_PORT; apiUrl.pathname = pathname; - - return apiUrl.toString(); + const basicAuth = btoa(`${apiUrl.username}:${apiUrl.password}`); + apiUrl.username = ''; + apiUrl.password = ''; + + return { + url: apiUrl.toString(), + options: { + headers: { + Authorization: `Basic ${basicAuth}`, + } + } + }; }; const releaseName = TAG || versions.escapeBranchName(BRANCH) || `${packageJson.version}-local-development`; @@ -157,24 +166,28 @@ const saveServiceTags = () => { fs.writeFileSync(tagsFilePath, JSON.stringify(tags)); }; -const updateServiceWorker = () => { - const updateSWUrl = getApiUrl('/api/v2/upgrade/service-worker'); +const updateServiceWorker = async () => { + const { url, options } = getApiUrl('/api/v2/upgrade/service-worker'); - return rpn.get(updateSWUrl).catch(err => { - if (err.status === 401) { - throw new Error('Environment variable COUCH_URL has invalid authentication'); + try { + const response = await fetch(url, options); + if (response.ok) { + return; } - if (err.status === 403) { - throw new Error('Environment variable COUCH_URL must have admin authentication'); + + throw response; + } catch (err) { + if (err.status === 401 || err.status === 403) { + throw new Error('Environment variable COUCH_URL does not have valid authentication'); } - if (err.error && err.error.code === 'ECONNREFUSED') { + if (err.cause?.code === 'ECONNREFUSED') { console.warn('API could not be reached, so the service-worker has not been updated. '); return; } throw err; - }); + } }; const setDdocsVersion = () => { diff --git a/scripts/bulk-password-update-export.js b/scripts/bulk-password-update-export.js index e3668384693..e9c0269d0aa 100755 --- a/scripts/bulk-password-update-export.js +++ b/scripts/bulk-password-update-export.js @@ -3,7 +3,6 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED=0; const minimist = require('minimist'); const {promises: fsPromises} = require('fs'); const readline = require('readline'); -const rpn = require('request-promise-native'); const {randomInt} = require('crypto'); const csvSync = require('csv-parse/sync'); @@ -46,10 +45,10 @@ const user = argv.user; const password = argv.password; const options = { - uri: url.href, - json: true, + method: 'POST', headers: { - 'Authorization': 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64') + 'Authorization': 'Basic ' + btoa(`${user}:${password}`), + 'Content-Type': 'application/json' } }; @@ -73,10 +72,8 @@ Do you want to continue? [y/N] const changeUserPass = async (user, options) => { const postOptions = {...options}; - postOptions.body = { - 'password': user.pass - }; - postOptions.uri = `${options.uri}/${user.name}`; + postOptions.body = JSON.stringify({ password: user.pass }); + const uri = `${url.href}/${user.name}`; try { if (admins.includes(user.name)) { throw new Error(`403 - Password change for "${user.name}" not allowed .`); @@ -84,7 +81,7 @@ const changeUserPass = async (user, options) => { if (user.name.toString().trim() === '') { throw new Error(`404 - Username is blank - check CSV and run again.`); } - await rpn.post(postOptions); + await fetch(uri, postOptions); console.log('SUCCESS', user.name, user.pass); } catch (e) { console.log('ERROR', user.name, e.message); diff --git a/scripts/generate-form-attachments/.eslintrc.json b/scripts/generate-form-attachments/.eslintrc.json deleted file mode 100644 index 750c332bcb0..00000000000 --- a/scripts/generate-form-attachments/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": 8, - "sourceType": "module" - }, - "rules": { - "semi": 2 - }, - "env": { - "node": true - } -} diff --git a/scripts/generate-form-attachments/README.md b/scripts/generate-form-attachments/README.md deleted file mode 100644 index 42397e2c512..00000000000 --- a/scripts/generate-form-attachments/README.md +++ /dev/null @@ -1,39 +0,0 @@ -This script will iterate over all reports in the indicated database and generate XML `content` attachments. - -Reports that are not XML type reports and reports that already have a `content` attachment will be skipped. - -To install, run `npm ci`. - -To run, the script requires the COUCH_URL environment variable, that defines which database to use. -ex: -``` -EXPORT COUCH_URL= -``` - -Run command: - -There are two ways to run the script, depending on how you require data to be read from the database. - -1. From `reports_by_form` view: -``` -npm run view -``` -This command is more performant, as it will only read report documents from the database (in batches) by using the `reports_by_form` view in `_design/medic-client`. -Use this command when running against an already installed `cht-core` - -2. From `_all_docs` -``` -npm run alldocs -``` -This command reads the all docs in the database (in batches) and skips over all docs that are not reports. Use this command against a database that does not have `cht-core` installed. - -Test command: - -``` -npm run test -``` - -*Disclaimer* -The script reads reports by querying a view, so run this script on a database that already has `cht-core` installed. -The script generates XML attachments based off of `doc.fields` and `doc.hidden_fields`. -It may not yield perfectly accurate (Enketo-friendly) results for overly complicated structures, like nested repeats, because of inconsistencies in how we manage these repeats. diff --git a/scripts/generate-form-attachments/package-lock.json b/scripts/generate-form-attachments/package-lock.json deleted file mode 100644 index de50480c1fa..00000000000 --- a/scripts/generate-form-attachments/package-lock.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "generate-form-attachments", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "generate-form-attachments", - "version": "1.0.0", - "license": "ISC" - } - } -} diff --git a/scripts/generate-form-attachments/package.json b/scripts/generate-form-attachments/package.json deleted file mode 100644 index b410c397f43..00000000000 --- a/scripts/generate-form-attachments/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "generate-form-attachments", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "view": "node ./src/index.js", - "alldocs": "node ./src/index.js --alldocs", - "test": "../../node_modules/.bin/eslint **/*.js && ../../node_modules/.bin/mocha ./test/**/*.spec.js" - }, - "license": "AGPL-3.0-only" -} diff --git a/scripts/generate-form-attachments/src/create-attachments.js b/scripts/generate-form-attachments/src/create-attachments.js deleted file mode 100644 index 11d4bf2d95f..00000000000 --- a/scripts/generate-form-attachments/src/create-attachments.js +++ /dev/null @@ -1,163 +0,0 @@ -const rpn = require('request-promise-native'); -const xmlJs = require('xml-js'); - - -const getUrl = (couchUrl, path) => { - return `${couchUrl.protocol}//${couchUrl.auth}@${couchUrl.host}/${couchUrl.path.substring(1)}/${path}`; -}; - -const getReports = async (uri, opts) => { - const result = await rpn.get({ uri, json: true, qs: opts }); - - const startKeyDocId = opts.start_key_doc_id || (opts.start_key && JSON.parse(opts.start_key)); - - if (!result.rows.length || result.rows.length === 1 && result.rows[0].id === startKeyDocId) { - return {}; - } - - const reports = result.rows.map(row => row.doc); - const { key: nextKey, id: nextKeyDocId } = result.rows.slice(-1)[0]; - - return { nextKey, nextKeyDocId, reports }; -}; - -const getReportsByForm = async (couchUrl, startKey = '', startKeyDocId = '') => { - const opts = { - limit: 1000, - include_docs: true, - reduce: false, - }; - - if (startKey) { - opts.start_key = JSON.stringify(startKey); - opts.start_key_doc_id = startKeyDocId; - } - - const uri = getUrl(couchUrl, '_design/medic-client/_view/reports_by_form'); - return getReports(uri, opts, startKeyDocId); -}; - -const getReportsByAllDocs = async (couchUrl, startKey = '') => { - const opts = { - limit: 1000, - include_docs: true, - start_key: JSON.stringify(startKey), - }; - const uri = getUrl(couchUrl, '_all_docs'); - return getReports(uri, opts); -}; - -const populateXmlFields = (attachmentObj, fieldName, fieldValue, path, hiddenFields = []) => { - let thisFieldPath = path; - if (fieldName) { - thisFieldPath = path ? `${path}.${fieldName}` : fieldName; - } - - const isHidden = hiddenFields && hiddenFields.includes(thisFieldPath); - - const field = Array.isArray(fieldValue) ? [] : {}; - - if (isHidden) { - field._attributes = { tag: 'hidden' }; - } - - if (Array.isArray(attachmentObj)) { - attachmentObj.push(field); - } else { - attachmentObj[fieldName] = field; - } - - if (!fieldValue || typeof fieldValue !== 'object') { - field._text = fieldValue; - return; - } - - if (Array.isArray(fieldValue)) { - fieldValue.forEach(value => { - populateXmlFields(field, undefined, value, thisFieldPath, hiddenFields); - }); - return; - } - - Object.keys(fieldValue).forEach(key => { - populateXmlFields(field, key, fieldValue[key], thisFieldPath, hiddenFields); - }); -}; - -const jsonToXml = (doc) => { - const attachmentJson = { - [doc.form]: { - _attributes: { - 'xmlns:jr': 'http://openrosa.org/javarosa', - 'xmlns:orx': 'http://openrosa.org/xforms', - delimiter: '#', - id: doc.form, - } - } - }; - - Object.keys(doc.fields).forEach(field => { - populateXmlFields(attachmentJson[doc.form], field, doc.fields[field], '', doc.hidden_fields); - }); - - return xmlJs.js2xml(attachmentJson, { compact: true }); -}; - -const createAttachments = async (couchUrl, reports) => { - if (!reports || !reports.length) { - return; - } - - const updates = reports - .map(report => { - if (!report || report.type !== 'data_record' || !report.form) { - // skip non-reports - return; - } - if (!report.content_type || report.content_type !== 'xml') { - // skip non-xform reports - return; - } - if (report._attachments && report._attachments.content) { - // skip reports that already have attachments - return; - } - console.log('generating attachment for', report._id); - const attachment = jsonToXml(report); - report._attachments = report._attachments || {}; - report._attachments.content = { - content_type: 'application/xml', - data: new Buffer.from(attachment).toString('base64') - }; - - return report; - }) - .filter(doc => doc); - - if (!updates.length) { - return; - } - - await rpn.post({ uri: getUrl(couchUrl, '_bulk_docs'), json: true, body: { docs: updates } }); -}; - -const create = async (couchUrl, allDocs = false) => { - let startKey; - let startKeyDocId; - do { - let result; - if (!allDocs) { - console.debug('requesting reports by view with startkey', startKey, startKeyDocId); - result = await getReportsByForm(couchUrl, startKey, startKeyDocId); - } else { - console.debug('requesting reports by _all_docs with startkey', startKey); - result = await getReportsByAllDocs(couchUrl, startKey); - } - - ({ nextKey: startKey, nextKeyDocId: startKeyDocId } = result); - - await createAttachments(couchUrl, result.reports); - } while (startKey && startKeyDocId); -}; - -module.exports = { create }; diff --git a/scripts/generate-form-attachments/src/index.js b/scripts/generate-form-attachments/src/index.js deleted file mode 100644 index 0eaa7a0161e..00000000000 --- a/scripts/generate-form-attachments/src/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const url = require('url'); -const createAttachments = require('./create-attachments'); -const { COUCH_URL } = process.env; - -if (!COUCH_URL) { - throw new Error('Required environment variable COUCH_URL is undefined. (eg. http://your:pass@localhost:5984/yourdb)'); -} - -const parsedUrl = url.parse(COUCH_URL); -if (!parsedUrl.auth) { - throw new Error('COUCH_URL must contain admin authentication information'); -} - -const args = process.argv; -const useAllDocs = args && args.length > 2 && args[2] === '--alldocs'; - -createAttachments.create(parsedUrl, useAllDocs); diff --git a/scripts/generate-form-attachments/test/.eslintrc.json b/scripts/generate-form-attachments/test/.eslintrc.json deleted file mode 100644 index 7eeefc33b66..00000000000 --- a/scripts/generate-form-attachments/test/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "mocha": true - } -} diff --git a/scripts/generate-form-attachments/test/create-attachments.spec.js b/scripts/generate-form-attachments/test/create-attachments.spec.js deleted file mode 100644 index 8b8e246e12c..00000000000 --- a/scripts/generate-form-attachments/test/create-attachments.spec.js +++ /dev/null @@ -1,682 +0,0 @@ -const chai = require('chai'); -const sinon = require('sinon'); -const rewire = require('rewire'); -const url = require('url'); - -const rpn = require('request-promise-native'); - -let createAttachmentsSpec; - -describe('generate form attachments', () => { - beforeEach(() => { - createAttachmentsSpec = rewire('../src/create-attachments.js'); - }); - afterEach(() => sinon.restore()); - - describe('create', () => { - it('should keep requesting reports until no more reports are returned with view', async () => { - const getReportsByForm = sinon.stub(); - const createAttachments = sinon.stub(); - getReportsByForm - .onCall(0).resolves({ nextKey: '"one"', nextKeyDocId: 'one-one', reports: ['1', '2', '3'] }) - .onCall(1).resolves({ nextKey: '"three"', nextKeyDocId: 'three-three', reports: ['3', '4', '5'] }) - .onCall(2).resolves({ nextKey: '"five"', nextKeyDocId: 'five-five', reports: ['5', '6', '7'] }) - .onCall(3).resolves({ nextKey: '"seven"', nextKeyDocId: 'seven-seven', reports: ['7', '8', '9'] }) - .onCall(4).resolves({}); - - createAttachmentsSpec.__set__('getReportsByForm', getReportsByForm); - createAttachmentsSpec.__set__('createAttachments', createAttachments); - await createAttachmentsSpec.create('url'); - - chai.expect(getReportsByForm.callCount).to.equal(5); - chai.expect(getReportsByForm.args).to.deep.equal([ - ['url', undefined, undefined], - ['url', '"one"', 'one-one'], - ['url', '"three"', 'three-three'], - ['url', '"five"', 'five-five'], - ['url', '"seven"', 'seven-seven'], - ]); - chai.expect(createAttachments.callCount).to.equal(5); - chai.expect(createAttachments.args).to.deep.equal([ - ['url', ['1', '2', '3']], - ['url', ['3', '4', '5']], - ['url', ['5', '6', '7']], - ['url', ['7', '8', '9']], - ['url', undefined], - ]); - }); - - it('should keep requesting reports until no more reports are returned with all docs', async () => { - const getReportsByAllDocs = sinon.stub(); - const createAttachments = sinon.stub(); - getReportsByAllDocs - .resolves({}) - .onCall(0).resolves({ nextKey: 'one', nextKeyDocId: 'one', reports: ['1', '2', '3'] }) - .onCall(1).resolves({ nextKey: 'three', nextKeyDocId: 'three', reports: ['3', '4', '5'] }) - .onCall(2).resolves({ nextKey: 'five', nextKeyDocId: 'five', reports: ['5', '6', '7'] }) - .onCall(3).resolves({ nextKey: 'seven', nextKeyDocId: 'seven', reports: ['7', '8', '9'] }) - .onCall(4).resolves({}); - - createAttachmentsSpec.__set__('getReportsByAllDocs', getReportsByAllDocs); - createAttachmentsSpec.__set__('createAttachments', createAttachments); - await createAttachmentsSpec.create('url', true); - - chai.expect(getReportsByAllDocs.callCount).to.equal(5); - chai.expect(getReportsByAllDocs.args).to.deep.equal([ - ['url', undefined], - ['url', 'one'], - ['url', 'three'], - ['url', 'five'], - ['url', 'seven'], - ]); - chai.expect(createAttachments.callCount).to.equal(5); - chai.expect(createAttachments.args).to.deep.equal([ - ['url', ['1', '2', '3']], - ['url', ['3', '4', '5']], - ['url', ['5', '6', '7']], - ['url', ['7', '8', '9']], - ['url', undefined], - ]); - }); - }); - - describe('getReportsByForm', () => { - const couchUrl = url.parse('http://admin:password@127.0.0.1/dbname'); - it('should skip startKey and startKeyDocId when not provided', async () => { - const getReportsByForm = createAttachmentsSpec.__get__('getReportsByForm'); - sinon.stub(rpn, 'get').resolves({ - rows: [ - { id: 'report1', key: ['form1'], doc: { _id: 'report1', fields: {} } }, - { id: 'report2', key: ['form1'], doc: { _id: 'report2', fields: {} } }, - { id: 'report3', key: ['form2'], doc: { _id: 'report3', fields: {} } }, - { id: 'report4', key: ['form2'], doc: { _id: 'report4', fields: {} } }, - ] - }); - const result = await getReportsByForm(couchUrl); - chai.expect(result).to.deep.equal({ - nextKey: ['form2'], - nextKeyDocId: 'report4', - reports: [ - { _id: 'report1', fields: {} }, - { _id: 'report2', fields: {} }, - { _id: 'report3', fields: {} }, - { _id: 'report4', fields: {} }, - ] - }); - - chai.expect(rpn.get.callCount).to.equal(1); - chai.expect(rpn.get.args[0]).to.deep.equal([{ - uri: 'http://admin:password@127.0.0.1/dbname/_design/medic-client/_view/reports_by_form', - qs: { - limit: 1000, - include_docs: true, - reduce: false, - }, - json: true - }]); - }); - - it('should request with provided startkey and startkeydocId', async () => { - const getReportsByForm = createAttachmentsSpec.__get__('getReportsByForm'); - sinon.stub(rpn, 'get').resolves({ - rows: [ - { id: 'report4', key: ['form2'], doc: { _id: 'report4', fields: {} } }, - { id: 'report5', key: ['form2'], doc: { _id: 'report5', fields: {} } }, - { id: 'report6', key: ['form3'], doc: { _id: 'report6', fields: {} } }, - { id: 'report7', key: ['form4'], doc: { _id: 'report7', fields: {} } }, - { id: 'report8', key: ['form4'], doc: { _id: 'report8', fields: {} } }, - ] - }); - const result = await getReportsByForm(couchUrl, ['form2'], 'report4'); - chai.expect(result).to.deep.equal({ - nextKey: ['form4'], - nextKeyDocId: 'report8', - reports: [ - { _id: 'report4', fields: {} }, - { _id: 'report5', fields: {} }, - { _id: 'report6', fields: {} }, - { _id: 'report7', fields: {} }, - { _id: 'report8', fields: {} }, - ] - }); - - chai.expect(rpn.get.callCount).to.equal(1); - chai.expect(rpn.get.args[0]).to.deep.equal([{ - uri: 'http://admin:password@127.0.0.1/dbname/_design/medic-client/_view/reports_by_form', - qs: { - limit: 1000, - include_docs: true, - reduce: false, - start_key: '["form2"]', - start_key_doc_id: 'report4', - }, - json: true - }]); - }); - }); - - describe('getReportsByAllDocs', () => { - const couchUrl = url.parse('http://admin:pass@127.0.0.1/dbname'); - it('should request with provided start key when empty', async () => { - const getReportsByAllDocs = createAttachmentsSpec.__get__('getReportsByAllDocs'); - sinon.stub(rpn, 'get').resolves({ - rows: [ - { id: 'report1', key: 'report1', doc: { _id: 'report1', fields: {} } }, - { id: 'report2', key: 'report2', doc: { _id: 'report2', fields: {} } }, - { id: 'report3', key: 'report3', doc: { _id: 'report3', fields: {} } }, - { id: 'report4', key: 'report4', doc: { _id: 'report4', fields: {} } }, - ] - }); - const result = await getReportsByAllDocs(couchUrl); - chai.expect(result).to.deep.equal({ - nextKey: 'report4', - nextKeyDocId: 'report4', - reports: [ - { _id: 'report1', fields: {} }, - { _id: 'report2', fields: {} }, - { _id: 'report3', fields: {} }, - { _id: 'report4', fields: {} }, - ] - }); - - chai.expect(rpn.get.callCount).to.equal(1); - chai.expect(rpn.get.args[0]).to.deep.equal([{ - uri: 'http://admin:pass@127.0.0.1/dbname/_all_docs', - qs: { - limit: 1000, - include_docs: true, - start_key: '""' - }, - json: true - }]); - }); - - it('should request with provided start key when not empty', async () => { - const getReportsByAllDocs = createAttachmentsSpec.__get__('getReportsByAllDocs'); - sinon.stub(rpn, 'get').resolves({ - rows: [ - { id: 'report4', key: 'report4', doc: { _id: 'report4', fields: {} } }, - { id: 'report5', key: 'report5', doc: { _id: 'report5', fields: {} } }, - { id: 'report6', key: 'report6', doc: { _id: 'report6', fields: {} } }, - { id: 'report7', key: 'report7', doc: { _id: 'report7', fields: {} } }, - ] - }); - const result = await getReportsByAllDocs(couchUrl, 'report4'); - chai.expect(result).to.deep.equal({ - nextKey: 'report7', - nextKeyDocId: 'report7', - reports: [ - { _id: 'report4', fields: {} }, - { _id: 'report5', fields: {} }, - { _id: 'report6', fields: {} }, - { _id: 'report7', fields: {} }, - ] - }); - - chai.expect(rpn.get.callCount).to.equal(1); - chai.expect(rpn.get.args[0]).to.deep.equal([{ - uri: 'http://admin:pass@127.0.0.1/dbname/_all_docs', - qs: { - limit: 1000, - include_docs: true, - start_key: '"report4"' - }, - json: true - }]); - }); - }); - - describe('createAttachments', () => { - const couchUrl = url.parse('http://admin:password@127.0.0.1/dbname'); - - const getCompactXml = xml => { - return xml - .split('\n') - .map(line => line.trim()) - .join(''); - }; - - it('should do nothing when no reports passed', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - await createAttachments(couchUrl); - }); - - it('should do nothing when no reports passed', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - await createAttachments(couchUrl, []); - }); - - it('should skip reports that are xml reports', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - const reports = [ - { _id: 'report1' }, - { _id: 'report1', content_type: 'not_xml' }, - { _id: 'report1', type: 'other' }, - undefined, - { _id: 'report1', type: 'data_record' }, - { _id: 'report1', type: 'data_record', form: 'form', content_type: 'not_xml' }, - ]; - await createAttachments(couchUrl, reports); - }); - - it('should skip reports that already have a "content" attachment', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - const reports = [ - { _id: 'report1', type: 'data_record', form: 'form', content_type: 'xml', _attachments: { content: {} }}, - { _id: 'report2', type: 'data_record', form: 'form', content_type: 'xml', _attachments: { content: {} }}, - ]; - await createAttachments(couchUrl, reports); - }); - - it('should create attachments for "simple" docs', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - sinon.stub(rpn, 'post').resolves([]); - - const reports = [ - { - _id: 'report1', - type: 'data_record', - content_type: 'xml', - form: 'pregnancy', - contact: { _id: 'contact_id' }, - fields: { - patient_id: '12345', - patient_uuid: 'patient_uuid', - lmp_date: '28-01-2020', - weeks_pregnant: 10, - patient_name: 'person' - }, - hidden_fields: [], - }, - { - _id: 'report2', - type: 'data_record', - content_type: 'xml', - form: 'home_visit', - contact: { _id: 'contact_id' }, - fields: { - patient_id: '999999', - patient_uuid: 'the_uuid', - visited_patient_uuid: 'hh_uuid', - visited_date: '16-04-2020' - }, - hidden_fields: [], - }, - ]; - - const xmlReport1 = ` - - 12345 - patient_uuid - 28-01-2020 - 10 - person - - `; - const xmlReport2 = ` - - 999999 - the_uuid - hh_uuid - 16-04-2020 - - `; - - await createAttachments(couchUrl, reports); - - chai.expect(rpn.post.callCount).to.equal(1); - chai.expect(rpn.post.args[0]).to.deep.equal([{ - uri: 'http://admin:password@127.0.0.1/dbname/_bulk_docs', - json: true, - body: { - docs: [ - { - _id: 'report1', - type: 'data_record', - content_type: 'xml', - form: 'pregnancy', - contact: { _id: 'contact_id' }, - fields: { - patient_id: '12345', - patient_uuid: 'patient_uuid', - lmp_date: '28-01-2020', - weeks_pregnant: 10, - patient_name: 'person' - }, - hidden_fields: [], - _attachments: { - content: { - content_type: 'application/xml', - data: new Buffer.from(getCompactXml(xmlReport1)).toString('base64'), - } - }, - }, - { - _id: 'report2', - type: 'data_record', - content_type: 'xml', - form: 'home_visit', - contact: { _id: 'contact_id' }, - fields: { - patient_id: '999999', - patient_uuid: 'the_uuid', - visited_patient_uuid: 'hh_uuid', - visited_date: '16-04-2020' - }, - hidden_fields: [], - _attachments: { - content: { - content_type: 'application/xml', - data: new Buffer.from(getCompactXml(xmlReport2)).toString('base64'), - } - }, - }, - ], - }, - }]); - }); - - it('should support "array" fields', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - sinon.stub(rpn, 'post').resolves([]); - - const reports = [ - { - _id: 'report1', - type: 'data_record', - content_type: 'xml', - form: 'delivery', - contact: { _id: 'contact_id' }, - fields: { - patient_id: '12345', - patient_uuid: 'patient_uuid', - children: { - child: [ - { - name: 'maria', - sex: 'female' - }, - { - name: 'george', - sex: 'male' - }, - ] - }, - }, - hidden_fields: [ - 'children.child.name' - ], - }, - ]; - - const xmlReport = ` - - 12345 - patient_uuid - - - maria - female - - - george - male - - - - `; - - await createAttachments(couchUrl, reports); - - chai.expect(rpn.post.callCount).to.equal(1); - chai.expect(rpn.post.args[0]).to.deep.equal([{ - uri: 'http://admin:password@127.0.0.1/dbname/_bulk_docs', - json: true, - body: { - docs: [ - { - _id: 'report1', - type: 'data_record', - content_type: 'xml', - form: 'delivery', - contact: { _id: 'contact_id' }, - fields: { - patient_id: '12345', - patient_uuid: 'patient_uuid', - children: { - child: [ - { - name: 'maria', - sex: 'female' - }, - { - name: 'george', - sex: 'male' - }, - ] - }, - }, - hidden_fields: [ - 'children.child.name' - ], - _attachments: { - content: { - content_type: 'application/xml', - data: new Buffer.from(getCompactXml(xmlReport)).toString('base64'), - } - }, - }, - ], - }, - }]); - }); - - it('should create attachments for complex reports', async () => { - const createAttachments = createAttachmentsSpec.__get__('createAttachments'); - sinon.stub(rpn, 'post').resolves([]); - - const reports = [ - { - _id: 'report1', - type: 'data_record', - content_type: 'xml', - form: 'delivery', - contact: { _id: 'contact_id' }, - fields: { - inputs: { - source: 'user', - contact: { - _id: 'contact_id', - name: 'chw', - parent: { - _id: 'parent_id', - } - } - }, - household_id: '123', - area_id: '456', - facility_id: '789', - patient_age_in_years: 23, - patient_id: '12345', - patient_uuid: 'patient_uuid', - patient_name: 'person', - condition: { - woman_outcome: 'no' - }, - death_info_woman: { - woman_death_date: 'no', - death_report: { - form: 'death_report', - type: 'data_record', - content_type: 'xml', - fields: { - patient_id: '12345', - patient_uuid: 'patient_uuid', - death_details: { - date_of_death: '2020-12-02', - place_of_death: 'home', - } - } - } - }, - woman_death_report_doc: 'yes', - delivery_outcome: { - babies_delivered: 2, - babies_alive: 2, - delivery_mode: 'surgical', - }, - baby_death: { - baby_death_repeat_count: 0, - }, - babys_condition: { - }, - meta: { - instanceID: 'instance_id' - } - }, - hidden_fields: [ - 'inputs', - 'meta', - 'death_info_woman.death_report.fields', - ], - }, - ]; - - const xmlReport = ` - - - user - - <_id>contact_id - chw - - <_id>parent_id - - - - 123 - 456 - 789 - 23 - 12345 - patient_uuid - person - - no - - - no - -
death_report
- data_record - xml - - 12345 - patient_uuid - - 2020-12-02 - home - - -
-
- yes - - 2 - 2 - surgical - - - 0 - - - - instance_id - -
- `; - - await createAttachments(couchUrl, reports); - - chai.expect(rpn.post.callCount).to.equal(1); - chai.expect(rpn.post.args[0]).to.deep.equal([{ - uri: 'http://admin:password@127.0.0.1/dbname/_bulk_docs', - json: true, - body: { - docs: [ - { - _id: 'report1', - type: 'data_record', - content_type: 'xml', - form: 'delivery', - contact: { _id: 'contact_id' }, - fields: { - inputs: { - source: 'user', - contact: { - _id: 'contact_id', - name: 'chw', - parent: { - _id: 'parent_id', - } - } - }, - household_id: '123', - area_id: '456', - facility_id: '789', - patient_age_in_years: 23, - patient_id: '12345', - patient_uuid: 'patient_uuid', - patient_name: 'person', - condition: { - woman_outcome: 'no' - }, - death_info_woman: { - woman_death_date: 'no', - death_report: { - form: 'death_report', - type: 'data_record', - content_type: 'xml', - fields: { - patient_id: '12345', - patient_uuid: 'patient_uuid', - death_details: { - date_of_death: '2020-12-02', - place_of_death: 'home', - } - } - } - }, - woman_death_report_doc: 'yes', - delivery_outcome: { - babies_delivered: 2, - babies_alive: 2, - delivery_mode: 'surgical', - }, - baby_death: { - baby_death_repeat_count: 0, - }, - babys_condition: { - }, - meta: { - instanceID: 'instance_id' - } - }, - hidden_fields: [ - 'inputs', - 'meta', - 'death_info_woman.death_report.fields', - ], - _attachments: { - content: { - content_type: 'application/xml', - data: new Buffer.from(getCompactXml(xmlReport)).toString('base64'), - } - }, - }, - ], - }, - }]); - }); - }); -}); diff --git a/scripts/generate-form-attachments/test/index.spec.js b/scripts/generate-form-attachments/test/index.spec.js deleted file mode 100644 index d3f960ad34e..00000000000 --- a/scripts/generate-form-attachments/test/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -const chai = require('chai'); -const sinon = require('sinon'); -const url = require('url'); - -describe('index', () => { - it('should crash when no COUCH_URL', () => { - try { - require('../src/index'); - chai.expect(2).to.equal(3); - } catch (err) { - chai.expect(err.message).to.equal('Required environment variable COUCH_URL is undefined. (eg. http://your:pass@localhost:5984/yourdb)'); - } - }); - - it('should crash when no authentication in couchURL', () => { - process.env = { COUCH_URL: 'http://localhost:5984/yourdb' }; - try { - require('../src/index'); - chai.expect(2).to.equal(3); - } catch (err) { - chai.expect(err.message).to.equal('COUCH_URL must contain admin authentication information'); - } - }); - - it('should call createAttachments with the parsed url', () => { - process.env = { COUCH_URL: 'http://your:pass@localhost:5984/yourdb' }; - const parsed = url.parse(process.env.COUCH_URL); - const createAttachments = require('../src/create-attachments'); - sinon.stub(createAttachments, 'create'); - - require('../src/index'); - - chai.expect(createAttachments.create.callCount).to.equal(1); - chai.expect(createAttachments.create.args[0]).to.deep.equal([parsed, false]); - }); -}); diff --git a/scripts/set-default-contact/.eslintrc b/scripts/set-default-contact/.eslintrc deleted file mode 100644 index 3a4e653a294..00000000000 --- a/scripts/set-default-contact/.eslintrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "@medic", - "parserOptions": { - "ecmaVersion": 8 - }, - "plugins": ["promise", "node"], - "rules": { - "array-bracket-newline": ["error", "consistent"], - "array-callback-return": ["error", { "allowImplicit": true }], - "arrow-spacing": ["error", { "before": true, "after": true }], - "brace-style": ["error", "1tbs"], - "comma-spacing": ["error", { "before": false, "after": true }], - "comma-style": ["error", "last"], - "default-param-last": "error", - "dot-location": ["error", "property"], - "dot-notation": ["error", { "allowKeywords": true }], - "func-call-spacing": ["error", "never"], - "func-style": ["error", "expression"], - "linebreak-style": ["error", "unix"], - "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], - "no-alert": "error", - "no-extra-bind": "error", - "no-lone-blocks": "error", - "no-nested-ternary": "error", - "no-undef-init": "error", - "no-useless-rename": "error", - "no-whitespace-before-property": "error", - "node/no-exports-assign": "error", - "rest-spread-spacing": ["error", "never"], - "semi-spacing": ["error", { "before": false, "after": true }], - "semi-style": ["error", "last"], - "template-curly-spacing": "error", - "unicode-bom": ["error", "never"] - } -} diff --git a/scripts/set-default-contact/index.js b/scripts/set-default-contact/index.js index 556fb2542fb..dee29b9fea9 100644 --- a/scripts/set-default-contact/index.js +++ b/scripts/set-default-contact/index.js @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -const request = require('request-promise-native'); +console.log(` Using COUCH_URL env var: ${process.env.COUCH_URL}\n`); const compileUrl = path => { try { @@ -10,37 +10,39 @@ const compileUrl = path => { } }; +const fetchUrl = async (url, options) => { + options = { + ...options, + headers: { + 'Authorization': 'Basic ' + btoa(`${url.username}:${url.password}`), + 'Content-Type': 'application/json' + } + }; + url.username = ''; + url.password = ''; + + const response = await fetch(url.toString(), options); + if (response.ok) { + return await response.json(); + } + + response.message = await response.text(); + throw response; +}; + const getChtUsers = async () => { - console.log(' Using COUCH_URL env var:', process.env.COUCH_URL); - console.log(''); // pin to v1 of API so it is backwards compatible with CHT 3.x const url = compileUrl('/api/v1/users'); - const options = { - uri: url.href, - json: true - }; - - return request.get(options) - .then(users => { - if (typeof users === 'object' ){ - console.log(' Found', users.length, 'users\n'); - return users; - } - }); + const users = await fetchUrl(url); + if (typeof users === 'object') { + console.log(` Found ${users.length} users\n`); + return users; + } }; const getObjectFromMedicDb = async id => { const url = compileUrl('/medic/' + id); - const options = { - uri: url.href, - json: true - }; - - return request.get(options) - .then(object => { - return object; - }); - + return await fetchUrl(url); }; const hasDefaultContact = async user => { @@ -72,12 +74,7 @@ const savePlace = async (placeId, contactId) => { } placeObj.contact._id = contactId; placeObj.contact.parent = {_id: placeId}; - const options = { - uri: url.href, - json: true, - body: placeObj - }; - return request.put(options); + return await fetchUrl(url, { body: JSON.stringify(placeObj), method: 'PUT' }); }; @@ -99,17 +96,12 @@ const go = async () => { const updatedCount = await setContactsAsPlacesDefaults(filteredUsers); console.log('\n Updated', updatedCount, 'users'); } catch (e) { - if (e.statusCode === 401) { + if (e.status === 401) { console.log(' Bad authentication for CouchDB. Check that COUCH_URL has correct username and password.'); return; } console.log(' ' + e.message); - if (process.env.DEBUG === 'True'){ - console.log('\n ' + e.stack); - } else { - console.log('\n Pass DEBUG=True to see stack trace'); - } } }; diff --git a/scripts/set-default-contact/readme.md b/scripts/set-default-contact/readme.md index ce854948e2a..a69494d25df 100644 --- a/scripts/set-default-contact/readme.md +++ b/scripts/set-default-contact/readme.md @@ -38,12 +38,4 @@ End * If a place had a contact set as default, but that contact was deleted, it is still considered as having a default contact already and will be skipped. The fix to this is to manually set the place to another contact and then unset that to be empty. * If two users are assigned to the same place, the one that is processed first will be set. The second (or Nth) user will be skipped because the place will already have a user -## Development - -This script has it's own ESLint rules found in the `.eslintrc` file. As of Dec 2022, the main difference is that it uses `ecmaVersion` of `8` instead of `6` like in `cht-core`. To ensure any changes comply, use this call before committing: - -```shell -../../node_modules/.bin/eslint index.js -``` - -Otherwise, use Docker Helper to spin up a local instance to test against to ensure contacts are set as default for places when users have them both assigned. +Use Docker Helper to spin up a local instance to test against to ensure contacts are set as default for places when users have them both assigned. diff --git a/tests/e2e/upgrade/wdio.conf.js b/tests/e2e/upgrade/wdio.conf.js index 8c32ab1e228..5b66b3026e6 100644 --- a/tests/e2e/upgrade/wdio.conf.js +++ b/tests/e2e/upgrade/wdio.conf.js @@ -7,7 +7,6 @@ const fs = require('fs'); const os = require('os'); const chai = require('chai'); chai.use(require('chai-exclude')); -const rpn = require('request-promise-native'); const semver = require('semver'); const utils = require('@utils'); @@ -31,7 +30,11 @@ const MAIN_BRANCH = 'medic:medic:master'; const COMPOSE_FILES = ['cht-core', 'cht-couchdb']; const getUpgradeServiceDockerCompose = async () => { - const contents = (await rpn.get('https://raw.githubusercontent.com/medic/cht-upgrade-service/main/docker-compose.yml')); + const contents = await utils.request({ + uri: 'https://raw.githubusercontent.com/medic/cht-upgrade-service/main/docker-compose.yml', + json: false, + noAuth: true, + }); await fs.promises.writeFile(UPGRADE_SERVICE_DC, contents); }; @@ -44,7 +47,7 @@ const getReleasesQuery = () => { startKey.push({}); } return { - start_key: JSON.stringify(startKey), + start_key: startKey, descending: true, limit: TAG ? 2 : 1, }; @@ -56,7 +59,12 @@ const getRelease = async () => { } const url = `${MARKET_URL_READ}/${STAGING_SERVER}/_design/builds/_view/releases`; - const releases = await rpn.get({ url: url, qs: getReleasesQuery(), json: true }); + const releases = await utils.request({ + uri: url, + qs: getReleasesQuery(), + noAuth: true, + }); + if (!releases.rows.length) { return MAIN_BRANCH; } @@ -68,7 +76,11 @@ const getMainCHTDockerCompose = async () => { const release = await getRelease(); for (const composeFile of COMPOSE_FILES) { const composeFileUrl = `${MARKET_URL_READ}/${STAGING_SERVER}/${release}/docker-compose/${composeFile}.yml`; - const contents = await rpn.get(composeFileUrl); + const contents = await utils.request({ + uri: composeFileUrl, + json: false, + noAuth: true, + }); const filePath = path.join(CHT_DOCKER_COMPOSE_FOLDER, `${composeFile}.yml`); await fs.promises.writeFile(filePath, contents); } diff --git a/tests/scalability/initial-replication.js b/tests/scalability/initial-replication.js index 188724c09b3..bb0372c53fb 100644 --- a/tests/scalability/initial-replication.js +++ b/tests/scalability/initial-replication.js @@ -4,17 +4,22 @@ const config = require('./config.json'); const user = config.users[threadId % config.users.length]; const rewire = require('rewire'); -const rpn = require('request-promise-native'); const PouchDB = require('pouchdb'); PouchDB.plugin(require('pouchdb-adapter-memory')); const fetchJSON = async (url) => { - return await rpn.get({ - url: `${config.url}${url}`, - auth: { username: user.name, password: user.pass }, - json: true, + const response = await fetch(`${config.url}${url}`, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + btoa(`${user.name}:${user.pass}`), + } }); + if (response.ok) { + return await response.json(); + } + + throw new Error(await response.text()); }; const remoteDb = new PouchDB(`${config.url}/medic`, { diff --git a/tests/utils/index.js b/tests/utils/index.js index ad846f714c7..b4016c4a9e4 100644 --- a/tests/utils/index.js +++ b/tests/utils/index.js @@ -214,7 +214,7 @@ const getResponseBody = async (response, sendJson) => { const request = async (options, { debug } = {}) => { const { uri, options: requestInit, resolveWithFullResponse, sendJson } = getRequestOptions(options); if (debug) { - console.debug('SENDING REQUEST', JSON.stringify({ ...options, body: null }, null, 2)); + console.debug('SENDING REQUEST', JSON.stringify({ ...options, uri, body: null }, null, 2)); } const response = await fetch(uri, requestInit);