Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into 9586-implement-free…
Browse files Browse the repository at this point in the history
…text-search-in-cht-datasource
  • Loading branch information
sugat009 committed Nov 18, 2024
2 parents 43efbef + 51da792 commit ede85fd
Show file tree
Hide file tree
Showing 103 changed files with 2,657 additions and 1,813 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ env:
TAG: ${{ (github.ref_type == 'tag' && github.ref_name) || '' }}
BRANCH: ${{ github.head_ref || github.ref_name }}
BUILD_NUMBER: ${{ github.run_id }}
NODE_VERSION: '20.11'
NODE_VERSION: '22.11'

jobs:

Expand Down
4 changes: 1 addition & 3 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
FROM alpine:3.19 AS base_build
FROM node:22-alpine AS base_build

RUN apk add --update --no-cache \
build-base \
curl \
nodejs~=20 \
npm~=10 \
tzdata \
libxslt \
bash \
Expand Down
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"url": "git://github.com/medic/cht-core.git"
},
"engines": {
"node": ">=20.11.0",
"npm": ">=10.2.4"
"node": ">=22.11.0",
"npm": ">=10.9.0"
},
"scripts": {
"toc": "doctoc --github --maxlevel 2 README.md",
Expand Down
12 changes: 8 additions & 4 deletions api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,27 @@ process
await migrations.run();
logger.info('Database migrations completed successfully');

startupLog.start('forms');
logger.info('Generating manifest');
await manifest.generate();
logger.info('Manifest generated successfully');

logger.info('Generating service worker');
await generateServiceWorker.run(true);
logger.info('Service worker generated successfully');
} catch (err) {
logger.error('Fatal error initialising API');
logger.error('%o', err);
process.exit(1);
}

try {
startupLog.start('forms');
logger.info('Updating xforms…');
await generateXform.updateAll();
logger.info('xforms updated successfully');

} catch (err) {
logger.error('Fatal error initialising API');
logger.error('Error initialising API');
logger.error('%o', err);
process.exit(1);
}

startupLog.complete();
Expand Down
6 changes: 6 additions & 0 deletions api/src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ PouchDB.plugin(require('pouchdb-adapter-http'));
PouchDB.plugin(require('pouchdb-session-authentication'));
PouchDB.plugin(require('pouchdb-find'));
PouchDB.plugin(require('pouchdb-mapreduce'));
const asyncLocalStorage = require('./services/async-storage');
const { REQUEST_ID_HEADER } = require('./server-utils');

const { UNIT_TEST_ENV } = process.env;

Expand Down Expand Up @@ -74,6 +76,10 @@ if (UNIT_TEST_ENV) {
const fetch = (url, opts) => {
// Adding audit flag (haproxy) Service that made the request initially.
opts.headers.set('X-Medic-Service', 'api');
const requestId = asyncLocalStorage.getRequestId();
if (requestId) {
opts.headers.set(REQUEST_ID_HEADER, requestId);
}
return PouchDB.fetch(url, opts);
};

Expand Down
11 changes: 9 additions & 2 deletions api/src/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const dbDocHandler = require('./controllers/db-doc');
const extensionLibs = require('./controllers/extension-libs');
const replication = require('./controllers/replication');
const app = express.Router({ strict: true });
const asyncLocalStorage = require('./services/async-storage');
const moment = require('moment');
const MAX_REQUEST_SIZE = '32mb';

Expand Down Expand Up @@ -158,9 +159,15 @@ if (process.argv.slice(2).includes('--allow-cors')) {
});
}

const shortUuid = () => {
const ID_LENGTH = 12;
return uuid.v4().replace(/-/g, '').toLowerCase().slice(0, ID_LENGTH);
};

app.use((req, res, next) => {
req.id = uuid.v4();
next();
req.id = shortUuid();
req.headers[serverUtils.REQUEST_ID_HEADER] = req.id;
asyncLocalStorage.set(req, () => next());
});
app.use(getLocale);

Expand Down
2 changes: 2 additions & 0 deletions api/src/server-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const environment = require('@medic/environment');
const isClientHuman = require('./is-client-human');
const logger = require('@medic/logger');
const MEDIC_BASIC_AUTH = 'Basic realm="Medic Web Services"';
const REQUEST_ID_HEADER = 'X-Request-Id';
const cookie = require('./services/cookie');
const {InvalidArgumentError} = require('@medic/cht-datasource');

