Skip to content

Commit

Permalink
Add offline ddoc logic to webapp along with initial contacts_by_freet…
Browse files Browse the repository at this point in the history
…ext view
  • Loading branch information
jkuester committed Oct 18, 2024
1 parent 06324e6 commit eeafdcc
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 6 deletions.
2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"scripts": {
"postinstall": "patch-package && ng cache clean",
"unit:mocha": "UNIT_TEST_ENV=1 mocha 'tests/mocha/**/*.spec.js'",
"unit:mocha": "UNIT_TEST_ENV=1 mocha 'tests/mocha/**/*.spec.js' && mocha --config tests/mocha/ts/.mocharc.js",
"unit:mocha:tz": "TZ=Canada/Pacific npm run unit:mocha && TZ=Africa/Monrovia npm run unit:mocha && TZ=Pacific/Auckland npm run unit:mocha",
"unit:cht-form": "ng test cht-form",
"unit": "UNIT_TEST_ENV=1 ng test webapp",
Expand Down
5 changes: 5 additions & 0 deletions webapp/src/js/offline-ddocs/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"globals": {
"emit": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module.exports.map = function(doc) {
const skip = [ '_id', '_rev', 'type', 'contact_type', 'refid', 'geolocation' ];

const usedKeys = [];
const emitMaybe = (key, value) => {
if (usedKeys.indexOf(key) === -1 && // Not already used
key.length > 2 // Not too short
) {
usedKeys.push(key);
emit([key], value);
}
};

const emitField = (key, value, order) => {
if (!value) {
return;
}
const lowerKey = key.toLowerCase();
if (skip.indexOf(lowerKey) !== -1 || /_date$/.test(lowerKey)) {
return;
}
if (typeof value === 'string') {
value
.toLowerCase()
.split(/\s+/)
.forEach((word) => emitMaybe(word, order));
}
};

const getTypeIndex = () => {
const types = [ 'district_hospital', 'health_center', 'clinic', 'person' ];
if (doc.type !== 'contact') {
return types.indexOf(doc.type);
}

const contactTypeIdx = types.indexOf(doc.contact_type);
if (contactTypeIdx >= 0) {
return contactTypeIdx;
}

return doc.contact_type;
};

const idx = getTypeIndex();
if (idx !== -1) {
const dead = !!doc.date_of_death;
const muted = !!doc.muted;
const order = dead + ' ' + muted + ' ' + idx + ' ' + (doc.name && doc.name.toLowerCase());
Object.keys(doc).forEach(function(key) {
emitField(key, doc[key], order);
});
}
};
3 changes: 2 additions & 1 deletion webapp/src/ts/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export class AppComponent implements OnInit, AfterViewInit {
});
}

