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); 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, }), ]) ), 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, }), ]) ); diff --git a/src/db/models/json/indicatorsAndCaseloads.ts b/src/db/models/json/indicatorsAndCaseloads.ts index d5bdf5a4..abc2cd06 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({ /** @@ -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), 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, }); diff --git a/src/db/models/planVersion.ts b/src/db/models/planVersion.ts index bfcd2d10..ec765513 100644 --- a/src/db/models/planVersion.ts +++ b/src/db/models/planVersion.ts @@ -15,12 +15,12 @@ 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, }); -const PLAN_VISIBILITY_PREFERENCES = t.type({ +export const PLAN_VISIBILITY_PREFERENCES = t.type({ isDisaggregationForCaseloads: t.boolean, isDisaggregationForIndicators: t.boolean, }); diff --git a/src/db/util/conditions.ts b/src/db/util/conditions.ts index cb6f887c..6c1fc22c 100644 --- a/src/db/util/conditions.ts +++ b/src/db/util/conditions.ts @@ -37,7 +37,8 @@ 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'); + export const CONTAINS = Symbol('contains'); /** * Symbols to use when constructing conditions for a single property */ @@ -52,6 +53,8 @@ export namespace PropertySymbols { LTE: LTE, GT: GT, GTE: GTE, + SIMILAR: SIMILAR, + CONTAINS: CONTAINS, } as const; } @@ -95,6 +98,12 @@ namespace PropertyConditions { export type GteCondition = { [Op.GTE]: T; }; + 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 */ @@ -109,7 +118,9 @@ namespace PropertyConditions { | LtCondition | LteCondition | GtCondition - | GteCondition; + | GteCondition + | SimilarCondition + | ContainsCondition; export const isEqualityCondition = ( condition: Condition @@ -165,6 +176,16 @@ 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); + + export const isContainsCondition = ( + condition: Condition + ): condition is ContainsCondition => + Object.prototype.hasOwnProperty.call(condition, Op.CONTAINS); } namespace OverallConditions { @@ -302,6 +323,20 @@ 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 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}`); } 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,