From 9ea6f9667ea5919c0c7d777e33902bdf40bb69a0 Mon Sep 17 00:00:00 2001 From: freddieptf Date: Tue, 19 Nov 2024 13:51:51 +0300 Subject: [PATCH 1/5] something like feature flags --- README.md | 1 + src/config/chis-ke/config.json | 36 ++++++++++++++++- src/config/index.ts | 5 ++- src/lib/remote-place-resolver.ts | 2 + src/liquid/app/nav.html | 24 +++++++---- src/liquid/place/directive_1_get_started.html | 16 ++++++-- src/liquid/place/list.html | 40 ++++++++++--------- src/liquid/place/list_lazy.html | 22 +++++----- src/routes/add-place.ts | 7 ++++ src/routes/move.ts | 4 ++ 10 files changed, 114 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 73f4975f..930e2947 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ To use the User Management Tool with your CHT project, you'll need to create a n `contact_types.contact_properties` | Array | Defines the attributes which are collected and set on the user's primary contact doc. See [ConfigProperty](#ConfigProperty). `contact_types.deactivate_users_on_replace` | boolean | Controls what should happen to the defunct contact and user documents when a user is replaced. When `false`, the contact and user account will be deleted. When `true`, the contact will be unaltered and the user account will be assigned the role `deactivated`. This allows for account restoration. `contact_types.hint` | string | Provide a brief hint or description to clarify the expected input for the property. +`contact_types.feature_flags` | Array | A list of features to enable for this contact type. Acceptable values are `create`, `replace-contact` and `move`. All features are enabled by default `logoBase64` | Image in base64 | Logo image for your project #### ConfigProperty diff --git a/src/config/chis-ke/config.json b/src/config/chis-ke/config.json index e528a4d4..cce3e50e 100644 --- a/src/config/chis-ke/config.json +++ b/src/config/chis-ke/config.json @@ -188,7 +188,6 @@ "friendly": "West Pokot", "domain": "westpokot.echis.go.ke" }, - { "friendly": "Staging (chis-staging.health.go.ke)", "domain": "chis-staging.health.go.ke" @@ -337,6 +336,41 @@ "required": true } ] + }, + { + "name": "e_household", + "friendly": "Household", + "contact_type": "f_client", + "user_role": [], + "place_properties": [], + "contact_properties": [], + "feature_flags": [ + "move" + ], + "replacement_property": { + "friendly_name": "", + "property_name": "replacement", + "type": "name", + "required": true + }, + "hierarchy": [ + { + "friendly_name": "CHU", + "property_name": "CHU", + "contact_type": "c_community_health_unit", + "type": "name", + "required": true, + "level": 2 + }, + { + "friendly_name": "CHP Area", + "property_name": "CHP", + "contact_type": "d_community_health_volunteer_area", + "type": "name", + "required": true, + "level": 1 + } + ] } ], "logoBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAREAAABJCAYAAAAaPhKgAAAAxHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjabVBbEsMgCPz3FD2CPGLwOKaxM71Bj981kE5sy4wrLLAiqb+ej3QbxqRJl9VKLSXDtGrlBseyWzuQsh7oAYVHM5+0RhODEtzioZVoPPlo+Ag2eMtFyO6R2OZE1dC3LyH2S8ZEw99DqIaQsCcoBJp/K5dq6/ULW8+zmZ80QG0e+ydesb19wTvC3IUkA0WKDyDjSJKGBANBoJBQMBgFinCIYSH/9nRaegPoIVkQzzQ3YAAAAYNpQ0NQSUNDIHByb2ZpbGUAAHicfZE9SMNQFIVPU0WRiIMZRByCVCcLoiKOWoUiVAi1QqsOJq+/0KQhSXFxFFwLDv4sVh1cnHV1cBUEwR8QVxcnRRcp8b6k0CLGB5f3cd47h/vuA4R6mel2xzigG46VjMfkdGZV7nqFCIlKxLDKbHNOURIIXF/3CPH9Lsqzgu/9uXqzOZsBIZl4lpmWQ7xBPL3pmJz3iSVWVLPE58RjFjVI/Mh1zec3zgWPBZ4pWankPLFELBfaWGtjVrR04iniSFY3KF9I+5zlvMVZL1dZs0/+QjFnrCxznWoIcSxiCQpkaKiihDIcRGk3SLGRpPNYgH/Q8yvk0shVAiPHAirQoXp+8D/4PVs7PznhJ4kxoPPFdT9GgK5doFFz3e9j122cAOFn4Mpo+St1YOaT9FpLixwBfdvAxXVL0/aAyx1g4MlULdWTwlRCPg+8n9E3ZYD+W6BnzZ9b8xynD0CKZpW4AQ4OgdECZa8HvLu7fW7/3mnO7wdSxnKaCzy0QQAADXhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4KIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgIHhtbG5zOkdJTVA9Imh0dHA6Ly93d3cuZ2ltcC5vcmcveG1wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6YmQ0YWQyZmMtZjcwNC00Y2M2LTk3MDYtYzUzMjQzZjM1ZjU3IgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmRjYjQwZmQ5LTFmY2ItNDIwOS1iNzdiLWEyNzE2MWJkZTJiZCIKICAgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjM0ZjcwYjEwLWNiYzAtNGM2NS1hNjhhLTI1ZDE2NTE0ZmUzOCIKICAgZGM6Rm9ybWF0PSJpbWFnZS9wbmciCiAgIEdJTVA6QVBJPSIyLjAiCiAgIEdJTVA6UGxhdGZvcm09IkxpbnV4IgogICBHSU1QOlRpbWVTdGFtcD0iMTcwMzA3NDQ3Mjg2MDUxMSIKICAgR0lNUDpWZXJzaW9uPSIyLjEwLjM2IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMzoxMjoyMFQxMzoxNDozMiswMTowMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjM6MTI6MjBUMTM6MTQ6MzIrMDE6MDAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5YTE2MzJlNi00ODA1LTQ2NjAtYjczNi1lY2MzOWI0ZmExMzEiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTEyLTIwVDEzOjE0OjMyKzAxOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PkajOMMAAAAGYktHRAAfALMAQzM+608AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfnDBQMDiBx6VhxAAAgAElEQVR42u2de1yUZdrHv8+cgQGGo4iAo5gnVMY85GHdxg5qpYlWdtgMrDW33naFst52txbt3a323Qzdt5O1G5BbrbYFnsoyBTO1MnU8oqgxigdEgeHMwMzc7x/PDCCCgoKH3fl9Ps8Hhee5z/fvvq7rvu7rhquA5O7djVxH6BMZacALL7xoFcqrkanOzy9N8vcvt1VVWa/1BjKGhJiFEA/YampyvcPFCy+uEUQFB5t+HhpaFhMcfE1LJD3Dw403h4eXxYaHG7295oUX1xgig4JyTAbDzu6hodekqtDNYDAMCgjYadTrc7y95YUXbUNxtTLW6HSLVfX1phGSlHYtNsxorTbNVV9valCpMr3DxAsvrlGMDg4uuNPPT4wJD0+6lsp1V1hY8ni9XowOCirw9pIXXlyjkgiAUKsX+EkSt7pc6cawsIRroUGiQ0OTop3ONJ3L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" diff --git a/src/config/index.ts b/src/config/index.ts index 69b32141..e7ed8667 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -19,13 +19,14 @@ export type ContactType = { contact_type: string; contact_friendly?: string; user_role: string[]; - username_from_place: boolean; + username_from_place?: boolean; hierarchy: HierarchyConstraint[]; replacement_property: ContactProperty; place_properties: ContactProperty[]; contact_properties: ContactProperty[]; - deactivate_users_on_replace: boolean; + deactivate_users_on_replace?: boolean; hint?: string; + feature_flags?: string[]; }; export type HierarchyConstraint = { diff --git a/src/lib/remote-place-resolver.ts b/src/lib/remote-place-resolver.ts index e7d10a57..487a0ae6 100644 --- a/src/lib/remote-place-resolver.ts +++ b/src/lib/remote-place-resolver.ts @@ -5,6 +5,7 @@ import { RemotePlace, ChtApi } from './cht-api'; import { Config, ContactType, HierarchyConstraint } from '../config'; import { Validation } from './validation'; import RemotePlaceCache from './remote-place-cache'; +import assert from 'assert'; type RemotePlaceMap = { [key: string]: RemotePlace }; @@ -111,6 +112,7 @@ export default class RemotePlaceResolver { function getFuzzFunction(place: Place, hierarchyLevel: HierarchyConstraint, contactType: ContactType) { const fuzzingProperty = hierarchyLevel.level === 0 ? contactType.replacement_property : hierarchyLevel; + assert(fuzzingProperty); if (fuzzingProperty.type === 'generated') { throw Error(`Invalid configuration: hierarchy properties cannot be of type "generated".`); } diff --git a/src/liquid/app/nav.html b/src/liquid/app/nav.html index 533ef2d8..6602082c 100644 --- a/src/liquid/app/nav.html +++ b/src/liquid/app/nav.html @@ -19,12 +19,20 @@ @@ -35,7 +43,9 @@ diff --git a/src/liquid/place/directive_1_get_started.html b/src/liquid/place/directive_1_get_started.html index a926790a..f3ef4b79 100644 --- a/src/liquid/place/directive_1_get_started.html +++ b/src/liquid/place/directive_1_get_started.html @@ -12,10 +12,18 @@ add Add {{contactType.friendly}} diff --git a/src/liquid/place/list.html b/src/liquid/place/list.html index 1cfc0afa..3931f6fc 100644 --- a/src/liquid/place/list.html +++ b/src/liquid/place/list.html @@ -1,23 +1,25 @@
{% for contactType in contactTypes %} -
-

{{contactType.friendly}}

- {% if contactType.places.length > 0 %} - - {% include "components/table_header.html" contactType=contactType %} - - {% for place in contactType.places %} - {% include "components/place_item.html" %} - {% endfor%} - -
- {% else %} -
- No Results -
- {% endif %} + {% if contactType.feature_flags == undefined or contactType.feature_flags contains "create" or contactType.feature_flags contains "replace-contact" %} +
+

{{contactType.friendly}}

+ {% if contactType.places.length > 0 %} + + {% include "components/table_header.html" contactType=contactType %} + + {% for place in contactType.places %} + {% include "components/place_item.html" %} + {% endfor%} + +
+ {% else %} +
+ No Results +
+ {% endif %} + {%endif%} {% endfor %}
\ No newline at end of file diff --git a/src/liquid/place/list_lazy.html b/src/liquid/place/list_lazy.html index 1f2e8f69..7a9255b4 100644 --- a/src/liquid/place/list_lazy.html +++ b/src/liquid/place/list_lazy.html @@ -1,15 +1,17 @@
{% for contactType in contactTypes %} -
-

{{contactType.friendly}}

-
- - {% include "components/table_header.html" contactType=contactType %} -
-
- Loading data -
-
+ {% if contactType.feature_flags == undefined or contactType.feature_flags contains "create" or contactType.feature_flags contains "replace-contact" %} +
+

{{contactType.friendly}}

+
+ + {% include "components/table_header.html" contactType=contactType %} +
+
+ Loading data +
+
+ {%endif%} {% endfor %}
\ No newline at end of file diff --git a/src/routes/add-place.ts b/src/routes/add-place.ts index 1d18459c..d162246f 100644 --- a/src/routes/add-place.ts +++ b/src/routes/add-place.ts @@ -17,6 +17,13 @@ export default async function addPlace(fastify: FastifyInstance) { ? Config.getContactType(queryParams.type) : contactTypes[contactTypes.length - 1]; const op = queryParams.op || 'new'; + if (contactType.feature_flags) { + if ((op === 'new' && !contactType.feature_flags.includes('create')) || + (op === 'replace' && !contactType.feature_flags.includes('replace-contact'))) { + resp.status(404); + return; + } + } const tmplData = { view: 'add', logo: Config.getLogoBase64(), diff --git a/src/routes/move.ts b/src/routes/move.ts index 0a2dbf77..0d7716fb 100644 --- a/src/routes/move.ts +++ b/src/routes/move.ts @@ -13,6 +13,10 @@ export default async function sessionCache(fastify: FastifyInstance) { const contactTypes = Config.contactTypes(); const contactType = Config.getContactType(placeType); + if (contactType.feature_flags && !contactType.feature_flags.includes('move')) { + resp.code(404).type("text/html").send("Not Found"); + return; + } const tmplData = { view: 'move', op: 'move', From 5b4aed5b02bb4aa32bad0bd2a01a283e0b2b10f8 Mon Sep 17 00:00:00 2001 From: freddieptf Date: Tue, 19 Nov 2024 14:03:51 +0300 Subject: [PATCH 2/5] fix lint --- src/routes/add-place.ts | 2 +- src/routes/move.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/add-place.ts b/src/routes/add-place.ts index d162246f..de54fae0 100644 --- a/src/routes/add-place.ts +++ b/src/routes/add-place.ts @@ -20,7 +20,7 @@ export default async function addPlace(fastify: FastifyInstance) { if (contactType.feature_flags) { if ((op === 'new' && !contactType.feature_flags.includes('create')) || (op === 'replace' && !contactType.feature_flags.includes('replace-contact'))) { - resp.status(404); + resp.code(404).type('text/html').send('Not Found'); return; } } diff --git a/src/routes/move.ts b/src/routes/move.ts index 0d7716fb..e7c93ac7 100644 --- a/src/routes/move.ts +++ b/src/routes/move.ts @@ -14,7 +14,7 @@ export default async function sessionCache(fastify: FastifyInstance) { const contactType = Config.getContactType(placeType); if (contactType.feature_flags && !contactType.feature_flags.includes('move')) { - resp.code(404).type("text/html").send("Not Found"); + resp.code(404).type('text/html').send('Not Found'); return; } const tmplData = { From 03727cf1263b60346f24564b7ff80d64db137215 Mon Sep 17 00:00:00 2001 From: freddieptf Date: Tue, 26 Nov 2024 03:50:29 +0300 Subject: [PATCH 3/5] address feedback --- src/config/config-factory.ts | 44 ++++++++++++++++++++++++++++++++---- src/config/index.ts | 7 ++++-- src/routes/add-place.ts | 5 ++-- src/routes/move.ts | 3 ++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/config/config-factory.ts b/src/config/config-factory.ts index dc6a7169..40bb12a1 100644 --- a/src/config/config-factory.ts +++ b/src/config/config-factory.ts @@ -4,11 +4,43 @@ import kenyaConfig from './chis-ke'; import togoConfig from './chis-tg'; import civConfig from './chis-civ'; +export enum Feature { + Create = 'create', + ReplaceContact = 'replace-contact', + Move = 'move', +} + +const parseConfig = (c: any): PartnerConfig => { + return { + config: { + ...c.config, + contact_types: c.config.contact_types.map((t: any) => { + return { + ...t, + feature_flags: t.feature_flags?.map((v: string) => { + if ((Object.values(Feature) as string[]).indexOf(v) === -1) { + throw new Error( + 'invalid feature flag: ' + + v + + '. Acceptable values are [' + + Object.values(Feature).join(' | ') + + ']' + ); + } + return v as Feature; + }), + }; + }), + }, + mutate: c.mutate, + }; +}; + const CONFIG_MAP: { [key: string]: PartnerConfig } = { - 'CHIS-KE': kenyaConfig, - 'CHIS-UG': ugandaConfig, - 'CHIS-TG': togoConfig, - 'CHIS-CIV': civConfig + 'CHIS-KE': parseConfig(kenyaConfig), + 'CHIS-UG': parseConfig(ugandaConfig), + 'CHIS-TG': parseConfig(togoConfig), + 'CHIS-CIV': parseConfig(civConfig), }; export default function getConfigByKey(key: string = 'CHIS-KE'): PartnerConfig { @@ -17,7 +49,9 @@ export default function getConfigByKey(key: string = 'CHIS-KE'): PartnerConfig { const result = CONFIG_MAP[usingKey]; if (!result) { const available = JSON.stringify(Object.keys(CONFIG_MAP)); - throw Error(`Failed to start: Cannot find configuration "${usingKey}". Configurations available are ${available}`); + throw Error( + `Failed to start: Cannot find configuration '${usingKey}'. Configurations available are ${available}` + ); } return result; diff --git a/src/config/index.ts b/src/config/index.ts index e7ed8667..0a1bc85d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { ChtApi, PlacePayload } from '../lib/cht-api'; -import getConfigByKey from './config-factory'; +import getConfigByKey, { Feature } from './config-factory'; export type ConfigSystem = { domains: AuthenticationInfo[]; @@ -26,7 +26,7 @@ export type ContactType = { contact_properties: ContactProperty[]; deactivate_users_on_replace?: boolean; hint?: string; - feature_flags?: string[]; + feature_flags?: Feature[]; }; export type HierarchyConstraint = { @@ -123,6 +123,9 @@ export class Config { } public static hasMultipleRoles(contactType: ContactType): boolean { + if (contactType.feature_flags?.length === 1 && contactType.feature_flags.includes(Feature.Move)) { + return false; + } if (!contactType.user_role.length || contactType.user_role.some(role => !role.trim())) { throw Error(`unvalidatable config: 'user_role' property is empty or contains empty strings`); } diff --git a/src/routes/add-place.ts b/src/routes/add-place.ts index de54fae0..2f7e19f3 100644 --- a/src/routes/add-place.ts +++ b/src/routes/add-place.ts @@ -7,6 +7,7 @@ import SessionCache from '../services/session-cache'; import RemotePlaceResolver from '../lib/remote-place-resolver'; import { UploadManager } from '../services/upload-manager'; import RemotePlaceCache from '../lib/remote-place-cache'; +import { Feature } from '../config/config-factory'; export default async function addPlace(fastify: FastifyInstance) { fastify.get('/add-place', async (req, resp) => { @@ -18,8 +19,8 @@ export default async function addPlace(fastify: FastifyInstance) { : contactTypes[contactTypes.length - 1]; const op = queryParams.op || 'new'; if (contactType.feature_flags) { - if ((op === 'new' && !contactType.feature_flags.includes('create')) || - (op === 'replace' && !contactType.feature_flags.includes('replace-contact'))) { + if ((op === 'new' && !contactType.feature_flags.includes(Feature.Create)) || + (op === 'replace' && !contactType.feature_flags.includes(Feature.ReplaceContact))) { resp.code(404).type('text/html').send('Not Found'); return; } diff --git a/src/routes/move.ts b/src/routes/move.ts index e7c93ac7..f0e77d75 100644 --- a/src/routes/move.ts +++ b/src/routes/move.ts @@ -5,6 +5,7 @@ import { ChtApi } from '../lib/cht-api'; import { FastifyInstance } from 'fastify'; import MoveLib from '../lib/move'; import SessionCache from '../services/session-cache'; +import { Feature } from '../config/config-factory'; export default async function sessionCache(fastify: FastifyInstance) { fastify.get('/move/:placeType', async (req, resp) => { @@ -13,7 +14,7 @@ export default async function sessionCache(fastify: FastifyInstance) { const contactTypes = Config.contactTypes(); const contactType = Config.getContactType(placeType); - if (contactType.feature_flags && !contactType.feature_flags.includes('move')) { + if (contactType.feature_flags && !contactType.feature_flags.includes(Feature.Move)) { resp.code(404).type('text/html').send('Not Found'); return; } From edef0950528a60fb5cc7e61559d49bc3012cc75b Mon Sep 17 00:00:00 2001 From: freddieptf Date: Tue, 26 Nov 2024 22:07:30 +0300 Subject: [PATCH 4/5] feedback --- README.md | 4 +- src/config/chis-ke/config.json | 7 ++-- src/config/config-factory.ts | 40 ++----------------- src/config/index.ts | 12 +++--- src/lib/remote-place-resolver.ts | 1 - src/liquid/app/nav.html | 10 ++--- src/liquid/place/directive_1_get_started.html | 8 ++-- src/liquid/place/list.html | 2 +- src/liquid/place/list_lazy.html | 2 +- src/routes/add-place.ts | 11 ++--- src/routes/move.ts | 3 +- 11 files changed, 34 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 930e2947..5f1a7c77 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,9 @@ To use the User Management Tool with your CHT project, you'll need to create a n `contact_types.contact_properties` | Array | Defines the attributes which are collected and set on the user's primary contact doc. See [ConfigProperty](#ConfigProperty). `contact_types.deactivate_users_on_replace` | boolean | Controls what should happen to the defunct contact and user documents when a user is replaced. When `false`, the contact and user account will be deleted. When `true`, the contact will be unaltered and the user account will be assigned the role `deactivated`. This allows for account restoration. `contact_types.hint` | string | Provide a brief hint or description to clarify the expected input for the property. -`contact_types.feature_flags` | Array | A list of features to enable for this contact type. Acceptable values are `create`, `replace-contact` and `move`. All features are enabled by default +`contact_types.can_create` | boolean | Optionally disable/enable creating places of this type. Defaults to true. +`contact_types.can_replace_contact` | boolean | Optionally disable/enable replacing contacts for places of this type. Defaults to true. +`contact_types.can_move` | boolean | Optionally disable/enable moving places of this type. Defaults to true. `logoBase64` | Image in base64 | Logo image for your project #### ConfigProperty diff --git a/src/config/chis-ke/config.json b/src/config/chis-ke/config.json index cce3e50e..9dd73672 100644 --- a/src/config/chis-ke/config.json +++ b/src/config/chis-ke/config.json @@ -344,9 +344,10 @@ "user_role": [], "place_properties": [], "contact_properties": [], - "feature_flags": [ - "move" - ], + "username_from_place": false, + "deactivate_users_on_replace": false, + "can_create": false, + "can_replace_contact": false, "replacement_property": { "friendly_name": "", "property_name": "replacement", diff --git a/src/config/config-factory.ts b/src/config/config-factory.ts index 40bb12a1..14b05399 100644 --- a/src/config/config-factory.ts +++ b/src/config/config-factory.ts @@ -4,43 +4,11 @@ import kenyaConfig from './chis-ke'; import togoConfig from './chis-tg'; import civConfig from './chis-civ'; -export enum Feature { - Create = 'create', - ReplaceContact = 'replace-contact', - Move = 'move', -} - -const parseConfig = (c: any): PartnerConfig => { - return { - config: { - ...c.config, - contact_types: c.config.contact_types.map((t: any) => { - return { - ...t, - feature_flags: t.feature_flags?.map((v: string) => { - if ((Object.values(Feature) as string[]).indexOf(v) === -1) { - throw new Error( - 'invalid feature flag: ' + - v + - '. Acceptable values are [' + - Object.values(Feature).join(' | ') + - ']' - ); - } - return v as Feature; - }), - }; - }), - }, - mutate: c.mutate, - }; -}; - const CONFIG_MAP: { [key: string]: PartnerConfig } = { - 'CHIS-KE': parseConfig(kenyaConfig), - 'CHIS-UG': parseConfig(ugandaConfig), - 'CHIS-TG': parseConfig(togoConfig), - 'CHIS-CIV': parseConfig(civConfig), + 'CHIS-KE': kenyaConfig, + 'CHIS-UG': ugandaConfig, + 'CHIS-TG': togoConfig, + 'CHIS-CIV': civConfig, }; export default function getConfigByKey(key: string = 'CHIS-KE'): PartnerConfig { diff --git a/src/config/index.ts b/src/config/index.ts index 0a1bc85d..8e4c8568 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { ChtApi, PlacePayload } from '../lib/cht-api'; -import getConfigByKey, { Feature } from './config-factory'; +import getConfigByKey from './config-factory'; export type ConfigSystem = { domains: AuthenticationInfo[]; @@ -19,14 +19,16 @@ export type ContactType = { contact_type: string; contact_friendly?: string; user_role: string[]; - username_from_place?: boolean; + username_from_place: boolean; hierarchy: HierarchyConstraint[]; replacement_property: ContactProperty; place_properties: ContactProperty[]; contact_properties: ContactProperty[]; - deactivate_users_on_replace?: boolean; + deactivate_users_on_replace: boolean; hint?: string; - feature_flags?: Feature[]; + can_create?: boolean; + can_replace_contact?: boolean; + can_move?: boolean; }; export type HierarchyConstraint = { @@ -123,7 +125,7 @@ export class Config { } public static hasMultipleRoles(contactType: ContactType): boolean { - if (contactType.feature_flags?.length === 1 && contactType.feature_flags.includes(Feature.Move)) { + if (contactType.can_move && (contactType.can_create === false && contactType.can_replace_contact === false)) { return false; } if (!contactType.user_role.length || contactType.user_role.some(role => !role.trim())) { diff --git a/src/lib/remote-place-resolver.ts b/src/lib/remote-place-resolver.ts index 487a0ae6..76605c94 100644 --- a/src/lib/remote-place-resolver.ts +++ b/src/lib/remote-place-resolver.ts @@ -112,7 +112,6 @@ export default class RemotePlaceResolver { function getFuzzFunction(place: Place, hierarchyLevel: HierarchyConstraint, contactType: ContactType) { const fuzzingProperty = hierarchyLevel.level === 0 ? contactType.replacement_property : hierarchyLevel; - assert(fuzzingProperty); if (fuzzingProperty.type === 'generated') { throw Error(`Invalid configuration: hierarchy properties cannot be of type "generated".`); } diff --git a/src/liquid/app/nav.html b/src/liquid/app/nav.html index 6602082c..0f5b9b65 100644 --- a/src/liquid/app/nav.html +++ b/src/liquid/app/nav.html @@ -19,18 +19,18 @@