@opencrvs/toolkit 1.8.0-rc.ff73871 → 1.8.0-rc.ffe8c17

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.
@@ -51,6 +51,7 @@ __export(events_exports, {
51
51
  Clause: () => Clause,
52
52
  Conditional: () => Conditional,
53
53
  ConditionalType: () => ConditionalType,
54
+ DataFieldValue: () => DataFieldValue,
54
55
  DateValue: () => DateValue,
55
56
  DeclareActionInput: () => DeclareActionInput,
56
57
  DeduplicationConfig: () => DeduplicationConfig,
@@ -81,6 +82,8 @@ __export(events_exports, {
81
82
  MarkedAsDuplicateActionInput: () => MarkedAsDuplicateActionInput,
82
83
  NotifyActionInput: () => NotifyActionInput,
83
84
  NumberFieldValue: () => NumberFieldValue,
85
+ PageConfig: () => PageConfig,
86
+ PageType: () => PageType,
84
87
  PrintCertificateActionInput: () => PrintCertificateActionInput,
85
88
  RegisterActionInput: () => RegisterActionInput,
86
89
  RejectCorrectionActionInput: () => RejectCorrectionActionInput,
@@ -97,17 +100,21 @@ __export(events_exports, {
97
100
  UrbanAddressUpdateValue: () => UrbanAddressUpdateValue,
98
101
  UrbanAddressValue: () => UrbanAddressValue,
99
102
  ValidateActionInput: () => ValidateActionInput,
103
+ VerificationPage: () => VerificationPage,
104
+ VerificationPageConfig: () => VerificationPageConfig,
100
105
  WorkqueueConfig: () => WorkqueueConfig,
101
106
  alwaysTrue: () => alwaysTrue,
102
107
  and: () => and,
103
108
  applyDraftsToEventIndex: () => applyDraftsToEventIndex,
104
109
  compositeFieldTypes: () => compositeFieldTypes,
110
+ createEmptyDraft: () => createEmptyDraft,
105
111
  createValidationSchema: () => createValidationSchema,
106
112
  deepDropNulls: () => deepDropNulls,
107
113
  defineConditional: () => defineConditional,
108
114
  defineConfig: () => defineConfig,
109
115
  defineForm: () => defineForm,
110
- defineFormPage: () => defineFormPage,
116
+ definePage: () => definePage,
117
+ errorMessages: () => errorMessages,
111
118
  event: () => event,
112
119
  eventMetadataLabelMap: () => eventMetadataLabelMap,
113
120
  eventPayloadGenerator: () => eventPayloadGenerator,
@@ -119,6 +126,7 @@ __export(events_exports, {
119
126
  findActiveActionForm: () => findActiveActionForm,
120
127
  findActiveActionFormFields: () => findActiveActionFormFields,
121
128
  findActiveActionFormPages: () => findActiveActionFormPages,
129
+ findActiveDrafts: () => findActiveDrafts,
122
130
  findInputPageFields: () => findInputPageFields,
123
131
  findPageFields: () => findPageFields,
124
132
  generateActionDocument: () => generateActionDocument,
@@ -141,6 +149,7 @@ __export(events_exports, {
141
149
  isBulletListFieldType: () => isBulletListFieldType,
142
150
  isCheckboxFieldType: () => isCheckboxFieldType,
143
151
  isCountryFieldType: () => isCountryFieldType,
152
+ isDataFieldType: () => isDataFieldType,
144
153
  isDateFieldType: () => isDateFieldType,
145
154
  isDividerFieldType: () => isDividerFieldType,
146
155
  isEmailFieldType: () => isEmailFieldType,
@@ -248,7 +257,8 @@ var FieldType = {
248
257
  ADMINISTRATIVE_AREA: "ADMINISTRATIVE_AREA",
249
258
  FACILITY: "FACILITY",
250
259
  OFFICE: "OFFICE",
251
- SIGNATURE: "SIGNATURE"
260
+ SIGNATURE: "SIGNATURE",
261
+ DATA: "DATA"
252
262
  };
253
263
  var fieldTypes = Object.values(FieldType);
254
264
  var compositeFieldTypes = [
@@ -333,6 +343,7 @@ var FieldValue = import_zod4.z.union([
333
343
  UrbanAddressValue,
334
344
  RuralAddressValue
335
345
  ]);
346
+ var DataFieldValue = import_zod4.z.record(import_zod4.z.string(), FieldValue);
336
347
  var FieldUpdateValue = import_zod4.z.union([
337
348
  TextValue,
338
349
  DateValue,
@@ -544,6 +555,14 @@ var Address = BaseField.extend({
544
555
  type: import_zod5.z.literal(FieldType.ADDRESS),
545
556
  defaultValue: AddressFieldValue.optional()
546
557
  }).describe("Address input field \u2013 a combination of location and text fields");
558
+ var Data = BaseField.extend({
559
+ type: import_zod5.z.literal(FieldType.DATA),
560
+ configuration: import_zod5.z.object({
561
+ title: TranslationConfig.optional(),
562
+ subtitle: TranslationConfig.optional(),
563
+ data: import_zod5.z.array(import_zod5.z.object({ fieldId: import_zod5.z.string() }))
564
+ })
565
+ }).describe("Data field for displaying read-only data");
547
566
  var FieldConfig = import_zod5.z.discriminatedUnion("type", [
548
567
  Address,
549
568
  TextField,
@@ -565,15 +584,44 @@ var FieldConfig = import_zod5.z.discriminatedUnion("type", [
565
584
  Office,
566
585
  SignatureField,
567
586
  EmailField,
568
- FileUploadWithOptions
587
+ FileUploadWithOptions,
588
+ Data
569
589
  ]);
570
590
 
571
591
  // ../commons/src/events/FormConfig.ts
592
+ var PageType = /* @__PURE__ */ ((PageType2) => {
593
+ PageType2["FORM"] = "FORM";
594
+ PageType2["VERIFICATION"] = "VERIFICATION";
595
+ return PageType2;
596
+ })(PageType || {});
572
597
  var FormPage = import_zod6.z.object({
573
598
  id: import_zod6.z.string().describe("Unique identifier for the page"),
574
599
  title: TranslationConfig.describe("Header title of the page"),
575
- fields: import_zod6.z.array(FieldConfig).describe("Fields to be rendered on the page")
600
+ fields: import_zod6.z.array(FieldConfig).describe("Fields to be rendered on the page"),
601
+ type: import_zod6.z.literal("FORM" /* FORM */).default("FORM" /* FORM */)
602
+ });
603
+ var VerificationPageConfig = import_zod6.z.object({
604
+ verify: import_zod6.z.object({ label: TranslationConfig }),
605
+ cancel: import_zod6.z.object({
606
+ label: TranslationConfig,
607
+ confirmation: import_zod6.z.object({
608
+ title: TranslationConfig,
609
+ body: TranslationConfig
610
+ })
611
+ })
612
+ }).describe("Actions available on the verification page");
613
+ var VerificationPage = FormPage.extend({
614
+ type: import_zod6.z.literal("VERIFICATION" /* VERIFICATION */),
615
+ actions: VerificationPageConfig
576
616
  });
617
+ var PageConfig = import_zod6.z.preprocess(
618
+ (data) => ({
619
+ type: data.type ?? "FORM" /* FORM */,
620
+ // Default type to "FORM" if not provided
621
+ ...data
622
+ }),
623
+ import_zod6.z.discriminatedUnion("type", [FormPage, VerificationPage])
624
+ );
577
625
  var FormConfig = import_zod6.z.object({
578
626
  label: TranslationConfig.describe("Human readable description of the form"),
579
627
  version: import_zod6.z.object({
@@ -585,7 +633,7 @@ var FormConfig = import_zod6.z.object({
585
633
  )
586
634
  }),
587
635
  active: import_zod6.z.boolean().default(false).describe("Whether the form is active"),
588
- pages: import_zod6.z.array(FormPage),
636
+ pages: import_zod6.z.array(PageConfig),
589
637
  review: import_zod6.z.object({
590
638
  title: TranslationConfig.describe(
591
639
  "Title of the form to show in review page"
@@ -640,23 +688,17 @@ var ValidateConfig = ActionConfigBase.merge(
640
688
  );
641
689
  var RejectDeclarationConfig = ActionConfigBase.merge(
642
690
  import_zod7.z.object({
643
- type: import_zod7.z.literal(ActionType.REJECT),
644
- comment: import_zod7.z.string(),
645
- isDuplicate: import_zod7.z.boolean()
691
+ type: import_zod7.z.literal(ActionType.REJECT)
646
692
  })
647
693
  );
648
694
  var MarkedAsDuplicateConfig = ActionConfigBase.merge(
649
695
  import_zod7.z.object({
650
- type: import_zod7.z.literal(ActionType.MARKED_AS_DUPLICATE),
651
- comment: import_zod7.z.string(),
652
- duplicates: import_zod7.z.array(import_zod7.z.string()).describe("UUIDs of duplicate records")
696
+ type: import_zod7.z.literal(ActionType.MARKED_AS_DUPLICATE)
653
697
  })
654
698
  );
655
699
  var ArchiveConfig = ActionConfigBase.merge(
656
700
  import_zod7.z.object({
657
- type: import_zod7.z.literal(ActionType.ARCHIVE),
658
- comment: import_zod7.z.string(),
659
- isDuplicate: import_zod7.z.boolean()
701
+ type: import_zod7.z.literal(ActionType.ARCHIVE)
660
702
  })
661
703
  );
662
704
  var RegisterConfig = ActionConfigBase.merge(
@@ -677,8 +719,8 @@ var PrintCertificateActionConfig = ActionConfigBase.merge(
677
719
  var RequestCorrectionConfig = ActionConfigBase.merge(
678
720
  import_zod7.z.object({
679
721
  type: import_zod7.z.literal(ActionType.REQUEST_CORRECTION),
680
- onboardingForm: import_zod7.z.array(FormPage),
681
- additionalDetailsForm: import_zod7.z.array(FormPage)
722
+ onboardingForm: import_zod7.z.array(PageConfig),
723
+ additionalDetailsForm: import_zod7.z.array(PageConfig)
682
724
  })
683
725
  );
684
726
  var RejectCorrectionConfig = ActionConfigBase.merge(
@@ -1120,6 +1162,9 @@ function mapFieldTypeToZod(type, required) {
1120
1162
  case FieldType.ADDRESS:
1121
1163
  schema = AddressFieldUpdateValue;
1122
1164
  break;
1165
+ case FieldType.DATA:
1166
+ schema = DataFieldValue;
1167
+ break;
1123
1168
  }
1124
1169
  return required ? schema : schema.nullish();
1125
1170
  }
@@ -1175,6 +1220,8 @@ function mapFieldTypeToMockValue(field2, i) {
1175
1220
  };
1176
1221
  case FieldType.FILE_WITH_OPTIONS:
1177
1222
  return null;
1223
+ case FieldType.DATA:
1224
+ return {};
1178
1225
  }
1179
1226
  }
1180
1227
  function mapFieldTypeToEmptyValue(field2) {
@@ -1197,6 +1244,7 @@ function mapFieldTypeToEmptyValue(field2) {
1197
1244
  case FieldType.EMAIL:
1198
1245
  case FieldType.DATE:
1199
1246
  case FieldType.CHECKBOX:
1247
+ case FieldType.DATA:
1200
1248
  return null;
1201
1249
  case FieldType.ADDRESS:
1202
1250
  return {
@@ -1284,6 +1332,9 @@ var isFacilityFieldType = (field2) => {
1284
1332
  var isOfficeFieldType = (field2) => {
1285
1333
  return field2.config.type === FieldType.OFFICE;
1286
1334
  };
1335
+ var isDataFieldType = (field2) => {
1336
+ return field2.config.type === FieldType.DATA;
1337
+ };
1287
1338
 
1288
1339
  // ../commons/src/conditionals/validate.ts
1289
1340
  var ajv = new import_ajv.default({
@@ -1320,49 +1371,66 @@ function isFieldVisible(field2, form) {
1320
1371
  function isFieldEnabled(field2, form) {
1321
1372
  return isFieldConditionMet(field2, form, ConditionalType.ENABLE);
1322
1373
  }
1374
+ var errorMessages = {
1375
+ hiddenField: {
1376
+ id: "v2.error.hidden",
1377
+ defaultMessage: "Hidden or disabled field should not receive a value",
1378
+ description: "Error message when field is hidden or disabled, but a value was received"
1379
+ },
1380
+ invalidDate: {
1381
+ defaultMessage: "Invalid date field",
1382
+ description: "Error message when date field is invalid",
1383
+ id: "v2.error.invalidDate"
1384
+ },
1385
+ invalidEmail: {
1386
+ defaultMessage: "Invalid email address",
1387
+ description: "Error message when email address is invalid",
1388
+ id: "v2.error.invalidEmail"
1389
+ },
1390
+ requiredField: {
1391
+ defaultMessage: "Required for registration",
1392
+ description: "Error message when required field is missing",
1393
+ id: "v2.error.required"
1394
+ },
1395
+ invalidInput: {
1396
+ defaultMessage: "Invalid input",
1397
+ description: "Error message when generic field is invalid",
1398
+ id: "v2.error.invalid"
1399
+ }
1400
+ };
1401
+ var createIntlError = (message) => ({
1402
+ message: {
1403
+ message
1404
+ }
1405
+ });
1323
1406
  var zodToIntlErrorMap = (issue, _ctx) => {
1324
- if (issue.code === "invalid_string" && issue.validation === "date") {
1325
- return {
1326
- message: {
1327
- message: {
1328
- defaultMessage: "Invalid date. Please use the format YYYY-MM-DD",
1329
- description: "This is the error message for invalid date fields",
1330
- id: "v2.error.invalidDate"
1331
- }
1407
+ switch (issue.code) {
1408
+ case "invalid_string": {
1409
+ if (_ctx.data === "") {
1410
+ return createIntlError(errorMessages.requiredField);
1332
1411
  }
1333
- };
1334
- }
1335
- if (issue.code === "invalid_string" && issue.validation === "email") {
1336
- return {
1337
- message: {
1338
- message: {
1339
- defaultMessage: "Invalid email address",
1340
- description: "This is the error message for invalid email fields",
1341
- id: "v2.error.invalidEmail"
1342
- }
1412
+ if (issue.validation === "date") {
1413
+ return createIntlError(errorMessages.invalidDate);
1343
1414
  }
1344
- };
1345
- }
1346
- if (issue.code === "invalid_type" && issue.expected !== issue.received && issue.received === "undefined" || issue.code === "too_small" && issue.message === void 0) {
1347
- return {
1348
- message: {
1349
- message: {
1350
- defaultMessage: "Required for registration",
1351
- description: "This is the error message for required fields",
1352
- id: "v2.error.required"
1353
- }
1415
+ if (issue.validation === "email") {
1416
+ return createIntlError(errorMessages.invalidEmail);
1354
1417
  }
1355
- };
1356
- }
1357
- return {
1358
- message: {
1359
- message: {
1360
- defaultMessage: "Invalid input",
1361
- description: "This is the error message for invalid field value",
1362
- id: "v2.error.invalid"
1418
+ break;
1419
+ }
1420
+ case "invalid_type": {
1421
+ if (issue.expected !== issue.received && issue.received === "undefined") {
1422
+ return createIntlError(errorMessages.requiredField);
1363
1423
  }
1424
+ break;
1364
1425
  }
1365
- };
1426
+ case "too_small": {
1427
+ if (issue.message === void 0) {
1428
+ return createIntlError(errorMessages.requiredField);
1429
+ }
1430
+ break;
1431
+ }
1432
+ }
1433
+ return createIntlError(errorMessages.invalidInput);
1366
1434
  };
1367
1435
  function getFieldValidationErrors({
1368
1436
  field: field2,
@@ -1377,11 +1445,7 @@ function getFieldValidationErrors({
1377
1445
  return {
1378
1446
  errors: [
1379
1447
  {
1380
- message: {
1381
- id: "v2.error.hidden",
1382
- defaultMessage: "Hidden or disabled field should not receive a value",
1383
- description: "Error message when field is hidden or disabled, but a value was received"
1384
- }
1448
+ message: errorMessages.hiddenField
1385
1449
  }
1386
1450
  ]
1387
1451
  };
@@ -1399,8 +1463,8 @@ function getFieldValidationErrors({
1399
1463
  conditionalParameters
1400
1464
  });
1401
1465
  return {
1402
- // Assumes that custom validation errors are more important than field validation errors
1403
- errors: [...customValidationResults, ...fieldValidationResult]
1466
+ // Assumes that custom validation errors are based on the field type, and extend the validation.
1467
+ errors: [...fieldValidationResult, ...customValidationResults]
1404
1468
  };
1405
1469
  }
1406
1470
  function runCustomFieldValidations({
@@ -1433,6 +1497,12 @@ function getOrThrow(x, message) {
1433
1497
  return x;
1434
1498
  }
1435
1499
 
1500
+ // ../commons/src/uuid.ts
1501
+ var import_uuid = require("uuid");
1502
+ function getUUID() {
1503
+ return (0, import_uuid.v4)();
1504
+ }
1505
+
1436
1506
  // ../commons/src/events/utils.ts
1437
1507
  function isMetadataField(field2) {
1438
1508
  return field2 in eventMetadataLabelMap;
@@ -1502,7 +1572,7 @@ function validateWorkqueueConfig(workqueueConfigs) {
1502
1572
  );
1503
1573
  if (!rootWorkqueue) {
1504
1574
  throw new Error(
1505
- `Invalid workqueue configuration: workqueue not found with id: ${workqueue.id}`
1575
+ `Invalid workqueue configuration: workqueue not found with id: ${workqueue.id}`
1506
1576
  );
1507
1577
  }
1508
1578
  });
@@ -1567,6 +1637,27 @@ function stripHiddenFields(fields, data) {
1567
1637
  return !isFieldVisible(field2, data);
1568
1638
  });
1569
1639
  }
1640
+ function findActiveDrafts(event2, drafts) {
1641
+ const actions = event2.actions.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1642
+ const lastAction = actions[actions.length - 1];
1643
+ return drafts.filter(({ createdAt }) => createdAt >= lastAction.createdAt).filter(({ eventId }) => eventId === event2.id);
1644
+ }
1645
+ function createEmptyDraft(eventId, draftId, actionType) {
1646
+ return {
1647
+ id: draftId,
1648
+ eventId,
1649
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1650
+ transactionId: getUUID(),
1651
+ action: {
1652
+ type: actionType,
1653
+ data: {},
1654
+ metadata: {},
1655
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1656
+ createdBy: "@todo",
1657
+ createdAtLocation: "@todo"
1658
+ }
1659
+ };
1660
+ }
1570
1661
 
1571
1662
  // ../commons/src/events/EventConfig.ts
1572
1663
  var EventConfig = import_zod18.z.object({
@@ -1608,7 +1699,7 @@ var EventConfig = import_zod18.z.object({
1608
1699
 
1609
1700
  // ../commons/src/events/EventConfigInput.ts
1610
1701
  var defineForm = (form) => FormConfig.parse(form);
1611
- var defineFormPage = (formPage) => FormPage.parse(formPage);
1702
+ var definePage = (formPage) => PageConfig.parse(formPage);
1612
1703
 
1613
1704
  // ../commons/src/events/Draft.ts
1614
1705
  var import_zod21 = require("zod");
@@ -2000,9 +2091,8 @@ function getCurrentEventState(event2) {
2000
2091
  });
2001
2092
  }
2002
2093
  function getCurrentEventStateWithDrafts(event2, drafts) {
2003
- const actions = event2.actions.slice().sort();
2004
- const lastAction = actions[actions.length - 1];
2005
- const activeDrafts = drafts.filter(({ eventId }) => eventId === event2.id).filter(({ createdAt }) => createdAt > lastAction.createdAt).map((draft) => draft.action).flatMap((action) => {
2094
+ const actions = event2.actions.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2095
+ const activeDrafts = findActiveDrafts(event2, drafts).map((draft) => draft.action).flatMap((action) => {
2006
2096
  if (action.type === ActionType.REQUEST_CORRECTION) {
2007
2097
  return [
2008
2098
  action,
@@ -2063,12 +2153,6 @@ var defineConfig = (config) => {
2063
2153
  });
2064
2154
  };
2065
2155
 
2066
- // ../commons/src/uuid.ts
2067
- var import_uuid = require("uuid");
2068
- function getUUID() {
2069
- return (0, import_uuid.v4)();
2070
- }
2071
-
2072
2156
  // ../commons/src/events/transactions.ts
2073
2157
  function generateTransactionId() {
2074
2158
  return getUUID();
@@ -3488,6 +3572,24 @@ var tennisClubMembershipEvent = defineConfig({
3488
3572
  })
3489
3573
  }
3490
3574
  ]
3575
+ },
3576
+ {
3577
+ type: ActionType.ARCHIVE,
3578
+ label: {
3579
+ id: "v2.event.tennis-club-membership.action.archive.label",
3580
+ defaultMessage: "Archive",
3581
+ description: "This is shown as the action name anywhere the user can trigger the action from"
3582
+ },
3583
+ forms: [TENNIS_CLUB_FORM]
3584
+ },
3585
+ {
3586
+ type: ActionType.REJECT,
3587
+ label: {
3588
+ id: "v2.event.tennis-club-membership.action.reject.label",
3589
+ defaultMessage: "Reject",
3590
+ description: "This is shown as the action name anywhere the user can trigger the action from"
3591
+ },
3592
+ forms: [TENNIS_CLUB_FORM]
3491
3593
  }
3492
3594
  ],
3493
3595
  advancedSearch: [
@@ -3570,7 +3672,7 @@ var eventPayloadGenerator = {
3570
3672
  archive: (eventId, input = {}, isDuplicate) => ({
3571
3673
  type: ActionType.ARCHIVE,
3572
3674
  transactionId: input.transactionId ?? getUUID(),
3573
- data: input.data ?? {},
3675
+ data: input.data ?? generateActionInput(tennisClubMembershipEvent, ActionType.ARCHIVE),
3574
3676
  metadata: { isDuplicate: isDuplicate ?? false },
3575
3677
  duplicates: [],
3576
3678
  eventId
@@ -3578,7 +3680,7 @@ var eventPayloadGenerator = {
3578
3680
  reject: (eventId, input = {}) => ({
3579
3681
  type: ActionType.REJECT,
3580
3682
  transactionId: input.transactionId ?? getUUID(),
3581
- data: input.data ?? {},
3683
+ data: input.data ?? generateActionInput(tennisClubMembershipEvent, ActionType.REJECT),
3582
3684
  duplicates: [],
3583
3685
  eventId
3584
3686
  }),
@@ -3637,7 +3739,9 @@ function generateActionDocument({
3637
3739
  defaults = {}
3638
3740
  }) {
3639
3741
  const actionBase = {
3640
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3742
+ // Offset is needed so the createdAt timestamps for events, actions and drafts make logical sense in storybook tests.
3743
+ // @TODO: This should be fixed in the future.
3744
+ createdAt: new Date(Date.now() - 500).toISOString(),
3641
3745
  createdBy: getUUID(),
3642
3746
  id: getUUID(),
3643
3747
  createdAtLocation: "TODO",
@@ -3692,9 +3796,13 @@ function generateEventDocument({
3692
3796
  actions: actions.map(
3693
3797
  (action) => generateActionDocument({ configuration, action })
3694
3798
  ),
3695
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3799
+ // Offset is needed so the createdAt timestamps for events, actions and drafts make logical sense in storybook tests.
3800
+ // @TODO: This should be fixed in the future.
3801
+ createdAt: new Date(Date.now() - 1e3).toISOString(),
3696
3802
  id: getUUID(),
3697
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3803
+ // Offset is needed so the createdAt timestamps for events, actions and drafts make logical sense in storybook tests.
3804
+ // @TODO: This should be fixed in the future.
3805
+ updatedAt: new Date(Date.now() - 1e3).toISOString()
3698
3806
  };
3699
3807
  }
3700
3808
  function generateEventDraftDocument(eventId, actionType = ActionType.DECLARE, data = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencrvs/toolkit",
3
- "version": "1.8.0-rc.ff73871",
3
+ "version": "1.8.0-rc.ffe8c17",
4
4
  "description": "OpenCRVS toolkit for building country configurations",
5
5
  "license": "MPL-2.0",
6
6
  "exports": {