Skip to content

Commit

Permalink
working version
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanbataire committed Jan 14, 2025
1 parent 09971b7 commit 8c4f3c8
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 154 deletions.
21 changes: 8 additions & 13 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
Expand Up @@ -10,7 +10,7 @@
"@fastify/cookie": "^9.2.0",
"@fastify/env": "^4.2.0",
"@fastify/formbody": "^7.4.0",
"@fastify/multipart": "^8.0.0",
"@fastify/multipart": "^8.3.0",
"@fastify/static": "^7.0.1",
"@fastify/view": "^8.2.0",
"@types/html-minifier": "^4.0.5",
Expand Down
2 changes: 1 addition & 1 deletion src/config/chis-ke/gross.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default async function mutate(payload: PlacePayload, chtApi: ChtApi, isRe
payload[chpKey] = result;
};

const contactType = Config.getContactType(payload.contact_type);
const contactType = await Config.getContactType(payload.contact_type);
const { parent: chu, sibling } = await chtApi.getParentAndSibling(payload.parent, contactType);
if (!chu && !sibling) {
throw Error(`CHU does not exist`);
Expand Down
87 changes: 0 additions & 87 deletions src/config/chis-ug/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,93 +15,6 @@
}
],
"contact_types": [
{
"name": "c40-parish",
"friendly": "Parish",
"contact_type": "person",
"user_role": ["chew"],
"username_from_place": true,
"deactivate_users_on_replace": true,
"hierarchy": [
{
"friendly_name": "Health Facility",
"property_name": "HEALTHFACILITY",
"contact_type": "c30-district_hospital",
"type": "name",
"required": true,
"parameter": ["\\sH[ /]*F\\s"],
"level": 1
}
],
"replacement_property": {
"friendly_name": "Outgoing CHEW",
"property_name": "REPLACEMENT",
"type": "name",
"required": true
},
"place_properties": [
{
"friendly_name": "Parish Name",
"property_name": "name",
"type": "name",
"required": true,
"unique": "parent"
},
{
"friendly_name": "Sub District",
"property_name": "sub_district",
"parameter": ["\\ssub\\s", "\\sdistrict\\s"],
"type": "name",
"required": false
},
{
"friendly_name": "County",
"property_name": "county",
"type": "name",
"parameter": ["\\scounty\\s"],
"required": true
},
{
"friendly_name": "Sub County",
"property_name": "sub_county",
"type": "name",
"parameter": ["\\ssub\\s", "\\scounty\\s"],
"required": true
}
],
"contact_properties": [
{
"friendly_name": "CHEW Name",
"property_name": "name",
"type": "name",
"required": true
},
{
"friendly_name": "Phone Number",
"property_name": "phone",
"type": "phone",
"parameter": "UG",
"required": true,
"unique": "all"
},
{
"friendly_name": "Date of Birth",
"property_name": "date_of_birth",
"type": "dob",
"required": true
},
{
"friendly_name": "Sex",
"property_name": "sex",
"type": "select_one",
"parameter": {
"male": "Male",
"female": "Female"
},
"required": true
}
]
},
{
"name": "c50-health_center",
"friendly": "VHT Area",
Expand Down
51 changes: 46 additions & 5 deletions src/config/config-factory.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,65 @@
import { PartnerConfig } from '.';
import { Config, ConfigSystem, PartnerConfig } from '.';
import ugandaConfig from './chis-ug';
import kenyaConfig from './chis-ke';
import togoConfig from './chis-tg';
import civConfig from './chis-civ';
import path from 'path';
import fs from 'fs';

export const CONFIG_MAP: { [key: string]: PartnerConfig } = {
export const uploadedConfigFilePath: string = path.join(getConfigUploadDirectory(), 'config.json');


export const DEFAULT_CONFIG_MAP: { [key: string]: PartnerConfig } = {
'CHIS-KE': kenyaConfig,
'CHIS-UG': ugandaConfig,
'CHIS-TG': togoConfig,
'CHIS-CIV': civConfig
};

export default function getConfigByKey(key: string = 'CHIS-KE'): PartnerConfig {
export default async function getConfigByKey(key: string = 'CHIS-KE'): Promise<PartnerConfig> {
if (fs.existsSync(uploadedConfigFilePath)) {
const uploadedConfig = await readConfig();
await Config.assertValid(uploadedConfig);
console.log(`Using uploaded configuration: ${uploadedConfigFilePath}`);
return uploadedConfig;
}

const usingKey = key.toUpperCase();
console.log(`Using configuration: ${key}`);
const result = CONFIG_MAP[usingKey];
const result = DEFAULT_CONFIG_MAP[usingKey];
if (!result) {
const available = JSON.stringify(Object.keys(CONFIG_MAP));
const available = JSON.stringify(Object.keys(DEFAULT_CONFIG_MAP));
throw Error(`Failed to start: Cannot find configuration "${usingKey}". Configurations available are ${available}`);
}

return result;
}

export function getConfigUploadDirectory (): string {
const configDir = path.join(__dirname,'..', 'config_uploads');
if(!fs.existsSync(configDir)) {
fs.mkdirSync(configDir);
}
return configDir;
}

export async function writeConfig(jsonConfigData: ConfigSystem): Promise<void> {
try {
const jsonString: string = JSON.stringify(jsonConfigData, null, 2);
await fs.promises.writeFile(uploadedConfigFilePath, jsonString);
} catch (error) {
throw new Error('writeConfig: Failed to write file');
}
}

export async function readConfig(): Promise<PartnerConfig> {
try {
const fileContent = await fs.promises.readFile(uploadedConfigFilePath, 'utf-8');

const config = JSON.parse(fileContent);
return { config };
} catch (error) {
console.error('readConfig:Failed to read config file:', error);
throw error;
}
}
39 changes: 26 additions & 13 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,21 @@ const {
CHT_DEV_HTTP
} = process.env;

const partnerConfig = getConfigByKey(CONFIG_NAME);
const { config } = partnerConfig;
async function init(): Promise<PartnerConfig> {
return await getConfigByKey(CONFIG_NAME);
}

export class Config {
private constructor() {}

public static contactTypes(): ContactType[] {
public static async contactTypes(): Promise<ContactType[]> {
const {config} = await init();
return config.contact_types;
}

public static getContactType(name: string) : ContactType {
const contactMatch = config.contact_types.find(c => c.name === name);
public static async getContactType(name: string) : Promise<ContactType> {
const {config} = await init();
const contactMatch = config.contact_types.find(c => c?.name === name);
if (!contactMatch) {
throw new Error(`unrecognized contact type: "${name}"`);
}
Expand All @@ -90,7 +93,7 @@ export class Config {
public static getParentProperty(contactType: ContactType): HierarchyConstraint {
const parentMatch = contactType.hierarchy.find(c => c.level === 1);
if (!parentMatch) {
throw new Error(`hierarchy at level 1 is required: "${contactType.name}"`);
throw new Error(`hierarchy at level 1 is required: "${contactType?.name}"`);
}

return parentMatch;
Expand Down Expand Up @@ -136,18 +139,20 @@ export class Config {
}

public static async mutate(payload: PlacePayload, chtApi: ChtApi, isReplacement: boolean): Promise<PlacePayload | undefined> {
const partnerConfig = await init();
return partnerConfig.mutate && partnerConfig.mutate(payload, chtApi, isReplacement);
}

public static getAuthenticationInfo(domain: string) : AuthenticationInfo {
const domainMatch = Config.getDomains().find(c => c.domain === domain);
public static async getAuthenticationInfo(domain: string) : Promise<AuthenticationInfo> {
const domainMatch = (await Config.getDomains()).find(c => c.domain === domain);
if (!domainMatch) {
throw new Error(`unrecognized domain: "${domain}"`);
}
return domainMatch;
}

public static getLogoBase64() : string {
public static async getLogoBase64() : Promise<string> {
const {config} = await init();
return config.logoBase64;
}

Expand All @@ -174,7 +179,8 @@ export class Config {
];
}

public static getDomains() : AuthenticationInfo[] {
public static async getDomains() : Promise<AuthenticationInfo[]> {
const {config} = await init();
const domains = [...config.domains];

// because all .env vars imported as strings, let's get the AuthenticationInfo object a boolean
Expand All @@ -194,15 +200,22 @@ export class Config {
return _.sortBy(domains, 'friendly');
}

public static getUniqueProperties(contactTypeName: string): ContactProperty[] {
public static async getUniqueProperties(contactTypeName: string): Promise<ContactProperty[]> {
const {config} = await init();
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) {
public static async assertValid(config?: PartnerConfig) {
const { config: assertionConfig } = config || await init();

if (!assertionConfig.contact_types || assertionConfig.contact_types.length === 0) {
throw Error(`invalid configuration: 'contact_types' property is empty`);
}

for (const contactType of assertionConfig.contact_types) {
const allHierarchyProperties = [...contactType.hierarchy, contactType.replacement_property];
const allProperties = [
...contactType.place_properties,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/remote-place-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default class RemotePlaceCache {

// 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 uniqueKeyProperties = await Config.getUniqueProperties(hierarchyLevel.contact_type);
const docs = await chtApi.getPlacesWithType(hierarchyLevel.contact_type);
return docs.map((doc: any) => this.convertContactToRemotePlace(doc, uniqueKeyProperties, hierarchyLevel));
}
Expand Down
2 changes: 1 addition & 1 deletion src/liquid/app/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<hr>
{% if session.isAdmin %}
<a class="navbar-item" href="/app/config">
<span class="material-symbols-outlined">settings</span> Upload Config
<span class="material-symbols-outlined">settings</span> Upload Configuration
</a>
{% endif %}
<a class="navbar-item" href="/logout">
Expand Down
9 changes: 5 additions & 4 deletions src/routes/add-place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export default async function addPlace(fastify: FastifyInstance) {
fastify.get('/add-place', async (req, resp) => {
const queryParams: any = req.query;

const contactTypes = Config.contactTypes();
const contactTypes = await Config.contactTypes();
const contactType = queryParams.type
? Config.getContactType(queryParams.type)
? await Config.getContactType(queryParams.type)
: contactTypes[contactTypes.length - 1];
const op = queryParams.op || 'new';
const tmplData = {
Expand All @@ -34,7 +34,8 @@ export default async function addPlace(fastify: FastifyInstance) {

fastify.post('/place/dob', async (req, resp) => {
const { place_type, prefix, prop_type } = req.query as any;
const contactType = Config.getContactType(place_type).contact_properties.find(prop => prop.type === prop_type);
const config = await Config.getContactType(place_type);
const contactType = config.contact_properties.find(prop => prop.type === prop_type);
return resp.view('src/liquid/components/contact_type_property.html', {
data: req.body,
include: {
Expand All @@ -49,7 +50,7 @@ export default async function addPlace(fastify: FastifyInstance) {
fastify.post('/place', async (req, resp) => {
const { op, type: placeType } = req.query as any;

const contactType = Config.getContactType(placeType);
const contactType = await Config.getContactType(placeType);
const sessionCache: SessionCache = req.sessionCache;
const chtApi = new ChtApi(req.chtSession);
if (op === 'new' || op === 'replace') {
Expand Down
Loading

0 comments on commit 8c4f3c8

Please sign in to comment.