@opencrvs/toolkit 2.0.0-rc.fef9d21 → 2.0.0-rc.ff04b30

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.
Files changed (52) hide show
  1. package/create-countryconfig/index.js +73 -0
  2. package/create-countryconfig/package.json +11 -0
  3. package/dist/application-config/index.js +68 -39
  4. package/dist/cli.js +539 -175
  5. package/dist/commons/api/router.d.ts +3334 -271
  6. package/dist/commons/application-config/index.d.ts +16 -4
  7. package/dist/commons/conditionals/conditionals.d.ts +6 -6
  8. package/dist/commons/conditionals/validate.d.ts +1 -0
  9. package/dist/commons/events/ActionInput.d.ts +108 -0
  10. package/dist/commons/events/Draft.d.ts +3 -0
  11. package/dist/commons/events/EventIndex.d.ts +0 -3
  12. package/dist/commons/events/EventMetadata.d.ts +0 -7
  13. package/dist/commons/events/FieldConfig.d.ts +10 -0
  14. package/dist/commons/events/WorkqueueConfig.d.ts +146 -146
  15. package/dist/commons/events/locations.d.ts +38 -1
  16. package/dist/commons/events/mocks.test.utils.d.ts +19 -0
  17. package/dist/commons/events/scopes.d.ts +24 -1
  18. package/dist/commons/events/state/index.d.ts +0 -4
  19. package/dist/commons/events/state/utils.d.ts +0 -2
  20. package/dist/commons/events/utils.d.ts +1 -1
  21. package/dist/conditionals/index.js +40 -8
  22. package/dist/events/index.js +184 -83
  23. package/dist/migrations/v2.0/add-birth-certificate-issuance-flag.d.ts.map +1 -1
  24. package/dist/migrations/v2.0/add-birth-certificate-issuance-flag.js +60 -35
  25. package/dist/migrations/v2.0/checkout-upstream-files.d.ts +1 -1
  26. package/dist/migrations/v2.0/checkout-upstream-files.d.ts.map +1 -1
  27. package/dist/migrations/v2.0/checkout-upstream-files.js +1 -2
  28. package/dist/migrations/v2.0/delete-infrastructure-directory.d.ts +9 -1
  29. package/dist/migrations/v2.0/delete-infrastructure-directory.d.ts.map +1 -1
  30. package/dist/migrations/v2.0/delete-infrastructure-directory.js +66 -20
  31. package/dist/migrations/v2.0/fix-accept-requested-registration-function-type.d.ts +3 -0
  32. package/dist/migrations/v2.0/fix-accept-requested-registration-function-type.d.ts.map +1 -0
  33. package/dist/migrations/v2.0/fix-accept-requested-registration-function-type.js +110 -0
  34. package/dist/migrations/v2.0/index.d.ts +5 -3
  35. package/dist/migrations/v2.0/index.d.ts.map +1 -1
  36. package/dist/migrations/v2.0/index.js +537 -173
  37. package/dist/migrations/v2.0/make-built-in-validate-actions-custom.d.ts.map +1 -1
  38. package/dist/migrations/v2.0/make-built-in-validate-actions-custom.js +90 -1
  39. package/dist/migrations/v2.0/merge-infrastructure-directory.d.ts +11 -1
  40. package/dist/migrations/v2.0/merge-infrastructure-directory.d.ts.map +1 -1
  41. package/dist/migrations/v2.0/merge-infrastructure-directory.js +17 -16
  42. package/dist/migrations/v2.0/migrate-scopes.d.ts.map +1 -1
  43. package/dist/migrations/v2.0/migrate-scopes.js +54 -30
  44. package/dist/migrations/v2.0/migrate-workqueue-configs.d.ts.map +1 -1
  45. package/dist/migrations/v2.0/migrate-workqueue-configs.js +140 -59
  46. package/dist/notification/index.js +78 -53
  47. package/dist/scopes/index.d.ts +87 -10
  48. package/dist/scopes/index.js +46 -9
  49. package/opencrvs-toolkit-2.0.0-rc.ff04b30.tgz +0 -0
  50. package/package.json +1 -1
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/opencrvs-toolkit-2.0.0-rc.fef9d21.tgz +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"make-built-in-validate-actions-custom.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/make-built-in-validate-actions-custom.ts"],"names":[],"mappings":"AA2WA,iBAAe,IAAI,kBAiDlB;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
1
+ {"version":3,"file":"make-built-in-validate-actions-custom.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/make-built-in-validate-actions-custom.ts"],"names":[],"mappings":"AAodA,iBAAe,IAAI,kBAiDlB;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
@@ -41,6 +41,10 @@ exports.main = main;
41
41
  * - Removes the `deduplication` property from those objects
42
42
  * - Replaces `review: { fields: <value> }` with `form: <value>`, or
43
43
  * `review: <expr>` with `form: <expr>.fields` when the value is not an inline object
44
+ * - Adds the top-level `validated` flag definition and `{ id: 'validated',
45
+ * operation: 'add' }` to the (now custom) validate action
46
+ * - Adds `{ id: 'validated', operation: 'remove' }` to every `REGISTER` action
47
+ * in the same config, closing the flag's lifecycle when registration happens
44
48
  * - Saves the modified files in-place
