@opencrvs/toolkit 2.0.0-rc.fe94e41 → 2.0.0-rc.fedb8b0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2394,14 +2394,20 @@ var encodeScope = (scope) => {
2394
2394
  encode: false
2395
2395
  });
2396
2396
  };
2397
- var decodeScope = (query) => {
2398
- const scope = qs.parse(query, {
2397
+ var decodedScopeCache = /* @__PURE__ */ new Map();
2398
+ var decodeScope = (encodedScope) => {
2399
+ if (decodedScopeCache.has(encodedScope)) {
2400
+ return decodedScopeCache.get(encodedScope);
2401
+ }
2402
+ const scope = qs.parse(encodedScope, {
2399
2403
  ignoreQueryPrefix: true,
2400
2404
  comma: true,
2401
2405
  allowDots: true
2402
2406
  });
2403
2407
  const unflattenedScope = unflattenScope(scope);
2404
- return Scope2.safeParse(unflattenedScope)?.data;
2408
+ const result = Scope2.safeParse(unflattenedScope)?.data;
2409
+ decodedScopeCache.set(encodedScope, result);
2410
+ return result;
2405
2411
  };
2406
2412
  var DEFAULT_SCOPE_OPTIONS = {
2407
2413
  placeOfEvent: JurisdictionFilter.enum.all,
@@ -888,7 +888,7 @@ var PlainDate = import_zod.z.string().date().brand("PlainDate").describe("Date i
888
888
  // ../commons/src/events/FieldValue.ts
889
889
  var TextValue = z12.string();
890
890
  var HiddenFieldValue = z12.string();
891
- var NonEmptyTextValue = TextValue.min(1);
891
+ var NonEmptyTextValue = z12.string().trim().min(1);
892
892
  var DateValue = z12.iso.date().describe("Date in the format YYYY-MM-DD");
893
893
  var AgeValue = z12.object({
894
894
  age: z12.number(),
@@ -917,6 +917,7 @@ var DateRangeFieldValue = z12.object({
917
917
  var EmailValue = z12.email();
918
918
  var CheckboxFieldValue = z12.boolean();
919
919
  var NumberFieldValue = z12.number();
920
+ var SignatureFieldValue = FileFieldValue;
920
921
  var ButtonFieldValue = z12.number();
921
922
  var VerificationStatusValue = z12.enum([
922
923
  "verified",
@@ -1666,8 +1667,18 @@ var FieldReference = import_v43.default.object({
1666
1667
  $$field: FieldId.describe("Id of the field to reference"),
1667
1668
  $$subfield: import_v43.default.array(import_v43.default.string()).optional().default([]).describe(
1668
1669
  'If the FieldValue is an object, subfield can be used to refer to e.g. `["foo", "bar"]` in `{ foo: { bar: 3 } }`'
1670
+ ),
1671
+ $$code: import_v43.default.string().optional().describe(
1672
+ "Serialised client-side function body. When present the expression is evaluated rather than dereferenced."
1673
+ )
1674
+ }).describe(
1675
+ "Reference to a field value, with an optional client-side computation applied."
1676
+ );
1677
+ var ComputedDefaultValue = import_v43.default.object({
1678
+ $$code: import_v43.default.string().describe(
1679
+ "Serialised client-side function body. Receives (undefined, context) where context includes $now, $online, and system variables."
1669
1680
  )
1670
- }).describe("Reference to a field by its ID");
1681
+ }).describe("A context-only computation used as a field default value.");
1671
1682
  var ValidationConfig = import_v43.default.object({
1672
1683
  validator: Conditional.describe(
1673
1684
  "Conditional expression that must hold for the field value to be considered valid."
@@ -1708,7 +1719,7 @@ var BaseField = import_v43.default.object({
1708
1719
  "Indicates whether the field can be modified during record correction."
1709
1720
  ),
1710
1721
  value: FieldReference.or(import_v43.default.array(FieldReference)).optional().describe(
1711
- "Reference to the source field or fields. When a value is defined, it is copied from the parent field when changed. If multiple references are provided, the first truthy value is used."
1722
+ "Reference to the source field or fields. When a value is defined, it is copied from the parent field when changed. If multiple references are provided, the first truthy value is used. A FieldReference with $$code computes the value via a custom client-side function."
1712
1723
  ),
1713
1724
  analytics: import_v43.default.boolean().default(false).optional().describe(
1714
1725
  "Indicates whether the field is included in analytics. When enabled, its value becomes available in the analytics dashboard."
@@ -1722,7 +1733,7 @@ var Divider = BaseField.extend({
1722
1733
  });
1723
1734
  var TextField = BaseField.extend({
1724
1735
  type: import_v43.default.literal(FieldType.TEXT),
1725
- defaultValue: import_v43.default.union([NonEmptyTextValue, SerializedUserField]).optional(),
1736
+ defaultValue: import_v43.default.union([NonEmptyTextValue, SerializedUserField, ComputedDefaultValue]).optional(),
1726
1737
  configuration: import_v43.default.object({
1727
1738
  maxLength: import_v43.default.number().optional().describe("Maximum length of the text"),
1728
1739
  type: import_v43.default.enum(["text", "password"]).optional(),
@@ -1735,7 +1746,7 @@ var TextField = BaseField.extend({
1735
1746
  });
1736
1747
  var NumberField = BaseField.extend({
1737
1748
  type: import_v43.default.literal(FieldType.NUMBER),
1738
- defaultValue: NumberFieldValue.optional(),
1749
+ defaultValue: NumberFieldValue.or(ComputedDefaultValue).optional(),
1739
1750
  configuration: import_v43.default.object({
1740
1751
  min: import_v43.default.number().optional().describe("Minimum value"),
1741
1752
  max: import_v43.default.number().optional().describe("Maximum value"),
@@ -1748,7 +1759,7 @@ var NumberField = BaseField.extend({
1748
1759
  });
1749
1760
  var TextAreaField = BaseField.extend({
1750
1761
  type: import_v43.default.literal(FieldType.TEXTAREA),
1751
- defaultValue: NonEmptyTextValue.optional(),
1762
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional(),
1752
1763
  configuration: import_v43.default.object({
1753
1764
  maxLength: import_v43.default.number().optional().describe("Maximum length of the text"),
1754
1765
  rows: import_v43.default.number().optional().describe("Number of visible text lines"),
@@ -1782,7 +1793,7 @@ var SignatureField = BaseField.extend({
1782
1793
  signaturePromptLabel: TranslationConfig.describe(
1783
1794
  "Title of the signature modal"
1784
1795
  ),
1785
- defaultValue: FieldValue.optional(),
1796
+ defaultValue: SignatureFieldValue.or(ComputedDefaultValue).optional(),
1786
1797
  configuration: import_v43.default.object({
1787
1798
  maxFileSize: import_v43.default.number().describe("Maximum file size in bytes").default(DEFAULT_MAX_FILE_SIZE_BYTES),
1788
1799
  acceptedFileTypes: MimeType.array().optional().describe("List of allowed file formats for the signature")
@@ -1798,14 +1809,14 @@ var EmailField = BaseField.extend({
1798
1809
  configuration: import_v43.default.object({
1799
1810
  maxLength: import_v43.default.number().optional().describe("Maximum length of the text")
1800
1811
  }).default({ maxLength: 255 }).optional(),
1801
- defaultValue: NonEmptyTextValue.optional()
1812
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional()
1802
1813
  }).meta({
1803
1814
  description: "An email input field",
1804
1815
  id: "EmailField"
1805
1816
  });
1806
1817
  var DateField = BaseField.extend({
1807
1818
  type: import_v43.default.literal(FieldType.DATE),
1808
- defaultValue: SerializedNowDateTime.or(PlainDate).optional().describe("Default date value(yyyy-MM-dd)"),
1819
+ defaultValue: SerializedNowDateTime.or(PlainDate).or(ComputedDefaultValue).optional().describe("Default date value(yyyy-MM-dd)"),
1809
1820
  configuration: import_v43.default.object({
1810
1821
  notice: TranslationConfig.describe(
1811
1822
  "Text to display above the date input"
@@ -1817,7 +1828,7 @@ var DateField = BaseField.extend({
1817
1828
  });
1818
1829
  var AgeField = BaseField.extend({
1819
1830
  type: import_v43.default.literal(FieldType.AGE),
1820
- defaultValue: NumberFieldValue.optional(),
1831
+ defaultValue: NumberFieldValue.or(ComputedDefaultValue).optional(),
1821
1832
  configuration: import_v43.default.object({
1822
1833
  asOfDate: FieldReference,
1823
1834
  prefix: TranslationConfig.optional(),
@@ -1829,7 +1840,7 @@ var AgeField = BaseField.extend({
1829
1840
  });
1830
1841
  var TimeField = BaseField.extend({
1831
1842
  type: import_v43.default.literal(FieldType.TIME),
1832
- defaultValue: SerializedNowDateTime.or(TimeValue).optional().describe("Default time value (HH-mm)"),
1843
+ defaultValue: SerializedNowDateTime.or(TimeValue).or(ComputedDefaultValue).optional().describe("Default time value (HH-mm)"),
1833
1844
  configuration: import_v43.default.object({
1834
1845
  use12HourFormat: import_v43.default.boolean().optional().describe("Whether to use 12-hour format"),
1835
1846
  notice: TranslationConfig.describe(
@@ -1842,7 +1853,7 @@ var TimeField = BaseField.extend({
1842
1853
  });
1843
1854
  var DateRangeField = BaseField.extend({
1844
1855
  type: import_v43.default.literal(FieldType.DATE_RANGE),
1845
- defaultValue: DateRangeFieldValue.optional(),
1856
+ defaultValue: DateRangeFieldValue.or(ComputedDefaultValue).optional(),
1846
1857
  configuration: import_v43.default.object({
1847
1858
  notice: TranslationConfig.describe(
1848
1859
  "Text to display above the date input"
@@ -1921,7 +1932,7 @@ var PageHeader = BaseField.extend({
1921
1932
  });
1922
1933
  var File = BaseField.extend({
1923
1934
  type: import_v43.default.literal(FieldType.FILE),
1924
- defaultValue: FileFieldValue.optional(),
1935
+ defaultValue: FileFieldValue.or(ComputedDefaultValue).optional(),
1925
1936
  configuration: import_v43.default.object({
1926
1937
  maxFileSize: import_v43.default.number().describe("Maximum file size in bytes").default(DEFAULT_MAX_FILE_SIZE_BYTES),
1927
1938
  acceptedFileTypes: MimeType.array().optional().describe("List of allowed file formats for the signature"),
@@ -1948,7 +1959,7 @@ var SelectOption = import_v43.default.object({
1948
1959
  });
1949
1960
  var NumberWithUnitField = BaseField.extend({
1950
1961
  type: import_v43.default.literal(FieldType.NUMBER_WITH_UNIT),
1951
- defaultValue: NumberWithUnitFieldValue.optional(),
1962
+ defaultValue: NumberWithUnitFieldValue.or(ComputedDefaultValue).optional(),
1952
1963
  options: import_v43.default.array(SelectOption).describe("A list of options for the unit select"),
1953
1964
  configuration: import_v43.default.object({
1954
1965
  min: import_v43.default.number().optional().describe("Minimum value of the number field"),
@@ -1963,7 +1974,7 @@ var NumberWithUnitField = BaseField.extend({
1963
1974
  });
1964
1975
  var RadioGroup = BaseField.extend({
1965
1976
  type: import_v43.default.literal(FieldType.RADIO_GROUP),
1966
- defaultValue: TextValue.optional(),
1977
+ defaultValue: TextValue.or(ComputedDefaultValue).optional(),
1967
1978
  options: import_v43.default.array(SelectOption).describe("A list of options"),
1968
1979
  configuration: import_v43.default.object({
1969
1980
  styles: import_v43.default.object({
@@ -1988,7 +1999,7 @@ var BulletList = BaseField.extend({
1988
1999
  });
1989
2000
  var Select = BaseField.extend({
1990
2001
  type: import_v43.default.literal(FieldType.SELECT),
1991
- defaultValue: TextValue.optional(),
2002
+ defaultValue: TextValue.or(ComputedDefaultValue).optional(),
1992
2003
  options: import_v43.default.array(SelectOption).describe("A list of options"),
1993
2004
  noOptionsMessage: TranslationConfig.optional().describe(
1994
2005
  `
@@ -2010,7 +2021,7 @@ var SelectDateRangeOption = import_v43.default.object({
2010
2021
  });
2011
2022
  var SelectDateRangeField = BaseField.extend({
2012
2023
  type: import_v43.default.literal(FieldType.SELECT_DATE_RANGE),
2013
- defaultValue: SelectDateRangeValue.optional(),
2024
+ defaultValue: SelectDateRangeValue.or(ComputedDefaultValue).optional(),
2014
2025
  options: import_v43.default.array(SelectDateRangeOption).describe("A list of options")
2015
2026
  }).meta({
2016
2027
  description: "A date range selection field",
@@ -2027,7 +2038,7 @@ var NameField = BaseField.extend({
2027
2038
  firstname: SerializedUserField.or(NonEmptyTextValue).optional(),
2028
2039
  middlename: SerializedUserField.or(NonEmptyTextValue).optional(),
2029
2040
  surname: SerializedUserField.or(NonEmptyTextValue).optional()
2030
- }).optional(),
2041
+ }).or(ComputedDefaultValue).optional(),
2031
2042
  configuration: import_v43.default.object({
2032
2043
  name: NameConfig.default({
2033
2044
  firstname: { required: true },
@@ -2051,14 +2062,14 @@ var NameField = BaseField.extend({
2051
2062
  id: "NameField"
2052
2063
  });
2053
2064
  var PhoneField = BaseField.extend({
2054
- defaultValue: NonEmptyTextValue.optional(),
2065
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional(),
2055
2066
  type: import_v43.default.literal(FieldType.PHONE)
2056
2067
  }).meta({
2057
2068
  description: "A field for entering a phone number",
2058
2069
  id: "PhoneField"
2059
2070
  });
2060
2071
  var IdField = BaseField.extend({
2061
- defaultValue: NonEmptyTextValue.optional(),
2072
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional(),
2062
2073
  type: import_v43.default.literal(FieldType.ID)
2063
2074
  }).meta({
2064
2075
  description: "A field for entering an ID",
@@ -2066,14 +2077,14 @@ var IdField = BaseField.extend({
2066
2077
  });
2067
2078
  var Checkbox = BaseField.extend({
2068
2079
  type: import_v43.default.literal(FieldType.CHECKBOX),
2069
- defaultValue: CheckboxFieldValue.default(false)
2080
+ defaultValue: CheckboxFieldValue.or(ComputedDefaultValue).default(false)
2070
2081
  }).meta({
2071
2082
  description: "A boolean checkbox field",
2072
2083
  id: "Checkbox"
2073
2084
  });
2074
2085
  var Country = BaseField.extend({
2075
2086
  type: import_v43.default.literal(FieldType.COUNTRY),
2076
- defaultValue: NonEmptyTextValue.optional(),
2087
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional(),
2077
2088
  optionOverrides: import_v43.default.array(SelectOption.omit({ label: true })).optional().describe(
2078
2089
  "Conditionals for specific countries. Countries not listed are always shown and enabled."
2079
2090
  )
@@ -2091,7 +2102,7 @@ var AdministrativeAreas = import_v43.default.enum([
2091
2102
  ]);
2092
2103
  var AdministrativeAreaField = BaseField.extend({
2093
2104
  type: import_v43.default.literal(FieldType.ADMINISTRATIVE_AREA),
2094
- defaultValue: import_v43.default.union([NonEmptyTextValue, SerializedUserField]).optional(),
2105
+ defaultValue: import_v43.default.union([NonEmptyTextValue, SerializedUserField, ComputedDefaultValue]).optional(),
2095
2106
  configuration: import_v43.default.object({
2096
2107
  partOf: FieldReference.optional().describe("Parent location"),
2097
2108
  type: AdministrativeAreas,
@@ -2103,7 +2114,7 @@ var AdministrativeAreaField = BaseField.extend({
2103
2114
  });
2104
2115
  var LocationInput = BaseField.extend({
2105
2116
  type: import_v43.default.literal(FieldType.LOCATION),
2106
- defaultValue: import_v43.default.union([NonEmptyTextValue, SerializedUserField]).optional(),
2117
+ defaultValue: import_v43.default.union([NonEmptyTextValue, SerializedUserField, ComputedDefaultValue]).optional(),
2107
2118
  configuration: import_v43.default.object({
2108
2119
  locationTypes: import_v43.default.array(import_v43.default.string()).optional().describe("Types of the locations that are available for selection."),
2109
2120
  allowedLocations: AllowedLocations
@@ -2115,7 +2126,7 @@ var LocationInput = BaseField.extend({
2115
2126
  var FileUploadWithOptions = BaseField.extend({
2116
2127
  type: import_v43.default.literal(FieldType.FILE_WITH_OPTIONS),
2117
2128
  options: import_v43.default.array(SelectOption).describe("A list of options"),
2118
- defaultValue: FileFieldWithOptionValue.optional(),
2129
+ defaultValue: FileFieldWithOptionValue.or(ComputedDefaultValue).optional(),
2119
2130
  configuration: import_v43.default.object({
2120
2131
  maxFileSize: import_v43.default.number().describe("Maximum file size in bytes").default(DEFAULT_MAX_FILE_SIZE_BYTES),
2121
2132
  maxImageSize: import_v43.default.object({
@@ -2131,12 +2142,12 @@ var FileUploadWithOptions = BaseField.extend({
2131
2142
  });
2132
2143
  var Facility = BaseField.extend({
2133
2144
  type: import_v43.default.literal(FieldType.FACILITY),
2134
- defaultValue: NonEmptyTextValue.optional(),
2145
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional(),
2135
2146
  configuration: import_v43.default.object({ allowedLocations: AllowedLocations }).optional()
2136
2147
  }).describe("Input field for a facility");
2137
2148
  var Office = BaseField.extend({
2138
2149
  type: import_v43.default.literal(FieldType.OFFICE),
2139
- defaultValue: NonEmptyTextValue.optional(),
2150
+ defaultValue: NonEmptyTextValue.or(ComputedDefaultValue).optional(),
2140
2151
  configuration: import_v43.default.object({ allowedLocations: AllowedLocations }).optional()
2141
2152
  }).describe("Input field for an office");
2142
2153
  var DefaultAddressFieldValue = DomesticAddressFieldValue.extend({
@@ -2172,7 +2183,7 @@ var Address = BaseField.extend({
2172
2183
  ).optional(),
2173
2184
  allowedLocations: AllowedLocations
2174
2185
  }).optional(),
2175
- defaultValue: DefaultAddressFieldValue.optional()
2186
+ defaultValue: DefaultAddressFieldValue.or(ComputedDefaultValue).optional()
2176
2187
  }).meta({
2177
2188
  description: "Address input field \u2013 a combination of location and text fields",
2178
2189
  id: "Address"
@@ -2219,7 +2230,7 @@ var ButtonConfiguration = import_v43.default.object({
2219
2230
  });
2220
2231
  var ButtonField = BaseField.extend({
2221
2232
  type: import_v43.default.literal(FieldType.BUTTON),
2222
- defaultValue: ButtonFieldValue.optional(),
2233
+ defaultValue: ButtonFieldValue.or(ComputedDefaultValue).optional(),
2223
2234
  configuration: ButtonConfiguration
2224
2235
  }).meta({
2225
2236
  description: "A generic button that can be used to trigger an action",
@@ -2247,7 +2258,7 @@ var AlphaPrintButton = BaseField.extend({
2247
2258
  });
2248
2259
  var HttpField = BaseField.extend({
2249
2260
  type: import_v43.default.literal(FieldType.HTTP),
2250
- defaultValue: HttpFieldValue.optional(),
2261
+ defaultValue: HttpFieldValue.or(ComputedDefaultValue).optional(),
2251
2262
  configuration: import_v43.default.object({
2252
2263
  trigger: FieldReference.optional().describe(
2253
2264
  "Reference to the field that triggers the HTTP request when its value changes. If not provided, the HTTP request is triggered once on component mount."
@@ -2333,7 +2344,7 @@ var LinkButtonField = BaseField.extend({
2333
2344
  });
2334
2345
  var VerificationStatus = BaseField.extend({
2335
2346
  type: import_v43.default.literal(FieldType.VERIFICATION_STATUS),
2336
- defaultValue: VerificationStatusValue.optional(),
2347
+ defaultValue: VerificationStatusValue.or(ComputedDefaultValue).optional(),
2337
2348
  configuration: import_v43.default.object({
2338
2349
  status: TranslationConfig.describe("Text to display on the status pill."),
2339
2350
  description: TranslationConfig.describe(
@@ -2355,7 +2366,7 @@ var QueryParamReaderField = BaseField.extend({
2355
2366
  });
2356
2367
  var QrReaderField = BaseField.extend({
2357
2368
  type: import_v43.default.literal(FieldType.QR_READER),
2358
- defaultValue: QrReaderFieldValue.optional(),
2369
+ defaultValue: QrReaderFieldValue.or(ComputedDefaultValue).optional(),
2359
2370
  configuration: import_v43.default.object({
2360
2371
  validator: import_v43.default.any().meta({
2361
2372
  description: "JSON Schema to validate the scanned QR code data against before populating the form fields.",
@@ -2368,7 +2379,7 @@ var QrReaderField = BaseField.extend({
2368
2379
  });
2369
2380
  var IdReaderField = BaseField.extend({
2370
2381
  type: import_v43.default.literal(FieldType.ID_READER),
2371
- defaultValue: IdReaderFieldValue.optional(),
2382
+ defaultValue: IdReaderFieldValue.or(ComputedDefaultValue).optional(),
2372
2383
  methods: import_v43.default.array(
2373
2384
  import_v43.default.union([QrReaderField, LinkButtonField]).describe("Methods for reading an ID")
2374
2385
  )
@@ -2927,6 +2938,9 @@ var BaseField3 = z28.object({
2927
2938
  ),
2928
2939
  validations: z28.array(ValidationConfig).optional().describe(
2929
2940
  `Option for overriding the field validations specifically for advanced search form.`
2941
+ ),
2942
+ allowedLocations: JurisdictionReference.optional().describe(
2943
+ `Override the allowedLocations for a location field in advanced search. Use this when the declaration form's allowedLocations references a scope (e.g. record.create) that search-only users don't have \u2014 specify record.search scope instead.`
2930
2944
  )
2931
2945
  });
2932
2946
  var SearchQueryParams = z28.object({
@@ -3033,7 +3047,37 @@ function resolveDataPath(rootData, dataPath, instancePath) {
3033
3047
  }
3034
3048
  return current;
3035
3049
  }
3050
+ function todayISO() {
3051
+ return (0, import_date_fns.formatISO)(/* @__PURE__ */ new Date(), { representation: "date" });
3052
+ }
3053
+ var compiledFunctionCache = /* @__PURE__ */ new Map();
3054
+ function compileClientFunction(code) {
3055
+ let fn = compiledFunctionCache.get(code);
3056
+ if (!fn) {
3057
+ fn = new Function(`return (${code})`)();
3058
+ compiledFunctionCache.set(code, fn);
3059
+ }
3060
+ return fn;
3061
+ }
3036
3062
  (0, import_ajv_formats.default)(ajv);
3063
+ function buildClientFunctionContext(input) {
3064
+ return {
3065
+ $form: input.form,
3066
+ $now: todayISO(),
3067
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
3068
+ $online: isOnline(),
3069
+ $user: input.validatorContext?.user,
3070
+ $event: input.validatorContext?.event,
3071
+ $leafAdminStructureLocationIds: input.validatorContext?.leafAdminStructureLocationIds ?? [],
3072
+ user: input.systemVariables?.user,
3073
+ $window: input.systemVariables?.$window,
3074
+ locations: input.locations,
3075
+ adminLevelIds: input.adminLevelIds
3076
+ };
3077
+ }
3078
+ function runClientFunction(code, data, context) {
3079
+ return compileClientFunction(code)(data, context);
3080
+ }
3037
3081
  ajv.addKeyword({
3038
3082
  keyword: "daysFromDate",
3039
3083
  type: "string",
@@ -3078,12 +3122,35 @@ ajv.addKeyword({
3078
3122
  $data: true,
3079
3123
  errors: true,
3080
3124
  // @ts-ignore -- Force type. We will move this away from AJV next. Parsing the array will take seconds and is only called by core.
3081
- validate(schema, data, _2, dataContext) {
3125
+ validate(_schema, data, _2, dataContext) {
3082
3126
  const locationIdInput = data;
3083
3127
  const locations = dataContext?.rootData.$leafAdminStructureLocationIds ?? [];
3084
3128
  return locations.some((location) => location.id === locationIdInput);
3085
3129
  }
3086
3130
  });
3131
+ ajv.addKeyword({
3132
+ keyword: "customClientValidator",
3133
+ schemaType: "object",
3134
+ errors: true,
3135
+ // @ts-expect-error -- AJV's public types don't expose `rootData`. All
3136
+ // `validate()` callers build root data via `buildClientFunctionContext`,
3137
+ // so the cast holds.
3138
+ validate(schema, data, _2, dataContext) {
3139
+ return Boolean(
3140
+ runClientFunction(
3141
+ schema.code,
3142
+ data,
3143
+ dataContext?.rootData ?? buildClientFunctionContext({ form: {} })
3144
+ )
3145
+ );
3146
+ }
3147
+ });
3148
+ function isOnline() {
3149
+ if (typeof window !== "undefined" && typeof navigator !== "undefined") {
3150
+ return navigator.onLine;
3151
+ }
3152
+ return true;
3153
+ }
3087
3154
 
3088
3155
  // ../commons/src/utils.ts
3089
3156
  var z32 = __toESM(require("zod/v4"));
@@ -3702,7 +3769,54 @@ function createFieldConditionals(fieldId) {
3702
3769
  }
3703
3770
  },
3704
3771
  required: [fieldId]
3705
- })
3772
+ }),
3773
+ /**
3774
+ * Custom client-side validator. The provided function is serialised and executed
3775
+ * just-in-time on the client only. External references (e.g. lodash) are not
3776
+ * available inside the function body — all logic must be self-contained.
3777
+ *
3778
+ * @example
3779
+ * field('nid').customClientValidator((value) => {
3780
+ * // LUHN check — all logic must be inline
3781
+ * const digits = String(value).split('').map(Number)
3782
+ * // ...
3783
+ * return isValid
3784
+ * })
3785
+ */
3786
+ customClientValidator(validationFn) {
3787
+ const code = validationFn.toString();
3788
+ return defineFormConditional({
3789
+ type: "object",
3790
+ properties: wrapToPath(
3791
+ { [fieldId]: { customClientValidator: { code } } },
3792
+ this.$$subfield
3793
+ ),
3794
+ required: [fieldId]
3795
+ });
3796
+ },
3797
+ /**
3798
+ * Custom client-side evaluation. Returns a {@link FieldReference} descriptor
3799
+ * that can be used as the `value` property or a DATA component entry.
3800
+ * The function receives the referenced field's value as the first argument and
3801
+ * the full form context as the second; its return value replaces the field reference.
3802
+ * The function is serialised and executed just-in-time on the client only.
3803
+ * External references (e.g. lodash) are not available inside the function body.
3804
+ *
3805
+ * For computing a default value without referencing a specific field, use
3806
+ * `evaluate(fn)` in the `defaultValue` property instead.
3807
+ *
3808
+ * @example
3809
+ * field('a').customClientEvaluation((aValue, ctx) =>
3810
+ * Number(aValue) + Number(ctx.$form.b)
3811
+ * )
3812
+ */
3813
+ customClientEvaluation(computationFn) {
3814
+ return {
3815
+ $$code: computationFn.toString(),
3816
+ $$field: fieldId,
3817
+ $$subfield: this.$$subfield
3818
+ };
3819
+ }
3706
3820
  };
3707
3821
  }
3708
3822
 
@@ -580,14 +580,12 @@ export type EncodedScope = z.infer<typeof EncodedScope>;
580
580
  */
581
581
  export declare const encodeScope: (scope: Scope) => EncodedScope;
582
582
  /**
583
- * Converts a scope object into an encoded query string representation.
583
+ * Converts an encoded scope string into a scope object.
584
584
  *
585
- * @TODO scope param could be defined as EncodedScope instead of string.
586
- *
587
- * @param scope - The scope object to encode.
588
- * @returns The encoded scope as a branded string (`EncodedScope`).
585
+ * @param scope - The encoded scope string to decode.
586
+ * @returns The decoded scope object.
589
587
  */
590
- export declare const decodeScope: (query: EncodedScope) => {
588
+ export declare const decodeScope: (encodedScope: EncodedScope) => {
591
589
  type: "record.create" | "record.declare" | "record.notify";
592
590
  options?: {
593
591
  event?: string[] | undefined;
@@ -354,14 +354,20 @@ var encodeScope = (scope) => {
354
354
  encode: false
355
355
  });
356
356
  };
357
- var decodeScope = (query) => {
358
- const scope = qs.parse(query, {
357
+ var decodedScopeCache = /* @__PURE__ */ new Map();
358
+ var decodeScope = (encodedScope) => {
359
+ if (decodedScopeCache.has(encodedScope)) {
360
+ return decodedScopeCache.get(encodedScope);
361
+ }
362
+ const scope = qs.parse(encodedScope, {
359
363
  ignoreQueryPrefix: true,
360
364
  comma: true,
361
365
  allowDots: true
362
366
  });
363
367
  const unflattenedScope = unflattenScope(scope);
364
- return Scope2.safeParse(unflattenedScope)?.data;
368
+ const result = Scope2.safeParse(unflattenedScope)?.data;
369
+ decodedScopeCache.set(encodedScope, result);
370
+ return result;
365
371
  };
366
372
  var DEFAULT_SCOPE_OPTIONS = {
367
373
  placeOfEvent: JurisdictionFilter.enum.all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencrvs/toolkit",
3
- "version": "2.0.0-rc.fe94e41",
3
+ "version": "2.0.0-rc.fedb8b0",
4
4
  "description": "OpenCRVS toolkit for building country configurations",
5
5
  "license": "MPL-2.0",
6
6
  "bin": {