Skip to content

Commit

Permalink
Warning System (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
kennsippell authored Jan 9, 2025
1 parent fbe22da commit 97aea6e
Show file tree
Hide file tree
Showing 43 changed files with 1,170 additions and 313 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ property_name | string | Defines how the value will be stored on the object.
type | ConfigPropertyType | Defines the validation rules, and auto-formatting rules. See [ConfigPropertyType](#ConfigPropertyType).
parameter | any | See [ConfigPropertyType](#ConfigPropertyType).
required | boolean | True if the object should not exist without this information.
unique | 'all' or 'parent' | Dismissable warnings are flagged if a place already exists with this attribute's value. Values can be `all` (warns if any place has the same value) or `parent` (warns if a place with the same parent has the same value). This can only be defined on a `place_properties` or `contact_properties`.

#### ConfigPropertyType
The `ConfigPropertyType` defines a property's validation rules and auto-formatting rules. The optional `parameter` information alters the behavior of the `ConfigPropertyType`.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cht-user-management",
"version": "2.0.0",
"version": "2.1.0",
"main": "dist/index.js",
"dependencies": {
"@bull-board/api": "^5.17.0",
Expand Down
2 changes: 1 addition & 1 deletion scripts/create-user-managers/create-user-managers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function parseCommandlineArguments(argv: string[]): CommandLineArgs {
}

async function getPlaceDocId(county: string | undefined, chtApi: typeof ChtApi) {
const counties = await RemotePlaceCache.getPlacesWithType(chtApi, UserManagerContactType, UserManagerContactType.hierarchy[0]);
const counties = await RemotePlaceCache.getRemotePlaces(chtApi, UserManagerContactType, UserManagerContactType.hierarchy[0]);
const countyMatches = counties.filter((c: RemotePlace) => !county || PropertyValues.isMatch(county, c.name));
if (countyMatches.length < 1) {
throw Error(`Could not find county "${county}"`);
Expand Down
6 changes: 4 additions & 2 deletions src/config/chis-civ/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"property_name": "name",
"type": "generated",
"parameter": "Zone de supervision de: {{ contact.last_name }} {{ contact.first_name }}",
"required": false
"required": false,
"unique": "parent"
}
],
"contact_properties": [
Expand Down Expand Up @@ -147,7 +148,8 @@
"type": "string",
"parameter": ["\\(*Site*\\)"],
"required": true,
"hint": "Nom du site de l'ASC"
"hint": "Nom du site de l'ASC",
"unique": "parent"
}
],
"contact_properties": [
Expand Down
12 changes: 8 additions & 4 deletions src/config/chis-ke/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,17 @@
"property_name": "name",
"type": "name",
"parameter": ["\\sCommunity Health Unit", "\\sCommunity Unit", "\\sUnit", "\\sCU", "\\sCHU"],
"required": true
"required": true,
"unique": "parent"
},
{
"friendly_name": "CHU Code",
"property_name": "code",
"type": "regex",
"parameter": "^\\d{6}$",
"errorDescription": "Expects a six digit number (eg. 123456)",
"required": true
"required": true,
"unique": "all"
},
{
"friendly_name": "Link Facility Name",
Expand Down Expand Up @@ -319,7 +321,8 @@
"property_name": "name",
"type": "generated",
"parameter": "{{ contact.name }} Area",
"required": true
"required": true,
"unique": "parent"
}
],
"contact_properties": [
Expand All @@ -334,7 +337,8 @@
"property_name": "phone",
"parameter": "KE",
"type": "phone",
"required": true
"required": true,
"unique": "all"
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion src/config/chis-tg/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"property_name": "name",
"type": "generated",
"parameter": "{{ contact.last_name }} {{ contact.first_name }} ({{ lineage.sanitary_training }})",
"required": false
"required": false,
"unique": "parent"
}
],
"contact_properties": [
Expand Down
6 changes: 4 additions & 2 deletions src/config/chis-ug/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"property_name": "name",
"type": "generated",
"parameter": "{{ contact.name }} Area ({{ place.village }})",
"required": true
"required": true,
"unique": "parent"
},

{
Expand Down Expand Up @@ -104,7 +105,8 @@
"property_name": "phone",
"type": "phone",
"parameter": "UG",
"required": true
"required": true,
"unique": "all"
},
{
"friendly_name": "Date of Birth",
Expand Down
36 changes: 18 additions & 18 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type HierarchyConstraint = {
required: boolean;
parameter? : string | string[] | object;
errorDescription? : string;
unique?: string;

contact_type: string;
level: number;
Expand All @@ -51,6 +52,7 @@ export type ContactProperty = {
required: boolean;
parameter? : string | string[] | object;
errorDescription? : string;
unique?: string;
};

export type AuthenticationInfo = {
Expand Down Expand Up @@ -192,6 +194,12 @@ export class Config {
return _.sortBy(domains, 'friendly');
}

public static getUniqueProperties(contactTypeName: string): ContactProperty[] {
const contactMatch = config.contact_types.find(c => c.name === contactTypeName);
const uniqueProperties = contactMatch?.place_properties.filter(prop => prop.unique);
return uniqueProperties || [];
}

// TODO: Joi? Chai?
public static assertValid({ config }: PartnerConfig = partnerConfig) {
for (const contactType of config.contact_types) {
Expand All @@ -206,6 +214,16 @@ export class Config {
Config.getPropertyWithName(contactType.place_properties, 'name');
Config.getPropertyWithName(contactType.contact_properties, 'name');

const parentLevel = contactType.hierarchy.find(hierarchy => hierarchy.level === 1);
if (!parentLevel) {
throw Error(`Must have a hierarchy with parent level (level: 1)`);
}

const invalidPropsWithUnique = allHierarchyProperties.filter(prop => prop.unique);
if (invalidPropsWithUnique.length) {
throw Error(`Only place_properties and contact_properties can have properties with "unique" values`);
}

allProperties.forEach(property => {
if (!KnownContactPropertyTypes.includes(property.type)) {
throw Error(`Unknown property type "${property.type}"`);
Expand All @@ -218,23 +236,5 @@ export class Config {
}
}
}

public static getCsvTemplateColumns(placeType: string) {
const placeTypeConfig = Config.getContactType(placeType);
const hierarchy = Config.getHierarchyWithReplacement(placeTypeConfig);
const userRoleConfig = Config.getUserRoleConfig(placeTypeConfig);

const extractColumns = (properties: ContactProperty[]) => properties
.filter(p => p.type !== 'generated')
.map(p => p.friendly_name);

const columns = _.uniq([
...hierarchy.map(p => p.friendly_name),
...extractColumns(placeTypeConfig.place_properties),
...extractColumns(placeTypeConfig.contact_properties),
...(Config.hasMultipleRoles(placeTypeConfig) ? [userRoleConfig.friendly_name] : []),
]);
return columns;
}
}

89 changes: 59 additions & 30 deletions src/lib/remote-place-cache.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Place from '../services/place';
import _ from 'lodash';

import Place, { FormattedPropertyCollection } from '../services/place';
import { ChtApi } from './cht-api';
import { IPropertyValue } from '../property-value';
import { ContactType, HierarchyConstraint } from '../config';
import { NamePropertyValue } from '../property-value/name-property-value';
import { IPropertyValue, RemotePlacePropertyValue } from '../property-value';
import { Config, ContactProperty, ContactType, HierarchyConstraint } from '../config';

type RemotePlacesByType = {
[key: string]: RemotePlace[];
Expand All @@ -17,26 +18,37 @@ export type RemotePlace = {
name: IPropertyValue;
lineage: string[];
ambiguities?: RemotePlace[];
placeType: string;
uniquePlaceValues: FormattedPropertyCollection;

// these are expensive to fetch on remote places; but are available on staged places
uniqueContactValues?: FormattedPropertyCollection;

// sadly, sometimes invalid or uncreated objects "pretend" to be remote
// should reconsider this naming
type: 'remote' | 'local' | 'invalid';
stagedPlace?: Place;
};

export default class RemotePlaceCache {
private static cache: RemotePlaceDatastore = {};

public static async getPlacesWithType(chtApi: ChtApi, contactType: ContactType, hierarchyLevel: HierarchyConstraint)
: Promise<RemotePlace[]> {
const domainStore = await RemotePlaceCache.getDomainStore(chtApi, contactType, hierarchyLevel);
return domainStore;
public static async getRemotePlaces(chtApi: ChtApi, contactType: ContactType, atHierarchyLevel?: HierarchyConstraint): Promise<RemotePlace[]> {
const hierarchyLevels = Config.getHierarchyWithReplacement(contactType, 'desc');
const fetchAll = hierarchyLevels.map(hierarchyLevel => this.fetchCachedOrRemotePlaces(chtApi, hierarchyLevel));
const allRemotePlaces = _.flatten(await Promise.all(fetchAll));
if (!atHierarchyLevel) {
return allRemotePlaces;
}

return allRemotePlaces.filter(remotePlace => remotePlace.placeType === atHierarchyLevel.contact_type);
}

public static add(place: Place, chtApi: ChtApi): void {
const { domain } = chtApi.chtSession.authInfo;
const placeType = place.type.name;

const places = RemotePlaceCache.cache[domain]?.[placeType];
const places = this.cache[domain]?.[placeType];
// if there is no cache existing, discard the value
// it will be fetched if needed when the cache is built
if (places) {
Expand All @@ -47,46 +59,63 @@ export default class RemotePlaceCache {
public static clear(chtApi: ChtApi, contactTypeName?: string): void {
const domain = chtApi?.chtSession?.authInfo?.domain;
if (!domain) {
RemotePlaceCache.cache = {};
this.cache = {};
} else if (!contactTypeName) {
delete RemotePlaceCache.cache[domain];
} else if (RemotePlaceCache.cache[domain]) {
delete RemotePlaceCache.cache[domain][contactTypeName];
delete this.cache[domain];
} else if (this.cache[domain]) {
delete this.cache[domain][contactTypeName];
}
}

private static async getDomainStore(chtApi: ChtApi, contactType: ContactType, hierarchyLevel: HierarchyConstraint)
// check if places are known and if they aren't, fetch via api
private static async fetchCachedOrRemotePlaces(chtApi: ChtApi, hierarchyLevel: HierarchyConstraint)
: Promise<RemotePlace[]> {
const { domain } = chtApi.chtSession.authInfo;
const placeType = hierarchyLevel.contact_type;
const { cache: domainCache } = RemotePlaceCache;
const places = domainCache[domain]?.[placeType];
if (!places) {
const fetchPlacesWithType = RemotePlaceCache.fetchRemotePlaces(chtApi, contactType, hierarchyLevel);
domainCache[domain] = {
...domainCache[domain],
[placeType]: await fetchPlacesWithType,
};
const fetchPlacesWithType = this.fetchRemotePlacesAtLevel(chtApi, hierarchyLevel);
if (!domainCache[domain]) {
domainCache[domain] = {};
}
domainCache[domain][placeType] = await fetchPlacesWithType;
}

return domainCache[domain][placeType];
}

private static async fetchRemotePlaces(chtApi: ChtApi, contactType: ContactType, hierarchyLevel: HierarchyConstraint): Promise<RemotePlace[]> {
function extractLineage(doc: any): string[] {
if (doc?.parent) {
return [doc.parent._id, ...extractLineage(doc.parent)];
// fetch docs of type and convert to RemotePlace
private static async fetchRemotePlacesAtLevel(chtApi: ChtApi, hierarchyLevel: HierarchyConstraint): Promise<RemotePlace[]> {
const uniqueKeyProperties = Config.getUniqueProperties(hierarchyLevel.contact_type);
const docs = await chtApi.getPlacesWithType(hierarchyLevel.contact_type);
return docs.map((doc: any) => this.convertContactToRemotePlace(doc, uniqueKeyProperties, hierarchyLevel));
}

private static convertContactToRemotePlace(doc: any, uniqueKeyProperties: ContactProperty[], hierarchyLevel: HierarchyConstraint): RemotePlace {
const uniqueKeyStringValues: FormattedPropertyCollection = {};
for (const property of uniqueKeyProperties) {
const value = doc[property.property_name];
if (value) {
uniqueKeyStringValues[property.property_name] = new RemotePlacePropertyValue(value, property);
}

return [];
}

const docs = await chtApi.getPlacesWithType(hierarchyLevel.contact_type);
return docs.map((doc: any): RemotePlace => ({
return {
id: doc._id,
name: new NamePropertyValue(doc.name, hierarchyLevel),
lineage: extractLineage(doc),
name: new RemotePlacePropertyValue(doc.name, hierarchyLevel),
placeType: hierarchyLevel.contact_type,
lineage: this.extractLineage(doc),
uniquePlaceValues: uniqueKeyStringValues,
type: 'remote',
}));
};
}

private static extractLineage(doc: any): string[] {
if (doc?.parent) {
return [doc.parent._id, ...this.extractLineage(doc.parent)];
}

return [];
}
}
Loading

0 comments on commit 97aea6e

Please sign in to comment.