45
49
  */
46
50
  const ts_morph_1 = require("ts-morph");
@@ -74,6 +78,8 @@ function ensureImports(sourceFile) {
74
78
  const DEFINE_CONFIG_NAME = 'defineConfig';
75
79
  const ACTIONS_PROPERTY_NAME = 'actions';
76
80
  const TYPE_PROPERTY_NAME = 'type';
81
+ const ID_PROPERTY_NAME = 'id';
82
+ const FLAGS_PROPERTY_NAME = 'flags';
77
83
  const CUSTOM_ACTION_TYPE_PROPERTY_NAME = 'customActionType';
78
84
  const AUDIT_HISTORY_LABEL_PROPERTY_NAME = 'auditHistoryLabel';
79
85
  const DEDUPLICATION_PROPERTY_NAME = 'deduplication';
@@ -83,9 +89,12 @@ const FORM_PROPERTY_NAME = 'form';
83
89
  const ACTION_TYPE_ENUM_NAME = 'ActionType';
84
90
  const VALIDATE_MEMBER_NAME = 'VALIDATE';
85
91
  const CUSTOM_MEMBER_NAME = 'CUSTOM';
92
+ const REGISTER_MEMBER_NAME = 'REGISTER';
86
93
  const VALIDATE_STRING_LITERAL = 'VALIDATE';
87
94
  const CUSTOM_STRING_LITERAL = 'CUSTOM';
95
+ const REGISTER_STRING_LITERAL = 'REGISTER';
88
96
  const VALIDATE_DECLARATION_VALUE = 'VALIDATE_DECLARATION';
97
+ const VALIDATED_FLAG_VALUE = 'validated';
89
98
  function isValidateType(typeInitializer) {
90
99
  // Matches: ActionType.VALIDATE
91
100
  if (typeInitializer.getKind() === ts_morph_1.SyntaxKind.PropertyAccessExpression) {
@@ -230,6 +239,72 @@ function transformValidateActionToCustom(obj, eventType) {
230
239
  }
231
240
  return true;
232
241
  }
242
+ function isRegisterAction(action) {
243
+ const typeProperty = action.getProperty(TYPE_PROPERTY_NAME);
244
+ if (!typeProperty || !ts_morph_1.Node.isPropertyAssignment(typeProperty))
245
+ return false;
246
+ const typeInit = typeProperty.getInitializer();
247
+ if (!typeInit)
248
+ return false;
249
+ // Matches: ActionType.REGISTER
250
+ if (ts_morph_1.Node.isPropertyAccessExpression(typeInit)) {
251
+ const enumExpr = typeInit.getExpression();
252
+ return (ts_morph_1.Node.isIdentifier(enumExpr) &&
253
+ enumExpr.getText() === ACTION_TYPE_ENUM_NAME &&
254
+ typeInit.getName() === REGISTER_MEMBER_NAME);
255
+ }
256
+ // Matches: "REGISTER"
257
+ if (ts_morph_1.Node.isStringLiteral(typeInit)) {
258
+ return typeInit.getLiteralValue() === REGISTER_STRING_LITERAL;
259
+ }
260
+ return false;
261
+ }
262
+ /**
263
+ * Returns true if an array literal contains an object literal with `id: <id>`.
264
+ */
265
+ function arrayHasElementWithId(array, id) {
266
+ for (const el of array.getElements()) {
267
+ if (!ts_morph_1.Node.isObjectLiteralExpression(el))
268
+ continue;
269
+ const idProp = el.getProperty(ID_PROPERTY_NAME);
270
+ if (!idProp || !ts_morph_1.Node.isPropertyAssignment(idProp))
271
+ continue;
272
+ const idInit = idProp.getInitializer();
273
+ if (idInit &&
274
+ ts_morph_1.Node.isStringLiteral(idInit) &&
275
+ idInit.getLiteralValue() === id) {
276
+ return true;
277
+ }
278
+ }
279
+ return false;
280
+ }
281
+ /**
282
+ * Adds `{ id: 'validated', operation: 'remove' }` to a REGISTER action's
283
+ * `flags` array. Skips if any `validated` flag entry is already present —
284
+ * this keeps the codemod idempotent and avoids stomping on an intentional
285
+ * `add` op a maintainer may have authored manually.
286
+ *
287
+ * Returns true if a change was made.
288
+ */
289
+ function ensureRegisterRemovesValidatedFlag(action) {
290
+ const entry = `{ id: '${VALIDATED_FLAG_VALUE}', operation: 'remove' }`;
291
+ const flagsProp = action.getProperty(FLAGS_PROPERTY_NAME);
292
+ if (flagsProp && ts_morph_1.Node.isPropertyAssignment(flagsProp)) {
293
+ const flagsInit = flagsProp.getInitializer();
294
+ if (flagsInit && ts_morph_1.Node.isArrayLiteralExpression(flagsInit)) {
295
+ if (arrayHasElementWithId(flagsInit, VALIDATED_FLAG_VALUE))
296
+ return false;
297
+ flagsInit.addElement(entry);
298
+ return true;
299
+ }
300
+ return false;
301
+ }
302
+ action.addPropertyAssignment({
303
+ name: FLAGS_PROPERTY_NAME,
304
+ initializer: `[${entry}]`
305
+ });
306
+ return true;
307
+ }
233
308
  function processFile(filePath, project) {
234
309
  const sourceFile = project.getSourceFile(filePath);
235
310
  if (!sourceFile)
@@ -297,6 +372,20 @@ function processFile(filePath, project) {
297
372
  });
298
373
  console.log(` [${path_1.default.relative(process.cwd(), filePath)}] Added top-level 'flags' array to defineConfig`);
299
374
  }
375
+ // Close the `validated` flag's lifecycle: when the validate action adds the
376
+ // flag, the matching REGISTER action(s) must remove it on registration.
377
+ if (configHadValidateAction) {
378
+ for (const element of actionsInitializer.getElements()) {
379
+ if (!ts_morph_1.Node.isObjectLiteralExpression(element))
380
+ continue;
381
+ if (!isRegisterAction(element))
382
+ continue;
383
+ if (ensureRegisterRemovesValidatedFlag(element)) {
384
+ transformedCount++;
385
+ console.log(` [${path_1.default.relative(process.cwd(), filePath)}] Added '${VALIDATED_FLAG_VALUE}' remove-flag to REGISTER action`);
386
+ }
387
+ }
388
+ }
300
389
  if (fileNeedsImports) {
301
390
  ensureImports(sourceFile);
302
391
  }
@@ -338,6 +427,6 @@ function main() {
338
427
  yield sourceFile.save();
339
428
  console.log(` Saved: ${path_1.default.relative(process.cwd(), filePath)}`);
340
429
  }
341
- console.log(`\nDone. Transformed ${totalTransformed} VALIDATE action(s) to CUSTOM with customActionType: '${VALIDATE_DECLARATION_VALUE}'.`);
430
+ console.log(`\nDone. Applied ${totalTransformed} change(s): VALIDATE CUSTOM (${VALIDATE_DECLARATION_VALUE}) and '${VALIDATED_FLAG_VALUE}' remove-flag on matching REGISTER actions.`);
342
431
  });