ngOnInit(): void {
async ngOnInit() {
this.recordStartupTelemetry();
this.subscribeToStore();
this.setupRouter();
Expand All @@ -294,6 +294,7 @@ export class AppComponent implements OnInit, AfterViewInit {

// initialisation tasks that can occur after the UI has been rendered
this.setupPromise = Promise.resolve()
.then(() => this.dbSyncService.init())
.then(() => this.chtDatasourceService.isInitialized())
.then(() => this.checkPrivacyPolicy())
.then(() => (this.initialisationComplete = true))
Expand Down
28 changes: 28 additions & 0 deletions webapp/src/ts/services/db-sync.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { TranslateService } from '@mm-services/translate.service';
import { MigrationsService } from '@mm-services/migrations.service';
import { ReplicationService } from '@mm-services/replication.service';
import { PerformanceService } from '@mm-services/performance.service';
import medicOfflineDdoc from '@mm-services/offline-ddocs/medic-offline-freetext.ddoc';
import { DesignDoc } from '@mm-services/offline-ddocs/design-doc';

const READ_ONLY_TYPES = ['form', 'translations'];
const READ_ONLY_IDS = ['resources', 'branding', 'service-worker-meta', 'zscore-charts', 'settings', 'partners'];
Expand All @@ -24,6 +26,9 @@ const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
const META_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes
const BATCH_SIZE = 100;
const MAX_SUCCESSIVE_SYNCS = 2;
const OFFLINE_DDOCS = [
medicOfflineDdoc,
];

const readOnlyFilter = function(doc) {
// Never replicate "purged" documents upwards
Expand Down Expand Up @@ -63,6 +68,21 @@ type SyncState = {

type SyncStateListener = Parameters<Subject<SyncState>['subscribe']>[0];

const getRev = async (db, id: string): Promise<string | undefined> => db
.get(id)
.then(({ _rev }) => _rev as string)
.catch((e) => {
if (e.status === 404) {
return undefined;
}
throw e;
});

const initDdoc = (db) => async (ddoc: DesignDoc) => db.put({
...ddoc,
_rev: await getRev(db, ddoc._id),
});

@Injectable({
providedIn: 'root'
})
Expand Down Expand Up @@ -100,6 +120,14 @@ export class DBSyncService {
return !this.sessionService.isOnlineOnly();
}

init = async () => {
if (!this.isEnabled()) {
return;
}
const medicDb = await this.dbService.get();
return Promise.all(OFFLINE_DDOCS.map(initDdoc(medicDb)));
};

private replicateToRetry({ batchSize=BATCH_SIZE }={}) {
const telemetryEntry = new DbSyncTelemetry(
this.telemetryService,
Expand Down
11 changes: 11 additions & 0 deletions webapp/src/ts/services/offline-ddocs/design-doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface DesignDoc {
readonly _id: `_design/${string}`;
readonly _rev?: string;
readonly views: {
[key: string]: {
map: string;
};
};
}

export const packageView = ({ map }: { map: Function }) => ({ map: map.toString() });
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DesignDoc, packageView } from './design-doc';

import * as contactByFreetext from '../../../js/offline-ddocs/medic-offline-freetext/contacts_by_freetext.js';

export default <DesignDoc>{
_id: '_design/medic-offline-freetext',
views: {
contacts_by_freetext: packageView(contactByFreetext),
}
};
3 changes: 3 additions & 0 deletions webapp/tests/karma/ts/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe('AppComponent', () => {
isOnlineOnly: sinon.stub()
};
dbSyncService = {
init: sinon.stub().resolves(),
addUpdateListener: sinon.stub(),
isEnabled: sinon.stub().returns(false),
sync: sinon.stub(),
Expand Down Expand Up @@ -479,6 +480,7 @@ describe('AppComponent', () => {
}]);
expect(globalActions.updateReplicationStatus.getCall(1).args).to.deep.equal([{disabled: true}]);
expect(dbSyncService.subscribe.callCount).to.equal(1);
expect(dbSyncService.init.calledOnceWithExactly()).to.be.true;
});

it('should sync db if enabled', async () => {
Expand All @@ -499,6 +501,7 @@ describe('AppComponent', () => {

expect(dbSyncService.sync.callCount).to.equal(1);
expect(dbSyncService.subscribe.callCount).to.equal(1);
expect(dbSyncService.init.calledOnceWithExactly()).to.be.true;
});

it('should set dbSync replication status in subcription callback', async () => {
Expand Down
59 changes: 59 additions & 0 deletions webapp/tests/karma/ts/services/db-sync.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PerformanceService } from '@mm-services/performance.service';
import { TranslateService } from '@mm-services/translate.service';
import { MigrationsService } from '@mm-services/migrations.service';
import { ReplicationService } from '@mm-services/replication.service';
import medicOfflineDdoc from '@mm-services/offline-ddocs/medic-offline-freetext.ddoc';

describe('DBSync service', () => {
let service:DBSyncService;
Expand Down Expand Up @@ -102,6 +103,8 @@ describe('DBSync service', () => {
replicate: { to: to },
info: sinon.stub().resolves({ update_seq: 99 }),
allDocs: sinon.stub(),
get: sinon.stub(),
put: sinon.stub(),
};
localMetaDb = {
replicate: { to: metaTo, from: metaFrom },
Expand Down Expand Up @@ -148,6 +151,62 @@ describe('DBSync service', () => {
clock.restore();
});

describe('init', () => {
it('adds new ddoc to database', async () => {
isOnlineOnly.returns(false);
localMedicDb.get.rejects({ status: 404 });

await service.init();

expect(isOnlineOnly.calledOnceWithExactly()).to.be.true;
expect(db.calledOnceWithExactly()).to.be.true;
expect(localMedicDb.get.calledOnceWithExactly(medicOfflineDdoc._id)).to.be.true;
expect(localMedicDb.put.calledOnceWithExactly({
...medicOfflineDdoc,
_rev: undefined,
})).to.be.true;
});

it('updates existing ddoc in database', async () => {
isOnlineOnly.returns(false);
localMedicDb.get.resolves({ _rev: '1-abc' });

await service.init();

expect(isOnlineOnly.calledOnceWithExactly()).to.be.true;
expect(db.calledOnceWithExactly()).to.be.true;
expect(localMedicDb.get.calledOnceWithExactly(medicOfflineDdoc._id)).to.be.true;
expect(localMedicDb.put.calledOnceWithExactly({
...medicOfflineDdoc,
_rev: '1-abc',
})).to.be.true;
});

it('rejects when there is an error getting the ddoc', async () => {
isOnlineOnly.returns(false);
const expectedError = new Error('Error getting ddoc');
localMedicDb.get.rejects(expectedError);

await expect(service.init()).to.be.rejectedWith(expectedError);

expect(isOnlineOnly.calledOnceWithExactly()).to.be.true;
expect(db.calledOnceWithExactly()).to.be.true;
expect(localMedicDb.get.calledOnceWithExactly(medicOfflineDdoc._id)).to.be.true;
expect(localMedicDb.put.notCalled).to.be.true;
});

it('does nothing when online only', async () => {
isOnlineOnly.returns(true);

await service.init();

expect(isOnlineOnly.calledOnceWithExactly()).to.be.true;
expect(db.notCalled).to.be.true;
expect(localMedicDb.get.notCalled).to.be.true;
expect(localMedicDb.put.notCalled).to.be.true;
});
});

describe('sync', () => {
it('does nothing for admins', () => {
isOnlineOnly.returns(true);
Expand Down
8 changes: 8 additions & 0 deletions webapp/tests/mocha/ts/.mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const chaiAsPromised = require('chai-as-promised');
const chai = require('chai');
chai.use(chaiAsPromised);

module.exports = {
spec: 'tests/mocha/ts/**/*.spec.ts',
require: 'ts-node/register',
};
Loading

0 comments on commit eeafdcc

Please sign in to comment.