Expand Down Expand Up @@ -49,6 +50,7 @@ const promptForBasicAuth = res => {

module.exports = {
MEDIC_BASIC_AUTH: MEDIC_BASIC_AUTH,
REQUEST_ID_HEADER: REQUEST_ID_HEADER,

/*
* Attempts to determine the correct response given the error code.
Expand Down
17 changes: 17 additions & 0 deletions api/src/services/async-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const { REQUEST_ID_HEADER } = require('../server-utils');

const request = require('@medic/couch-request');

module.exports = {
set: (req, callback) => {
asyncLocalStorage.run({ clientRequest: req }, callback);
},
getRequestId: () => {
const localStorage = asyncLocalStorage.getStore();
return localStorage?.clientRequest?.id;
},
};

request.initialize(module.exports, REQUEST_ID_HEADER);
1 change: 0 additions & 1 deletion api/src/services/setup/view-indexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ const indexView = async (dbName, ddocId, viewName) => {
uri: `${environment.serverUrl}/${dbName}/${ddocId}/_view/${viewName}`,
json: true,
qs: { limit: 1 },
timeout: 2000,
});
} catch (requestError) {
if (!continueIndexing) {
Expand Down
37 changes: 37 additions & 0 deletions api/tests/mocha/db.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const sinon = require('sinon');
require('chai').use(require('chai-as-promised'));
const PouchDB = require('pouchdb-core');
const { expect } = require('chai');
const rewire = require('rewire');
const request = require('@medic/couch-request');
Expand All @@ -8,6 +9,7 @@ let db;
let unitTestEnv;

const env = require('@medic/environment');
const asyncLocalStorage = require('../../src/services/async-storage');

describe('db', () => {
beforeEach(() => {
Expand Down Expand Up @@ -404,4 +406,39 @@ describe('db', () => {
.to.be.rejectedWith(Error, `Cannot add security: invalid db name dbanme or role`);
});
});

describe('fetch extension', () => {
it('should set headers where there is an active client request', async () => {
sinon.stub(PouchDB, 'fetch').resolves({
json: sinon.stub().resolves({ result: true }),
ok: true,
});
sinon.stub(asyncLocalStorage, 'getRequestId').returns('the_id');
db = rewire('../../src/db');

await db.medic.info();
const headers = PouchDB.fetch.args.map(arg => arg[1].headers);
expect(headers.length).to.equal(4);
headers.forEach((header) => {
expect(header.get('X-Medic-Service')).to.equal('api');
expect(header.get('X-Request-Id')).to.equal('the_id');
});
});

it('should work when call is made without an active clinet request', async () => {
sinon.stub(PouchDB, 'fetch').resolves({
json: sinon.stub().resolves({ result: true }),
ok: true,
});
sinon.stub(asyncLocalStorage, 'getRequestId').returns(undefined);
db = rewire('../../src/db');

await db.medic.info();
const headers = PouchDB.fetch.args.map(arg => arg[1].headers);
expect(headers.length).to.equal(4);
headers.forEach((header) => {
expect(header.get('X-Medic-Service')).to.equal('api');
});
});
});
});
4 changes: 4 additions & 0 deletions api/tests/mocha/server-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,8 @@ describe('Server utils', () => {
chai.expect(serverUtilsError.calledOnceWithExactly(error, req, res)).to.be.true;
});
});

it('should export request header', () => {
chai.expect(serverUtils.REQUEST_ID_HEADER).to.equal('X-Request-Id');
});
});
62 changes: 62 additions & 0 deletions api/tests/mocha/services/async-storage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const sinon = require('sinon');
const rewire = require('rewire');
const { expect } = require('chai');
const request = require('@medic/couch-request');
const serverUtils = require('../../../src/server-utils');

describe('async-storage', () => {
let service;

beforeEach(() => {
sinon.stub(request, 'initialize');
});

afterEach(() => {
sinon.restore();
});

it('should initialize async storage and initialize couch-request', async () => {
service = rewire('../../../src/services/async-storage');
expect(request.initialize.args).to.deep.equal([[
service,
serverUtils.REQUEST_ID_HEADER
]]);
});

it('set should set request uuid', () => {
service = rewire('../../../src/services/async-storage');
const asyncLocalStorage = service.__get__('asyncLocalStorage');
sinon.stub(asyncLocalStorage, 'run');

const req = { this: 'is a req' };
const cb = sinon.stub();
Object.freeze(req);
service.set(req, cb);
expect(asyncLocalStorage.run.args).to.deep.equal([[
{ clientRequest: req },
cb
]]);
});

it('getRequestId should return request id when set', done => {
service = rewire('../../../src/services/async-storage');
const req = { id: 'uuid' };
service.set(req, () => {
expect(service.getRequestId()).to.equal('uuid');
done();
});
});

it('getRequestId should return nothing when there is no local storage', () => {
service = rewire('../../../src/services/async-storage');
expect(service.getRequestId()).to.equal(undefined);
});

it('getRequestId should return nothing when there is no client request', done => {
service = rewire('../../../src/services/async-storage');
service.set(undefined, () => {
expect(service.getRequestId()).to.equal(undefined);
done();
});
});
});
13 changes: 3 additions & 10 deletions api/tests/mocha/services/setup/view-indexer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const db = require('../../../../src/db');
const env = require('@medic/environment');
const request = require('@medic/couch-request');
const databases = require('../../../../src/services/setup/databases');
const upgradeLogService = require('../../../../src/services/setup/upgrade-log');
const upgradeLogService = require('../../../../src/service' +
's/setup/upgrade-log');

let viewIndexer;

Expand Down Expand Up @@ -60,38 +61,33 @@ describe('View indexer service', () => {
uri: 'http://localhost/thedb/_design/:staged:one/_view/view1',
json: true,
qs: { limit: 1 },
timeout: 2000,
}],
[{
uri: 'http://localhost/thedb/_design/:staged:one/_view/view2',
json: true,
qs: { limit: 1 },
timeout: 2000,
}],
[{
uri: 'http://localhost/thedb/_design/:staged:one/_view/view3',
json: true,
qs: { limit: 1 },
timeout: 2000,
}],
[{
uri: 'http://localhost/thedb/_design/:staged:three/_view/view4',
json: true,
qs: { limit: 1 },
timeout: 2000,
}],
[{
uri: 'http://localhost/thedb-users-meta/_design/:staged:four/_view/view',
json: true,
qs: { limit: 1 },
timeout: 2000,
}],
]);
});
});

describe('indexView', () => {
it('should query the view with a timeout', async () => {
it('should query the view', async () => {
sinon.stub(request, 'get').resolves();
sinon.stub(env, 'serverUrl').value('http://localhost');

Expand All @@ -102,7 +98,6 @@ describe('View indexer service', () => {
uri: 'http://localhost/medic/_design/:staged:medic/_view/contacts',
json: true,
qs: { limit: 1 },
timeout: 2000,
}]);
});

Expand All @@ -119,7 +114,6 @@ describe('View indexer service', () => {
uri: 'http://localhost/other/_design/mydesign/_view/viewname',
json: true,
qs: { limit: 1 },
timeout: 2000,
};
expect(request.get.args).to.deep.equal(Array.from({ length: 21 }).map(() => [params]));
});
Expand All @@ -137,7 +131,6 @@ describe('View indexer service', () => {
uri: 'http://localhost/other/_design/mydesign/_view/viewname',
json: true,
qs: { limit: 1 },
timeout: 2000,
};
expect(request.get.args).to.deep.equal(Array.from({ length: 21 }).map(() => [params]));
});
Expand Down
3 changes: 2 additions & 1 deletion haproxy/default_frontend.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ frontend http-in
http-request capture req.hdr(x-medic-service) len 200 # capture.req.hdr(1)
http-request capture req.hdr(x-medic-user) len 200 # capture.req.hdr(2)
http-request capture req.hdr(user-agent) len 600 # capture.req.hdr(3)
http-request capture req.hdr(x-request-id) len 12 # capture.req.hdr(4)
capture response header Content-Length len 10 # capture.res.hdr(0)
http-response set-header Connection Keep-Alive
http-response set-header Keep-Alive timeout=18000
log global
log-format "%ci,%s,%ST,%Ta,%Ti,%TR,%[capture.req.method],%[capture.req.uri],%[capture.req.hdr(1)],%[capture.req.hdr(2)],'%[capture.req.hdr(0),lua.replacePassword]',%B,%Tr,%[capture.res.hdr(0)],'%[capture.req.hdr(3)]'"
log-format "%ci,%s,%ST,%Ta,%Ti,%TR,%[capture.req.method],%[capture.req.uri],%[capture.req.hdr(1)],%[capture.req.hdr(2)],%[capture.req.hdr(4)],'%[capture.req.hdr(0),lua.replacePassword]',%B,%Tr,%[capture.res.hdr(0)],'%[capture.req.hdr(3)]'"
default_backend couchdb-servers

Loading

0 comments on commit ede85fd

Please sign in to comment.