From 08994fc1ee64ae5e303d1a77add51f70d135e265 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:18:35 +0800 Subject: [PATCH 01/11] Export `ENTITY_REFS` --- src/db/models/blueprint.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/db/models/blueprint.ts b/src/db/models/blueprint.ts index 0f3b5390..0ae656df 100644 --- a/src/db/models/blueprint.ts +++ b/src/db/models/blueprint.ts @@ -27,13 +27,20 @@ export const BLUEPRINT_TYPE = t.keyof({ operation: null, }); -const ENTITY_REFS = t.array( +export const ENTITY_REFS = t.array( t.type({ refCode: t.string, cardinality: t.string, }) ); +export const ENTITY_REFS_OR_XOR = t.union([ + ENTITY_REFS, + t.type({ + xor: ENTITY_REFS, + }), +]); + const BLUEPRINT_MODEL_ATTACHMENT_TYPE = t.union([ ATTACHMENT_TYPE, t.keyof({ @@ -84,12 +91,7 @@ export const BLUEPRINT_MODEL = t.type({ t.partial({ possibleChildren: ENTITY_REFS, description: LOCALIZED_STRING, - canSupport: t.union([ - ENTITY_REFS, - t.type({ - xor: ENTITY_REFS, - }), - ]), + canSupport: ENTITY_REFS_OR_XOR, }), ]) ), From 3fa611349dceb46eceb9a6cd17a80ce08c3d7fb6 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:19:08 +0800 Subject: [PATCH 02/11] Add `ENTITY_PROTOTYPE_CARDINALITY` codec --- src/db/models/entityPrototype.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/db/models/entityPrototype.ts b/src/db/models/entityPrototype.ts index 43c56b74..32234a4b 100644 --- a/src/db/models/entityPrototype.ts +++ b/src/db/models/entityPrototype.ts @@ -39,6 +39,11 @@ export const ENTITY_PROTOTYPE_TYPE = t.keyof({ PE: null, }); +export const ENTITY_PROTOTYPE_CARDINALITY = t.union([ + t.literal('1-1'), + t.literal('0-N'), +]); + const ENTITY_REFS = t.array( t.intersection([ t.type({ @@ -52,7 +57,7 @@ const ENTITY_REFS = t.array( refCode: t.string, }), t.partial({ - cardinality: t.union([t.literal('1-1'), t.literal('0-N')]), + cardinality: ENTITY_PROTOTYPE_CARDINALITY, /** * @deprecated * There are records in database that have @@ -61,7 +66,7 @@ const ENTITY_REFS = t.array( * TODO: Rename this property to "cardinality" in DB, then, * drop this definition and make "cardinality" required */ - cadinality: t.union([t.literal('1-1'), t.literal('0-N')]), + cadinality: ENTITY_PROTOTYPE_CARDINALITY, }), ]) ); From 44c963051c5e844ea44714c091c9d09ca733de93 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:14:11 +0800 Subject: [PATCH 03/11] Add `CREATE_PLAN` permission --- src/auth/permissions.ts | 1 + src/auth/roles.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/auth/permissions.ts b/src/auth/permissions.ts index 57b805bd..7f1d94ba 100644 --- a/src/auth/permissions.ts +++ b/src/auth/permissions.ts @@ -84,6 +84,7 @@ export const AUTH_PERMISSIONS = { * Can clone data from any project to a new project */ CLONE_ANY_PROJECT: 'cloneAnyProject', + CREATE_PLAN: 'createPlan', DELETE_ANY_PROJECT: 'canDeleteAnyProject', DELETE_ANY_PLAN: 'canDeleteAnyPlan', /** diff --git a/src/auth/roles.ts b/src/auth/roles.ts index abd618f2..6a70e3af 100644 --- a/src/auth/roles.ts +++ b/src/auth/roles.ts @@ -177,6 +177,7 @@ export const calculatePermissionsFromRolesGrant = async < } } else if (role === 'rpmAdmin') { // New Permissions + global.add(P.global.CREATE_PLAN); global.add(P.global.VIEW_ANY_PLAN_DATA); global.add(P.global.EDIT_ANY_PLAN_DATA); global.add(P.global.EDIT_ANY_MEASUREMENT); From d6626e86db098228b3d80e154be61a526d40f077 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:15:14 +0800 Subject: [PATCH 04/11] Export `PLAN_VERSION_CLUSTER_SELECTION_TYPE` --- src/db/models/planVersion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/planVersion.ts b/src/db/models/planVersion.ts index bfcd2d10..db95d0b8 100644 --- a/src/db/models/planVersion.ts +++ b/src/db/models/planVersion.ts @@ -15,7 +15,7 @@ export type PlanVersionId = Brand< export const PLAN_VERSION_ID = brandedType(t.number); -const PLAN_VERSION_CLUSTER_SELECTION_TYPE = t.keyof({ +export const PLAN_VERSION_CLUSTER_SELECTION_TYPE = t.keyof({ single: null, multi: null, }); From acdc63af933d281d4614e75de22e255ffa6b4de1 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:15:34 +0800 Subject: [PATCH 05/11] Export `PLAN_VISIBILITY_PREFERENCES` --- src/db/models/planVersion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/planVersion.ts b/src/db/models/planVersion.ts index db95d0b8..ec765513 100644 --- a/src/db/models/planVersion.ts +++ b/src/db/models/planVersion.ts @@ -20,7 +20,7 @@ export const PLAN_VERSION_CLUSTER_SELECTION_TYPE = t.keyof({ multi: null, }); -const PLAN_VISIBILITY_PREFERENCES = t.type({ +export const PLAN_VISIBILITY_PREFERENCES = t.type({ isDisaggregationForCaseloads: t.boolean, isDisaggregationForIndicators: t.boolean, }); From 724846440cd246ecf17f40c140e7b361c68373f5 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 3 Sep 2024 16:21:07 +0800 Subject: [PATCH 06/11] Export `LOCATION_STATUS` --- src/db/models/location.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/location.ts b/src/db/models/location.ts index a44f65aa..e339da9a 100644 --- a/src/db/models/location.ts +++ b/src/db/models/location.ts @@ -12,7 +12,7 @@ export type LocationId = Brand< export const LOCATION_ID = brandedType(t.number); -const LOCATION_STATUS = t.keyof({ +export const LOCATION_STATUS = t.keyof({ active: null, expired: null, }); From 4a7e27efdd2ee7ff139a92629fcfcfbb1887762a Mon Sep 17 00:00:00 2001 From: enxtur Date: Wed, 4 Sep 2024 23:08:50 +0800 Subject: [PATCH 07/11] Add `SIMILAR` CONDITION --- src/db/util/conditions.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/db/util/conditions.ts b/src/db/util/conditions.ts index cb6f887c..228530c8 100644 --- a/src/db/util/conditions.ts +++ b/src/db/util/conditions.ts @@ -37,6 +37,7 @@ export namespace PropertySymbols { export const LTE = Symbol('less than or equal to'); export const GT = Symbol('greater than'); export const GTE = Symbol('greater than or equal to'); + export const SIMILAR = Symbol('similar to'); /** * Symbols to use when constructing conditions for a single property @@ -52,6 +53,7 @@ export namespace PropertySymbols { LTE: LTE, GT: GT, GTE: GTE, + SIMILAR: SIMILAR, } as const; } @@ -95,6 +97,9 @@ namespace PropertyConditions { export type GteCondition = { [Op.GTE]: T; }; + export type SimilarCondition = { + [Op.SIMILAR]: T & string; + }; /** * A condition that must hold over a single property whose type is T */ @@ -109,7 +114,8 @@ namespace PropertyConditions { | LtCondition | LteCondition | GtCondition - | GteCondition; + | GteCondition + | SimilarCondition; export const isEqualityCondition = ( condition: Condition @@ -165,6 +171,11 @@ namespace PropertyConditions { condition: Condition ): condition is GteCondition => Object.prototype.hasOwnProperty.call(condition, Op.GTE); + + export const isSimilarCondition = ( + condition: Condition + ): condition is SimilarCondition => + Object.prototype.hasOwnProperty.call(condition, Op.SIMILAR); } namespace OverallConditions { @@ -302,6 +313,12 @@ export const prepareCondition = builder.where(property as any, '>', propertyCondition[Op.GT]); } else if (PropertyConditions.isGteCondition(propertyCondition)) { builder.where(property as any, '>=', propertyCondition[Op.GTE]); + } else if (PropertyConditions.isSimilarCondition(propertyCondition)) { + builder.where( + property as string, + 'similar to', + propertyCondition[Op.SIMILAR] + ); } else { throw new Error(`Unexpected condition: ${propertyCondition}`); } From 4f8f537619b7ecc6b5494fe2c12b6cd18d8254b2 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:06:00 +0800 Subject: [PATCH 08/11] Export `METRIC_WITH_VALUE` --- src/db/models/json/indicatorsAndCaseloads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/json/indicatorsAndCaseloads.ts b/src/db/models/json/indicatorsAndCaseloads.ts index d5bdf5a4..452463c3 100644 --- a/src/db/models/json/indicatorsAndCaseloads.ts +++ b/src/db/models/json/indicatorsAndCaseloads.ts @@ -14,7 +14,7 @@ export type CaseloadOrIndicatorMetricDefinition = t.TypeOf< typeof METRIC_DEFINITION >; -const METRIC_WITH_VALUE = t.intersection([ +export const METRIC_WITH_VALUE = t.intersection([ METRIC_DEFINITION, t.partial({ /** From 0741c60ef1c4142a8f53a10990219190281e72a2 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:06:26 +0800 Subject: [PATCH 09/11] Export `DISAGGREGATED_DATA` --- src/db/models/json/indicatorsAndCaseloads.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/json/indicatorsAndCaseloads.ts b/src/db/models/json/indicatorsAndCaseloads.ts index 452463c3..abc2cd06 100644 --- a/src/db/models/json/indicatorsAndCaseloads.ts +++ b/src/db/models/json/indicatorsAndCaseloads.ts @@ -49,7 +49,7 @@ export const DISAGGREGATED_LOCATIONS = t.array( * Disaggregated data that may be present in a caseload, indicator, or * measurement for a caseload or indicator. */ -const DISAGGREGATED_DATA = t.type({ +export const DISAGGREGATED_DATA = t.type({ categories: t.array( t.type({ ids: t.array(t.number), From a3c8e282d6855de018f0fa2a6100517595d6c881 Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:07:54 +0800 Subject: [PATCH 10/11] Add `CONTAINS` condition --- src/db/util/conditions.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/db/util/conditions.ts b/src/db/util/conditions.ts index 228530c8..6c1fc22c 100644 --- a/src/db/util/conditions.ts +++ b/src/db/util/conditions.ts @@ -38,7 +38,7 @@ export namespace PropertySymbols { export const GT = Symbol('greater than'); export const GTE = Symbol('greater than or equal to'); export const SIMILAR = Symbol('similar to'); - + export const CONTAINS = Symbol('contains'); /** * Symbols to use when constructing conditions for a single property */ @@ -54,6 +54,7 @@ export namespace PropertySymbols { GT: GT, GTE: GTE, SIMILAR: SIMILAR, + CONTAINS: CONTAINS, } as const; } @@ -100,6 +101,9 @@ namespace PropertyConditions { export type SimilarCondition = { [Op.SIMILAR]: T & string; }; + export type ContainsCondition = { + [Op.CONTAINS]: T; + }; /** * A condition that must hold over a single property whose type is T */ @@ -115,7 +119,8 @@ namespace PropertyConditions { | LteCondition | GtCondition | GteCondition - | SimilarCondition; + | SimilarCondition + | ContainsCondition; export const isEqualityCondition = ( condition: Condition @@ -176,6 +181,11 @@ namespace PropertyConditions { condition: Condition ): condition is SimilarCondition => Object.prototype.hasOwnProperty.call(condition, Op.SIMILAR); + + export const isContainsCondition = ( + condition: Condition + ): condition is ContainsCondition => + Object.prototype.hasOwnProperty.call(condition, Op.CONTAINS); } namespace OverallConditions { @@ -319,6 +329,14 @@ export const prepareCondition = 'similar to', propertyCondition[Op.SIMILAR] ); + } else if (PropertyConditions.isContainsCondition(propertyCondition)) { + const prop = `"${String(property)}"::varchar[]`; + const value = propertyCondition[Op.CONTAINS]; + const values = (Array.isArray(value) ? value : [value]) + .map((v) => `'${v}'`) + .join(','); + const wrappedValues = `ARRAY[${values}]::varchar[]`; + builder.whereRaw(`${prop} @> ${wrappedValues}`); } else { throw new Error(`Unexpected condition: ${propertyCondition}`); } From ecb9a6b63568edeb5c4883ee5c390b64f76df6dd Mon Sep 17 00:00:00 2001 From: enxtur Date: Tue, 1 Oct 2024 00:08:13 +0800 Subject: [PATCH 11/11] Export `ExtendedFields` --- src/db/util/legacy-versioned-model.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/db/util/legacy-versioned-model.ts b/src/db/util/legacy-versioned-model.ts index dd64ab35..3aedd3df 100644 --- a/src/db/util/legacy-versioned-model.ts +++ b/src/db/util/legacy-versioned-model.ts @@ -29,7 +29,8 @@ const VERSIONED_FIELDS = { }, } as const; -type ExtendedFields = F & typeof VERSIONED_FIELDS; +export type ExtendedFields = F & + typeof VERSIONED_FIELDS; export type FieldsWithVersioned< F extends FieldDefinition,