Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CHT-Api library should change behavior to adapt to differing cht-core version #133

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/commander": "^2.12.2",
"@types/mocha": "^10.0.6",
"@types/rewire": "^2.5.30",
"@types/semver": "^7.5.8",
"@types/sinon": "^17.0.2",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
Expand All @@ -60,6 +61,7 @@
"eslint-plugin-promise": "^6.1.1",
"mocha": "^10.2.0",
"rewire": "^7.0.0",
"semver": "^7.6.0",
"sinon": "^17.0.1",
"ts-mocha": "^10.0.0",
"tsc-watch": "^6.0.4"
Expand Down
2 changes: 1 addition & 1 deletion src/lib/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ChtSession from './cht-session';
const LOGIN_EXPIRES_AFTER_MS = 4 * 24 * 60 * 60 * 1000;
const QUEUE_SESSION_EXPIRATION = '96h';
const { COOKIE_PRIVATE_KEY, WORKER_PRIVATE_KEY } = process.env;
const PRIVATE_KEY_SALT = '_'; // change to logout all users
const PRIVATE_KEY_SALT = '2'; // change to logout all users
const COOKIE_SIGNING_KEY = COOKIE_PRIVATE_KEY + PRIVATE_KEY_SALT;

export default class Auth {
Expand Down
162 changes: 116 additions & 46 deletions src/lib/cht-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import _ from 'lodash';
import { AxiosInstance } from 'axios';
import * as semver from 'semver';

import ChtSession from './cht-session';
import { Config, ContactType } from '../config';
import { UserPayload } from '../services/user-payload';
Expand All @@ -18,17 +20,44 @@ export type PlacePayload = {
[key: string]: any;
};

export type CreatedPlaceResult = {
placeId: string;
contactId?: string;
};

export class ChtApi {
public readonly chtSession: ChtSession;
private axiosInstance: AxiosInstance;
protected axiosInstance: AxiosInstance;
private session: ChtSession;
private version: string;

constructor(session: ChtSession) {
this.chtSession = session;
protected constructor(session: ChtSession) {
this.session = session;
this.axiosInstance = session.axiosInstance;
this.version = 'base';
}

public static create(chtSession: ChtSession): ChtApi {
let result;
const coercedVersion = semver.valid(semver.coerce(chtSession.chtCoreVersion));
if (!coercedVersion) {
throw Error(`invalid cht core version "${chtSession.chtCoreVersion}"`);
}

if (semver.gte(coercedVersion, '4.7.0') || chtSession.chtCoreVersion === '4.6.0-local-development') {
result = new ChtApi_4_7(chtSession);
result.version = '4.7';
} else if (semver.gte(coercedVersion, '4.6.0')) {
result = new ChtApi_4_6(chtSession);
result.version = '4.6';
} else {
result = new ChtApi(chtSession);
}

return result;
}

// workaround https://github.com/medic/cht-core/issues/8674
updateContactParent = async (parentId: string): Promise<string> => {
async updateContactParent(parentId: string): Promise<string> {
const parentDoc = await this.getDoc(parentId);
const contactId = parentDoc?.contact?._id;
if (!contactId) {
Expand All @@ -50,24 +79,32 @@ export class ChtApi {
}

return contactDoc._id;
};
}

createPlace = async (payload: PlacePayload): Promise<string> => {
async createPlace(payload: PlacePayload): Promise<CreatedPlaceResult> {
const url = `api/v1/places`;
console.log('axios.post', url);
const resp = await this.axiosInstance.post(url, payload);
return resp.data.id;
};
return {
placeId: resp.data.id,
contactId: resp.data.contact?.id,
};
}

// because there is no PUT for /api/v1/places
createContact = async (payload: PlacePayload): Promise<string> => {
async createContact(payload: PlacePayload): Promise<string> {
const payloadWithPlace = {
...payload.contact,
place: payload._id,
};

const url = `api/v1/people`;
console.log('axios.post', url);
const resp = await this.axiosInstance.post(url, payload.contact);
const resp = await this.axiosInstance.post(url, payloadWithPlace);
return resp.data.id;
};
}

updatePlace = async (payload: PlacePayload, contactId: string): Promise<any> => {
async updatePlace(payload: PlacePayload, contactId: string): Promise<any> {
const doc: any = await this.getDoc(payload._id);

const payloadClone:any = _.cloneDeep(payload);
Expand All @@ -90,9 +127,9 @@ export class ChtApi {
}

return doc;
};
}

deleteDoc = async (docId: string): Promise<void> => {
async deleteDoc(docId: string): Promise<void> {
const doc: any = await this.getDoc(docId);

const deleteContactUrl = `medic/${doc._id}?rev=${doc._rev}`;
Expand All @@ -101,49 +138,34 @@ export class ChtApi {
if (!resp.data.ok) {
throw Error('response from chtApi.deleteDoc was not OK');
}
};
}

disableUsersWithPlace = async (placeId: string): Promise<string[]> => {
async disableUsersWithPlace(placeId: string): Promise<string[]> {
const usersToDisable: string[] = await this.getUsersAtPlace(placeId);
for (const userDocId of usersToDisable) {
await this.disableUser(userDocId);
}
return usersToDisable;
};

disableUser = async (docId: string): Promise<void> => {
const username = docId.substring('org.couchdb.user:'.length);
const url = `api/v1/users/${username}`;
console.log('axios.delete', url);
return this.axiosInstance.delete(url);
};
}

deactivateUsersWithPlace = async (placeId: string): Promise<string[]> => {
async deactivateUsersWithPlace(placeId: string): Promise<string[]> {
const usersToDeactivate: string[] = await this.getUsersAtPlace(placeId);
for (const userDocId of usersToDeactivate) {
await this.deactivateUser(userDocId);
}
return usersToDeactivate;
};

deactivateUser = async (docId: string): Promise<void> => {
const username = docId.substring('org.couchdb.user:'.length);
const url = `api/v1/users/${username}`;
console.log('axios.post', url);
const deactivationPayload = { roles: ['deactivated' ]};
return this.axiosInstance.post(url, deactivationPayload);
};
}

createUser = async (user: UserPayload): Promise<void> => {
async createUser(user: UserPayload): Promise<void> {
const url = `api/v1/users`;
console.log('axios.post', url);
const axiosRequestionConfig = {
'axios-retry': { retries: 0 }, // upload-manager handles retries for this
};
await this.axiosInstance.post(url, user, axiosRequestionConfig);
};
}

getParentAndSibling = async (parentId: string, contactType: ContactType): Promise<{ parent: any; sibling: any }> => {
async getParentAndSibling(parentId: string, contactType: ContactType): Promise<{ parent: any; sibling: any }> {
const url = `medic/_design/medic/_view/contacts_by_depth`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url, {
Expand All @@ -160,7 +182,7 @@ export class ChtApi {
const parent = docs.find((d: any) => d.contact_type === parentType);
const sibling = docs.find((d: any) => d.contact_type === contactType.name);
return { parent, sibling };
};
}

getPlacesWithType = async (placeType: string)
: Promise<any[]> => {
Expand All @@ -174,14 +196,15 @@ export class ChtApi {
return resp.data.rows.map((row: any) => row.doc);
};

getDoc = async (id: string): Promise<any> => {
const url = `medic/${id}`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url);
return resp.data;
};
public get chtSession(): ChtSession {
return this.session.clone();
}

private async getUsersAtPlace(placeId: string): Promise<string[]> {
public get coreVersion(): string {
return this.version;
}

protected async getUsersAtPlace(placeId: string): Promise<string[]> {
const url = `_users/_find`;
const payload = {
selector: {
Expand All @@ -193,6 +216,53 @@ export class ChtApi {
const resp = await this.axiosInstance.post(url, payload);
return resp.data?.docs?.map((d: any) => d._id);
}

private async getDoc(id: string): Promise<any> {
const url = `medic/${id}`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url);
return resp.data;
}

private async deactivateUser(docId: string): Promise<void> {
const username = docId.substring('org.couchdb.user:'.length);
const url = `api/v1/users/${username}`;
console.log('axios.post', url);
const deactivationPayload = { roles: ['deactivated' ]};
return this.axiosInstance.post(url, deactivationPayload);
}

private async disableUser(docId: string): Promise<void> {
const username = docId.substring('org.couchdb.user:'.length);
const url = `api/v1/users/${username}`;
console.log('axios.delete', url);
return this.axiosInstance.delete(url);
}
}

class ChtApi_4_6 extends ChtApi {
public constructor(session: ChtSession) {
super(session);
}

// #8674: assign parent place to new contacts
public override updateContactParent = async (): Promise<string> => {
throw Error(`program should never update contact's parent after cht-core 4.6`);
};
}

class ChtApi_4_7 extends ChtApi_4_6 {
public constructor(session: ChtSession) {
super(session);
}

// #8877: Look up users from their facility_id or contact_id
protected override async getUsersAtPlace(placeId: string): Promise<string[]> {
const url = `api/v2/users?facility_id=${placeId}`;
console.log('axios.get', url);
const resp = await this.axiosInstance.get(url);
return resp.data?.map((d: any) => d.id);
}
}

function minify(doc: any): any {
Expand Down
Loading
Loading