343
432
  }
@@ -1,3 +1,13 @@
1
- declare function main(): Promise<void>;
1
+ interface MergeInfrastructureOptions {
2
+ /**
3
+ * Top-level subdirectories of `infrastructure/` to scope the merge to.
4
+ * When omitted or empty, the entire `infrastructure/` tree is merged
5
+ * (legacy behavior). Each name is relative to `infrastructure/` —
6
+ * e.g. `['postgres', 'metabase']` merges only
7
+ * `infrastructure/postgres/` and `infrastructure/metabase/`.
8
+ */
9
+ subdirs?: string[];
10
+ }
11
+ declare function main({ subdirs }?: MergeInfrastructureOptions): Promise<void>;
2
12
  export { main };
3
13
  //# sourceMappingURL=merge-infrastructure-directory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-infrastructure-directory.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/merge-infrastructure-directory.ts"],"names":[],"mappings":"AAsHA,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAkInC;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
1
+ {"version":3,"file":"merge-infrastructure-directory.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/merge-infrastructure-directory.ts"],"names":[],"mappings":"AAsHA,UAAU,0BAA0B;IAClC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAID,iBAAe,IAAI,CAAC,EAClB,OAAY,EACb,GAAE,0BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC,CAoIjD;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
@@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.main = main;
23
23
  /**
24
24
  * Codemod: Merge upstream changes into the local `infrastructure/` directory
25
- * from opencrvs/opencrvs-countryconfig.
25
+ * (or selected subdirectories of it) from opencrvs/opencrvs-countryconfig.
26
26
  *
27
27
  * Usage:
28
28
  * ts-node -r tsconfig-paths/register src/migrations/v2.0/merge-infrastructure-directory.ts
@@ -33,8 +33,10 @@ exports.main = main;
33
33
  * - the upstream "theirs" branch (v2.0) → `UPSTREAM_BRANCH`
34
34
  * - the synthetic merge-base branch (latest 1.9 release line)
35
35
  * → `BASE_BRANCH`
36
- * 2. For every file under `infrastructure/` on upstream "theirs", performs
37
- * a file-scoped 3-way merge:
36
+ * 2. For every file under `infrastructure/` on upstream "theirs" (or, when
37
+ * `subdirs` is provided, only files under
38
+ * `infrastructure/<subdir>/` for each requested subdir), performs a
39
+ * file-scoped 3-way merge:
38
40
  * - new file on upstream → checked out and staged
39
41
  * - existing file → 3-way merged in place via `git merge-file`,
40
42
  * using the BASE_BRANCH version of the file
@@ -116,8 +118,14 @@ function gitShowBytes(ref, path) {
116
118
  }
117
119
  // ─── Entry point ─────────────────────────────────────────────────────────────
118
120
  function main() {
119
- return __awaiter(this, void 0, void 0, function* () {
120
- console.log(`Merging '${INFRASTRUCTURE_DIR}/' from ${checkout_upstream_files_1.UPSTREAM_URL}@${checkout_upstream_files_1.UPSTREAM_BRANCH} (base: ${BASE_BRANCH})...\n`);
121
+ return __awaiter(this, arguments, void 0, function* ({ subdirs = [] } = {}) {
122
+ // Build the pathspec list passed to `git ls-tree`. Trailing slashes make
123
+ // git treat them as tree paths (recurse into them).
124
+ const pathspecs = subdirs.length > 0
125
+ ? subdirs.map((s) => `${INFRASTRUCTURE_DIR}/${s}/`)
126
+ : [`${INFRASTRUCTURE_DIR}/`];
127
+ const scopeLabel = pathspecs.map((p) => `'${p}'`).join(', ');
128
+ console.log(`Merging ${scopeLabel} from ${checkout_upstream_files_1.UPSTREAM_URL}@${checkout_upstream_files_1.UPSTREAM_BRANCH} (base: ${BASE_BRANCH})...\n`);
121
129
  (0, checkout_upstream_files_1.assertIsGitRepo)();
122
130
  // Best-effort cleanup of a stale temp remote from a previous aborted run.
123
131
  (0, checkout_upstream_files_1.tryGit)(['remote', 'remove', TEMP_REMOTE]);
@@ -131,20 +139,13 @@ function main() {
131
139
  const ref = `${TEMP_REMOTE}/${checkout_upstream_files_1.UPSTREAM_BRANCH}`;
132
140
  const baseRef = `${TEMP_REMOTE}/${BASE_BRANCH}`;
133
141
  // -z gives NUL-separated paths so filenames with spaces or newlines
134
- // survive the round-trip.
135
- const upstreamFiles = (0, checkout_upstream_files_1.runGit)([
136
- 'ls-tree',
137
- '-r',
138
- '--name-only',
139
- '-z',
140
- ref,
141
- '--',
142
- `${INFRASTRUCTURE_DIR}/`
143
- ], { silent: true })
142
+ // survive the round-trip. Multiple pathspecs after `--` are unioned
143
+ // by git, which scopes the listing to the requested subdirectories.
144
+ const upstreamFiles = (0, checkout_upstream_files_1.runGit)(['ls-tree', '-r', '--name-only', '-z', ref, '--', ...pathspecs], { silent: true })
144
145
  .split('\0')
145
146
  .filter(Boolean);
146
147
  if (upstreamFiles.length === 0) {
147
- console.log(` No files found under '${INFRASTRUCTURE_DIR}/' on ${ref}. Nothing to merge.`);
148
+ console.log(` No files found under ${scopeLabel} on ${ref}. Nothing to merge.`);
148
149
  return;
149
150
  }
150
151
  const tmpDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'opencrvs-merge-infra-'));
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-scopes.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/migrate-scopes.ts"],"names":[],"mappings":"AAyVA,iBAAe,IAAI,kBAmDlB;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
1
+ {"version":3,"file":"migrate-scopes.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/migrate-scopes.ts"],"names":[],"mappings":"AAoXA,iBAAe,IAAI,kBAmDlB;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
@@ -25,7 +25,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.main = main;
26
26
  const ts_morph_1 = require("ts-morph");
27
27
  const path_1 = __importDefault(require("path"));
28
- const scopes_1 = require("../commons/scopes");
29
28
  const scopes_deprecated_do_not_use_1 = require("../commons/scopes.deprecated.do-not-use");
30
29
  const ROLES_FILE_RELATIVE_PATH = 'src/data-seeding/roles/roles.ts';
31
30
  const SCOPES_PROPERTY_NAME = 'scopes';
@@ -65,60 +64,85 @@ function resolveLegacyScopeFromElement(element) {
65
64
  }
66
65
  return null;
67
66
  }
67
+ /**
68
+ * Walks each `scopes: [...]` array in the roles file, classifies elements,
69
+ * and rewrites the whole array using `migrateLegacyScopesArrayToV2Scopes`
70
+ * (which preserves v1.9 AND semantics by merging pairs of legacy scopes
71
+ * that targeted the same v2 type). See issue #12489.
72
+ *
73
+ * Elements are partitioned into three kinds:
74
+ * - drop: `SCOPES.X` references where X is not in the SCOPES map (warned
75
+ * and removed, matching pre-refactor behavior).
76
+ * - raw: non-literal expressions we cannot statically resolve (e.g.
77
+ * `...sharedScopes` spreads). Preserved verbatim in output so authoring
78
+ * intent isn't lost.
79
+ * - legacy: literal strings or resolvable `SCOPES.X` references.
80
+ * Collected and passed as a batch to the merge function.
81
+ *
82
+ * The output array text is `[...rawTexts, ...mergedScopesAsJson]`. Order of
83
+ * legacy entries relative to raw expressions is not preserved — scopes in a
84
+ * v2 array compose with OR, so position is semantically irrelevant.
85
+ */
68
86
  function migrateScopesArrays(rolesFilePath, project) {
69
87
  const sourceFile = project.getSourceFile(rolesFilePath);
70
- if (!sourceFile)
71
- return { migratedCount: 0, removedCount: 0 };
88
+ if (!sourceFile) {
89
+ return { migratedCount: 0, removedCount: 0, mergedCount: 0 };
90
+ }
72
91
  let migratedCount = 0;
73
92
  let removedCount = 0;
93
+ let mergedCount = 0;
74
94
  const scopesProperties = sourceFile
75
95
  .getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)
76
96
  .filter((prop) => prop.getName() === SCOPES_PROPERTY_NAME);
97
+ const relPath = path_1.default.relative(process.cwd(), rolesFilePath);
77
98
  for (const scopesProp of scopesProperties) {
78
99
  const initializer = scopesProp.getInitializer();
79
100
  if (!initializer || !ts_morph_1.Node.isArrayLiteralExpression(initializer))
80
101
  continue;
81
- const elements = initializer.getElements();
82
- for (let index = elements.length - 1; index >= 0; index--) {
83
- const element = elements[index];
84
- if (!ts_morph_1.Node.isExpression(element))
85
- continue;
102
+ const rawTexts = [];
103
+ const legacyStrings = [];
104
+ let anyChange = false;
105
+ for (const element of initializer.getElements()) {
86
106
  const scopeKey = resolveScopeKeyFromElementAccess(element);
87
107
  if (scopeKey && !(scopeKey in scopes_deprecated_do_not_use_1.SCOPES)) {
88
108
  // eslint-disable-next-line no-console
89
- console.warn(` [${path_1.default.relative(process.cwd(), rolesFilePath)}] Removing unknown scope reference: SCOPES.${scopeKey}`);
90
- initializer.removeElement(index);
109
+ console.warn(` [${relPath}] Removing unknown scope reference: SCOPES.${scopeKey}`);
91
110
  removedCount++;
111
+ anyChange = true;
92
112
  continue;
93
113
  }
94
114
  const legacyScope = resolveLegacyScopeFromElement(element);
95
115
  if (!legacyScope) {
96
116
  // eslint-disable-next-line no-console
97
- console.warn(` [${path_1.default.relative(process.cwd(), rolesFilePath)}] Skipping non-literal scope expression: ${element.getText()}`);
117
+ console.warn(` [${relPath}] Preserving non-literal scope expression verbatim: ${element.getText()}`);
118
+ rawTexts.push(element.getText());
98
119
  continue;
99
120
  }
100
- try {
101
- const migratedScope = (0, scopes_deprecated_do_not_use_1.legacyScopeToV2Scope)(legacyScope);
102
- const scope = (0, scopes_1.decodeScope)(migratedScope);
103
- if (!scope) {
104
- // eslint-disable-next-line no-console
105
- console.warn(` [${path_1.default.relative(process.cwd(), rolesFilePath)}] Could not decode migrated scope '${migratedScope}'.`);
106
- continue;
107
- }
108
- element.replaceWithText(JSON.stringify(scope));
109
- migratedCount++;
110
- }
111
- catch (error) {
112
- // eslint-disable-next-line no-console
113
- console.warn(` [${path_1.default.relative(process.cwd(), rolesFilePath)}] Could not migrate scope '${legacyScope}': ${error}`);
114
- }
121
+ legacyStrings.push(legacyScope);
122
+ anyChange = true;
123
+ }
124
+ if (!anyChange)
125
+ continue;
126
+ let mergedScopes;
127
+ try {
128
+ mergedScopes = (0, scopes_deprecated_do_not_use_1.migrateLegacyScopesArrayToV2Scopes)(legacyStrings);
129
+ }
130
+ catch (error) {
131
+ throw new Error(`[${relPath}] Failed to migrate scopes for role: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
115
132
  }
133
+ migratedCount += legacyStrings.length;
134
+ mergedCount += legacyStrings.length - mergedScopes.length;
135
+ const outputElements = [
136
+ ...rawTexts,
137
+ ...mergedScopes.map((s) => JSON.stringify(s))
138
+ ];
139
+ scopesProp.setInitializer(`[${outputElements.join(', ')}]`);
116
140
  }
117
141
  if (removedCount > 0) {
118
142
  // eslint-disable-next-line no-console
119
- console.log(` Removed ${removedCount} unknown SCOPES reference(s) from '${path_1.default.relative(process.cwd(), rolesFilePath)}'.`);
143
+ console.log(` Removed ${removedCount} unknown SCOPES reference(s) from '${relPath}'.`);
120
144
  }
121
- return { migratedCount, removedCount };
145
+ return { migratedCount, removedCount, mergedCount };
122
146
  }
123
147
  function ensureScopesHelpersImport(rolesFilePath, project) {
124
148
  const sourceFile = project.getSourceFile(rolesFilePath);
@@ -262,7 +286,7 @@ function main() {
262
286
  return;
263
287
  }
264
288
  const importChanged = ensureScopesHelpersImport(rolesFilePath, project);
265
- const { migratedCount, removedCount } = migrateScopesArrays(rolesFilePath, project);
289
+ const { migratedCount, removedCount, mergedCount } = migrateScopesArrays(rolesFilePath, project);
266
290
  const wrappedScopesCount = wrapScopesArraysWithDefineScopes(rolesFilePath, project);
267
291
  const wrappedRoles = wrapRolesWithDefineRoles(rolesFilePath, project);
268
292
  const removedRoleType = removeRoleTypeDefinitionAndUnusedImports(rolesFilePath, project);
@@ -276,6 +300,6 @@ function main() {
276
300
  return;
277
301
  }
278
302
  yield sourceFile.save();
279
- console.log(`Done. Migrated ${migratedCount} legacy scope(s), removed ${removedCount} unknown SCOPES reference(s), wrapped ${wrappedScopesCount} scope array(s) with defineScopes()${wrappedRoles ? ', wrapped roles with defineRoles()' : ''}${removedRoleType ? ', and removed Role type definition' : ''}${importChanged ? '. Updated toolkit/scopes imports.' : '.'}`);
303
+ console.log(`Done. Migrated ${migratedCount} legacy scope(s), merged ${mergedCount} AND-paired scope(s) into single v2 objects (privilege-escalation fix, see issue #12489), removed ${removedCount} unknown SCOPES reference(s), wrapped ${wrappedScopesCount} scope array(s) with defineScopes()${wrappedRoles ? ', wrapped roles with defineRoles()' : ''}${removedRoleType ? ', and removed Role type definition' : ''}${importChanged ? '. Updated toolkit/scopes imports.' : '.'}`);
280
304
  });
281
305
  }
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-workqueue-configs.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/migrate-workqueue-configs.ts"],"names":[],"mappings":"AA+OA,iBAAe,IAAI,kBAqDlB;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
1
+ {"version":3,"file":"migrate-workqueue-configs.d.ts","sourceRoot":"","sources":["../../../src/migrations/v2.0/migrate-workqueue-configs.ts"],"names":[],"mappings":"AAgVA,iBAAe,IAAI,kBAqDlB;AAED,OAAO,EAAE,IAAI,EAAE,CAAA"}
@@ -39,6 +39,10 @@ exports.main = main;
39
39
  * - `actions: [{...}, {...}, ...]` -> keeps first as `action`, warns about dropped extras
40
40
  * - If `action` already exists, removes deprecated `actions` and keeps `action`
41
41
  * - Removes unsupported `action.type` values (`DEFAULT`, `VALIDATE`, etc.)
42
+ * - Ensures every workqueue ends up with a valid `action`. When none is
43
+ * present (or the existing one is unsupported), falls back to
44
+ * `action: { type: ActionType.READ }` and ensures `ActionType` is imported
45
+ * from `@opencrvs/toolkit/events` in the file.
42
46
  * - Saves modified files in-place
43
47
  */
44
48
  const ts_morph_1 = require("ts-morph");
@@ -49,6 +53,10 @@ const ACTION_PROPERTY_NAME = 'action';
49
53
  const CONDITIONALS_PROPERTY_NAME = 'conditionals';
50
54
  const TYPE_PROPERTY_NAME = 'type';
51
55
  const SLUG_PROPERTY_NAME = 'slug';
56
+ const READ_ACTION_TYPE = 'READ';
57
+ const ACTION_TYPE_IMPORT_NAME = 'ActionType';
58
+ const TOOLKIT_EVENTS_MODULE = '@opencrvs/toolkit/events';
59
+ const READ_ACTION_FALLBACK_INITIALIZER = `{ type: ${ACTION_TYPE_IMPORT_NAME}.${READ_ACTION_TYPE} }`;
52
60
  const SUPPORTED_WORKQUEUE_ACTION_TYPES = new Set([
53
61
  'READ',
54
62
  'DELETE',
@@ -96,80 +104,146 @@ function isUnsupportedActionType(action) {
96
104
  }
97
105
  return !SUPPORTED_WORKQUEUE_ACTION_TYPES.has(typeName);
98
106
  }
99
- function migrateWorkqueueActions(workqueue, relPath) {
100
- const actionsProperty = workqueue.getProperty(ACTIONS_PROPERTY_NAME);
101
- if (!actionsProperty || !ts_morph_1.Node.isPropertyAssignment(actionsProperty)) {
102
- return 0;
103
- }
104
- const actionsInitializer = actionsProperty.getInitializer();
105
- if (!actionsInitializer || !ts_morph_1.Node.isArrayLiteralExpression(actionsInitializer)) {
106
- return 0;
107
+ /**
108
+ * Ensures the workqueue has a valid singular `action` property. When the
109
+ * existing `action` is missing or has an unsupported type, replaces it with
110
+ * `action: { type: ActionType.READ }`.
111
+ *
112
+ * Returns whether anything was changed and whether the READ fallback was
113
+ * applied (so the caller can ensure `ActionType` is imported in the file).
114
+ */
115
+ function ensureValidAction(workqueue, relPath) {
116
+ const workqueueLabel = getWorkqueueLabel(workqueue);
117
+ const actionProperty = workqueue.getProperty(ACTION_PROPERTY_NAME);
118
+ if (actionProperty && ts_morph_1.Node.isPropertyAssignment(actionProperty)) {
119
+ const initializer = actionProperty.getInitializer();
120
+ if (initializer && ts_morph_1.Node.isObjectLiteralExpression(initializer)) {
121
+ if (!isUnsupportedActionType(initializer)) {
122
+ return { changed: false, usedReadFallback: false };
123
+ }
124
+ actionProperty.set({
125
+ name: ACTION_PROPERTY_NAME,
126
+ initializer: READ_ACTION_FALLBACK_INITIALIZER
127
+ });
128
+ // eslint-disable-next-line no-console
129
+ console.log(` [${relPath}] Replaced unsupported 'action' type with '${ACTION_TYPE_IMPORT_NAME}.${READ_ACTION_TYPE}' on workqueue '${workqueueLabel}'`);
130
+ return { changed: true, usedReadFallback: true };
131
+ }
107
132
  }
133
+ workqueue.addPropertyAssignment({
134
+ name: ACTION_PROPERTY_NAME,
135
+ initializer: READ_ACTION_FALLBACK_INITIALIZER
136
+ });
137
+ // eslint-disable-next-line no-console
138
+ console.log(` [${relPath}] Added default 'action: ${READ_ACTION_FALLBACK_INITIALIZER}' to workqueue '${workqueueLabel}'`);
139
+ return { changed: true, usedReadFallback: true };
140
+ }
141
+ function migrateWorkqueueActions(workqueue, relPath) {
108
142
  const workqueueLabel = getWorkqueueLabel(workqueue);
109
- const existingActionProperty = workqueue.getProperty(ACTION_PROPERTY_NAME);
110
- if (existingActionProperty) {
111
- if (ts_morph_1.Node.isPropertyAssignment(existingActionProperty)) {
112
- const existingActionInitializer = existingActionProperty.getInitializer();
113
- if (existingActionInitializer && ts_morph_1.Node.isObjectLiteralExpression(existingActionInitializer)) {
114
- const conditionalsProperty = existingActionInitializer.getProperty(CONDITIONALS_PROPERTY_NAME);
115
- if (conditionalsProperty) {
116
- conditionalsProperty.remove();
117
- // eslint-disable-next-line no-console
118
- console.log(` [${relPath}] Removed deprecated 'conditionals' from 'action' on workqueue '${workqueueLabel}'`);
143
+ const actionsProperty = workqueue.getProperty(ACTIONS_PROPERTY_NAME);
144
+ let actionsArrayMigrated = false;
145
+ if (actionsProperty && ts_morph_1.Node.isPropertyAssignment(actionsProperty)) {
146
+ const actionsInitializer = actionsProperty.getInitializer();
147
+ if (actionsInitializer &&
148
+ ts_morph_1.Node.isArrayLiteralExpression(actionsInitializer)) {
149
+ const existingActionProperty = workqueue.getProperty(ACTION_PROPERTY_NAME);
150
+ if (existingActionProperty) {
151
+ if (ts_morph_1.Node.isPropertyAssignment(existingActionProperty)) {
152
+ const existingActionInitializer = existingActionProperty.getInitializer();
153
+ if (existingActionInitializer &&
154
+ ts_morph_1.Node.isObjectLiteralExpression(existingActionInitializer)) {
155
+ const conditionalsProperty = existingActionInitializer.getProperty(CONDITIONALS_PROPERTY_NAME);
156
+ if (conditionalsProperty) {
157
+ conditionalsProperty.remove();
158
+ // eslint-disable-next-line no-console
159
+ console.log(` [${relPath}] Removed deprecated 'conditionals' from 'action' on workqueue '${workqueueLabel}'`);
160
+ }
161
+ }
119
162
  }
120
- if (isUnsupportedActionType(existingActionInitializer)) {
121
- existingActionProperty.remove();
163
+ actionsProperty.remove();
164
+ // eslint-disable-next-line no-console
165
+ console.log(` [${relPath}] Removed deprecated 'actions' from workqueue '${workqueueLabel}' (existing 'action' kept)`);
166
+ actionsArrayMigrated = true;
167
+ }
168
+ else {
169
+ const actionCandidates = actionsInitializer
170
+ .getElements()
171
+ .filter((element) => ts_morph_1.Node.isObjectLiteralExpression(element));
172
+ if (actionCandidates.length === 0) {
173
+ actionsProperty.remove();
122
174
  // eslint-disable-next-line no-console
123
- console.log(` [${relPath}] Removed unsupported 'action' type on workqueue '${workqueueLabel}'`);
175
+ console.log(` [${relPath}] Removed empty 'actions' from workqueue '${workqueueLabel}'`);
176
+ actionsArrayMigrated = true;
177
+ }
178
+ else {
179
+ if (actionCandidates.length > 1) {
180
+ const droppedCount = actionCandidates.length - 1;
181
+ // eslint-disable-next-line no-console
182
+ console.warn(` [${relPath}] Workqueue '${workqueueLabel}' has ${actionCandidates.length} actions; keeping first as 'action' and dropping ${droppedCount} extra action(s)`);
183
+ }
184
+ const conditionalsProperty = actionCandidates[0].getProperty(CONDITIONALS_PROPERTY_NAME);
185
+ if (conditionalsProperty) {
186
+ conditionalsProperty.remove();
187
+ // eslint-disable-next-line no-console
188
+ console.log(` [${relPath}] Removed deprecated 'conditionals' from migrated 'action' on workqueue '${workqueueLabel}'`);
189
+ }
190
+ if (isUnsupportedActionType(actionCandidates[0])) {
191
+ actionsProperty.remove();
192
+ // eslint-disable-next-line no-console
193
+ console.log(` [${relPath}] Removed deprecated 'actions' from workqueue '${workqueueLabel}' because action type is unsupported`);
194
+ actionsArrayMigrated = true;
195
+ }
196
+ else {
197
+ const firstActionText = actionCandidates[0].getText();
198
+ workqueue.addPropertyAssignment({
199
+ name: ACTION_PROPERTY_NAME,
200
+ initializer: firstActionText
201
+ });
202
+ actionsProperty.remove();
203
+ // eslint-disable-next-line no-console
204
+ console.log(` [${relPath}] Replaced 'actions' with 'action' on workqueue '${workqueueLabel}'`);
205
+ actionsArrayMigrated = true;
206
+ }
124
207
  }
125
208
  }
126
209
  }
127
- actionsProperty.remove();
128
- // eslint-disable-next-line no-console
129
- console.log(` [${relPath}] Removed deprecated 'actions' from workqueue '${workqueueLabel}' (existing 'action' kept)`);
130
- return 1;
131
- }
132
- const actionCandidates = actionsInitializer
133
- .getElements()
134
- .filter((element) => ts_morph_1.Node.isObjectLiteralExpression(element));
135
- if (actionCandidates.length === 0) {
136
- actionsProperty.remove();
137
- // eslint-disable-next-line no-console
138
- console.log(` [${relPath}] Removed empty 'actions' from workqueue '${workqueueLabel}'`);
139
- return 1;
140
- }
141
- if (actionCandidates.length > 1) {
142
- const droppedCount = actionCandidates.length - 1;
143
- // eslint-disable-next-line no-console
144
- console.warn(` [${relPath}] Workqueue '${workqueueLabel}' has ${actionCandidates.length} actions; keeping first as 'action' and dropping ${droppedCount} extra action(s)`);
145
- }
146
- const conditionalsProperty = actionCandidates[0].getProperty(CONDITIONALS_PROPERTY_NAME);
147
- if (conditionalsProperty) {
148
- conditionalsProperty.remove();
149
- // eslint-disable-next-line no-console
150
- console.log(` [${relPath}] Removed deprecated 'conditionals' from migrated 'action' on workqueue '${workqueueLabel}'`);
151
210
  }
152
- if (isUnsupportedActionType(actionCandidates[0])) {
153
- actionsProperty.remove();
154
- // eslint-disable-next-line no-console
155
- console.log(` [${relPath}] Removed deprecated 'actions' from workqueue '${workqueueLabel}' because action type is unsupported`);
156
- return 1;
211
+ // Always ensure the workqueue ends up with a valid `action`. This both
212
+ // covers workqueues that never had an `actions` array, and patches up
213
+ // workqueues whose existing/migrated action type is unsupported.
214
+ const fallback = ensureValidAction(workqueue, relPath);
215
+ return {
216
+ changed: actionsArrayMigrated || fallback.changed,
217
+ usedReadFallback: fallback.usedReadFallback
218
+ };
219
+ }
220
+ /**
221
+ * Ensures `import { ActionType } from '@opencrvs/toolkit/events'` exists in
222
+ * the source file, merging into any existing import declaration from the
223
+ * same module specifier.
224
+ */
225
+ function ensureActionTypeImport(sourceFile) {
226
+ const existingImport = sourceFile.getImportDeclaration((decl) => decl.getModuleSpecifierValue() === TOOLKIT_EVENTS_MODULE);
227
+ if (existingImport) {
228
+ const alreadyImported = existingImport
229
+ .getNamedImports()
230
+ .some((ni) => ni.getName() === ACTION_TYPE_IMPORT_NAME);
231
+ if (!alreadyImported) {
232
+ existingImport.addNamedImport(ACTION_TYPE_IMPORT_NAME);
233
+ }
234
+ return;
157
235
  }
158
- const firstActionText = actionCandidates[0].getText();
159
- workqueue.addPropertyAssignment({
160
- name: ACTION_PROPERTY_NAME,
161
- initializer: firstActionText
236
+ sourceFile.addImportDeclaration({
237
+ moduleSpecifier: TOOLKIT_EVENTS_MODULE,
238
+ namedImports: [ACTION_TYPE_IMPORT_NAME]
162
239
  });
163
- actionsProperty.remove();
164
- // eslint-disable-next-line no-console
165
- console.log(` [${relPath}] Replaced 'actions' with 'action' on workqueue '${workqueueLabel}'`);
166
- return 1;
167
240
  }
168
241
  function processFile(filePath, project) {
169
242
  const sourceFile = project.getSourceFile(filePath);
170
243
  if (!sourceFile)
171
244
  return 0;
172
245
  let migratedWorkqueues = 0;
246
+ let needsActionTypeImport = false;
173
247
  const relPath = path_1.default.relative(process.cwd(), filePath);
174
248
  const callExpressions = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
175
249
  for (const call of callExpressions) {
@@ -187,9 +261,16 @@ function processFile(filePath, project) {
187
261
  for (const element of workqueuesArg.getElements()) {
188
262
  if (!ts_morph_1.Node.isObjectLiteralExpression(element))
189
263
  continue;
190
- migratedWorkqueues += migrateWorkqueueActions(element, relPath);
264
+ const result = migrateWorkqueueActions(element, relPath);
265
+ if (result.changed)
266
+ migratedWorkqueues++;
267
+ if (result.usedReadFallback)
268
+ needsActionTypeImport = true;
191
269
  }
192
270
  }
271
+ if (needsActionTypeImport) {
272
+ ensureActionTypeImport(sourceFile);
273
+ }
193
274
  return migratedWorkqueues;
194
275
  }
195
276
  function main() {