@cosmicdrift/kumiko-framework 0.334.0 → 0.336.0

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 (178) hide show
  1. package/dist/api/anonymous-live-entities.d.ts +2 -0
  2. package/dist/api/anonymous-live-entities.js +17 -0
  3. package/dist/api/auth-routes.js +19 -4
  4. package/dist/api/client-ip.js +3 -6
  5. package/dist/api/extra-route.js +3 -2
  6. package/dist/api/index.d.ts +2 -1
  7. package/dist/api/index.js +1 -0
  8. package/dist/api/pii-leak-guard.js +7 -4
  9. package/dist/api/redis-sse-broker.d.ts +2 -0
  10. package/dist/api/redis-sse-broker.js +6 -7
  11. package/dist/api/routes.js +16 -17
  12. package/dist/api/server.js +20 -10
  13. package/dist/api/sse-broker.d.ts +3 -0
  14. package/dist/api/sse-broker.js +14 -0
  15. package/dist/api/sse-route.d.ts +5 -2
  16. package/dist/api/sse-route.js +71 -11
  17. package/dist/bun-db/connection.js +0 -2
  18. package/dist/bun-db/index.d.ts +1 -1
  19. package/dist/bun-db/index.js +1 -1
  20. package/dist/bun-db/query.d.ts +1 -0
  21. package/dist/bun-db/query.js +15 -4
  22. package/dist/crypto/kek-source.js +40 -36
  23. package/dist/crypto/kms-adapter.js +2 -2
  24. package/dist/db/apply-entity-event.js +8 -2
  25. package/dist/db/blind-index-cleanup.js +32 -11
  26. package/dist/db/entity-table-meta.js +5 -3
  27. package/dist/db/event-store-executor-context.js +13 -19
  28. package/dist/db/event-store-executor-read.js +45 -38
  29. package/dist/db/event-store-executor-write.js +15 -17
  30. package/dist/db/migrate-generator.js +27 -28
  31. package/dist/db/pg-error.js +9 -5
  32. package/dist/db/queries/backfill-pii.js +15 -5
  33. package/dist/db/queries/shadow-swap.js +5 -4
  34. package/dist/db/table-builder.d.ts +1 -0
  35. package/dist/db/table-builder.js +7 -15
  36. package/dist/db/tenant-db.js +18 -2
  37. package/dist/derivatives/variant-key.js +3 -1
  38. package/dist/engine/boot-validator/access-declarations.js +12 -3
  39. package/dist/engine/boot-validator/entity-handler.js +13 -1
  40. package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
  41. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  42. package/dist/engine/boot-validator/index.js +6 -6
  43. package/dist/engine/boot-validator/nav.d.ts +2 -1
  44. package/dist/engine/boot-validator/nav.js +10 -20
  45. package/dist/engine/boot-validator/ownership.d.ts +1 -1
  46. package/dist/engine/boot-validator/ownership.js +2 -1
  47. package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
  48. package/dist/engine/boot-validator/pii-retention.js +5 -2
  49. package/dist/engine/boot-validator/projection-list-screens.js +8 -0
  50. package/dist/engine/boot-validator/query-output-columns.js +53 -5
  51. package/dist/engine/boot-validator/screens.d.ts +2 -2
  52. package/dist/engine/boot-validator/screens.js +125 -46
  53. package/dist/engine/boot-validator/transfer-graph.js +9 -6
  54. package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
  55. package/dist/engine/boot-validator/zod-shape.js +17 -4
  56. package/dist/engine/build-app-schema.js +65 -20
  57. package/dist/engine/build-config-feature-schema.js +8 -1
  58. package/dist/engine/changeset-changes.js +24 -10
  59. package/dist/engine/create-app.js +10 -1
  60. package/dist/engine/extension-names.d.ts +4 -0
  61. package/dist/engine/extension-names.js +4 -0
  62. package/dist/engine/feature-ast/extractors/events.js +11 -8
  63. package/dist/engine/feature-ast/extractors/handlers.js +39 -16
  64. package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
  65. package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
  66. package/dist/engine/feature-ast/parse.d.ts +1 -0
  67. package/dist/engine/feature-ast/parse.js +18 -3
  68. package/dist/engine/feature-ast/patch.js +80 -61
  69. package/dist/engine/feature-ast/patcher.d.ts +1 -0
  70. package/dist/engine/feature-ast/patcher.js +2 -1
  71. package/dist/engine/feature-ast/render.js +1 -1
  72. package/dist/engine/feature-entity-handlers.d.ts +1 -0
  73. package/dist/engine/feature-entity-handlers.js +2 -0
  74. package/dist/engine/index.d.ts +4 -3
  75. package/dist/engine/index.js +3 -2
  76. package/dist/engine/ownership.d.ts +1 -0
  77. package/dist/engine/ownership.js +15 -2
  78. package/dist/engine/pattern-library/mixed-schemas.js +15 -29
  79. package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
  80. package/dist/engine/pattern-library/shared-fields.js +17 -0
  81. package/dist/engine/registry-facade.js +3 -0
  82. package/dist/engine/registry-validate.d.ts +1 -0
  83. package/dist/engine/registry-validate.js +14 -0
  84. package/dist/engine/registry.js +2 -1
  85. package/dist/engine/role-assignment.d.ts +7 -1
  86. package/dist/engine/role-assignment.js +27 -7
  87. package/dist/engine/schema-builder.d.ts +1 -0
  88. package/dist/engine/schema-builder.js +9 -2
  89. package/dist/engine/screen-helpers.js +15 -4
  90. package/dist/engine/steps/_event-match.js +34 -7
  91. package/dist/engine/steps/_read-source.js +8 -0
  92. package/dist/engine/system-user.d.ts +1 -0
  93. package/dist/engine/system-user.js +3 -0
  94. package/dist/engine/where-rule-lint.js +22 -4
  95. package/dist/env/dry-run.js +10 -3
  96. package/dist/env/index.js +21 -1
  97. package/dist/errors/reasons.d.ts +0 -1
  98. package/dist/errors/reasons.js +2 -5
  99. package/dist/event-store/event-store.js +10 -2
  100. package/dist/event-store/provenance-append.js +4 -1
  101. package/dist/files/file-routes.d.ts +0 -1
  102. package/dist/files/file-routes.js +22 -7
  103. package/dist/files/local-provider.js +13 -10
  104. package/dist/files/provider-resolver.js +20 -1
  105. package/dist/files/types.js +16 -1
  106. package/dist/i18n/request-locale.d.ts +3 -2
  107. package/dist/i18n/request-locale.js +16 -6
  108. package/dist/i18n/required-surface-keys.js +23 -35
  109. package/dist/i18n/settings-hub-keys.js +3 -0
  110. package/dist/jobs/index.d.ts +2 -2
  111. package/dist/jobs/index.js +1 -1
  112. package/dist/jobs/job-runner.d.ts +2 -0
  113. package/dist/jobs/job-runner.js +172 -59
  114. package/dist/migrations/index.d.ts +1 -1
  115. package/dist/migrations/index.js +1 -1
  116. package/dist/migrations/pending-rebuilds.d.ts +4 -0
  117. package/dist/migrations/pending-rebuilds.js +23 -1
  118. package/dist/observability/escape-hatch-report.d.ts +1 -0
  119. package/dist/observability/escape-hatch-report.js +3 -0
  120. package/dist/observability/index.d.ts +2 -1
  121. package/dist/observability/index.js +2 -1
  122. package/dist/observability/standard-metrics.d.ts +4 -0
  123. package/dist/observability/standard-metrics.js +16 -0
  124. package/dist/pipeline/dispatch-query.d.ts +1 -1
  125. package/dist/pipeline/dispatch-query.js +2 -2
  126. package/dist/pipeline/dispatch-shared.d.ts +2 -3
  127. package/dist/pipeline/dispatch-shared.js +14 -9
  128. package/dist/pipeline/dispatch-stream.js +2 -5
  129. package/dist/pipeline/dispatch-write.js +22 -5
  130. package/dist/pipeline/dispatcher.js +1 -2
  131. package/dist/pipeline/event-dispatcher-admin.js +12 -2
  132. package/dist/pipeline/event-dispatcher-delivery.js +5 -0
  133. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  134. package/dist/pipeline/event-dispatcher.js +30 -12
  135. package/dist/pipeline/index.d.ts +1 -1
  136. package/dist/pipeline/index.js +1 -1
  137. package/dist/pipeline/member-read-only-transaction.js +1 -14
  138. package/dist/pipeline/msp-rebuild.d.ts +1 -0
  139. package/dist/pipeline/msp-rebuild.js +1 -0
  140. package/dist/pipeline/system-hooks.d.ts +1 -1
  141. package/dist/pipeline/system-hooks.js +19 -20
  142. package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
  143. package/dist/pipeline/tenant-timezone-cache.js +20 -6
  144. package/dist/pipeline/write-origin.js +13 -6
  145. package/dist/redis/pubsub-signal.d.ts +2 -0
  146. package/dist/redis/pubsub-signal.js +2 -2
  147. package/dist/schema-cli.js +4 -14
  148. package/dist/search/meilisearch-adapter.js +17 -6
  149. package/dist/search/purge-subject.js +8 -3
  150. package/dist/stack/drain-event-consumers.js +3 -0
  151. package/dist/stack/drain-jobs.d.ts +3 -1
  152. package/dist/stack/drain-jobs.js +55 -13
  153. package/dist/stack/table-helpers.js +10 -8
  154. package/dist/testing/closed-connection-error.js +2 -3
  155. package/dist/testing/wait-for.js +1 -0
  156. package/dist/ui-types/index.d.ts +1 -1
  157. package/dist/ui-types/index.js +1 -1
  158. package/dist/upgrade-cli.d.ts +1 -0
  159. package/dist/upgrade-cli.js +36 -15
  160. package/package.json +4 -4
  161. package/src/changes.json +264 -0
  162. package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
  163. package/src/scripts/codemod/migrate-db-raw.ts +16 -8
  164. package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
  165. package/src/scripts/codemod/pii-personal-migration.ts +111 -44
  166. package/src/scripts/codemod/unprocessable-error-details-reason.ts +136 -42
  167. package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
  168. package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
  169. package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
  170. package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
  171. package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
  172. package/dist/scripts/codemod/migrate-db-raw.js +0 -345
  173. package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
  174. package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
  175. package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
  176. package/dist/scripts/codemod/pii-personal-migration.js +0 -615
  177. package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
  178. package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
@@ -105,9 +105,25 @@ function resolveDetailsProperty(optsLiteral: ObjectLiteralExpression): DetailsRe
105
105
  /** True if removing this shorthand-bound identifier would leave its declaration with no other use. */
106
106
  function wouldLeaveUnusedVariable(nameNode: Identifier): boolean {
107
107
  try {
108
- const refs = nameNode.findReferencesAsNodes();
109
- const others = refs.filter((r) => r !== nameNode);
110
- return others.length === 0;
108
+ // The shorthand name resolves to the object-literal property symbol too, and
109
+ // a declaration counts as a reference, so look up the variable's own uses.
110
+ const shorthand = nameNode.getParent();
111
+ const variableSymbol = Node.isShorthandPropertyAssignment(shorthand)
112
+ ? shorthand.getValueSymbol()
113
+ : undefined;
114
+ const declarations = variableSymbol?.getDeclarations() ?? [];
115
+ const declarationNames = declarations.flatMap((declaration) =>
116
+ Node.isVariableDeclaration(declaration) || Node.isParameterDeclaration(declaration)
117
+ ? [declaration.getNameNode()]
118
+ : [],
119
+ );
120
+ // No resolvable declaration: can't verify, treat as unsafe.
121
+ if (declarationNames.length === 0) return true;
122
+ return declarationNames
123
+ .flatMap((declarationName) =>
124
+ Node.isIdentifier(declarationName) ? declarationName.findReferencesAsNodes() : [],
125
+ )
126
+ .every((usage) => usage === nameNode);
111
127
  } catch {
112
128
  // Can't verify — treat as unsafe rather than guess.
113
129
  return true;
@@ -122,12 +138,11 @@ type RemovalResult =
122
138
  /** Removes the `reason` property from a `details` object literal, in place. */
123
139
  function removeReasonProperty(detailsLiteral: ObjectLiteralExpression): RemovalResult {
124
140
  const properties = detailsLiteral.getProperties();
125
- if (properties.some((p) => Node.isSpreadAssignment(p))) {
126
- return {
127
- kind: "skip",
128
- note: "details object literal contains a spread — not statically decidable",
129
- };
130
- }
141
+ const hasSpread = properties.some((p) => Node.isSpreadAssignment(p));
142
+ const spreadSkip: RemovalResult = {
143
+ kind: "skip",
144
+ note: "details object literal has a spread next to an explicit reason — not statically decidable",
145
+ };
131
146
 
132
147
  for (const prop of properties) {
133
148
  if (Node.isPropertyAssignment(prop)) {
@@ -145,12 +160,14 @@ function removeReasonProperty(detailsLiteral: ObjectLiteralExpression): RemovalR
145
160
  ? nameNode.getLiteralText()
146
161
  : undefined;
147
162
  if (propName === "reason") {
163
+ if (hasSpread) return spreadSkip;
148
164
  prop.remove();
149
165
  return { kind: "removed" };
150
166
  }
151
167
  continue;
152
168
  }
153
169
  if (Node.isShorthandPropertyAssignment(prop) && prop.getName() === "reason") {
170
+ if (hasSpread) return spreadSkip;
154
171
  if (wouldLeaveUnusedVariable(prop.getNameNode())) {
155
172
  return {
156
173
  kind: "skip",
@@ -166,13 +183,20 @@ function removeReasonProperty(detailsLiteral: ObjectLiteralExpression): RemovalR
166
183
 
167
184
  type CallOutcome = "changed" | "skipped" | "not-a-match";
168
185
 
169
- function processNewExpression(args: readonly Node[], report: (note: string) => void): CallOutcome {
186
+ // "manual": a concrete `reason` the codemod found but cannot remove safely.
187
+ // "unverified": opts/details not statically analyzable — may well be fine, so
188
+ // it is listed apart to keep the actionable list short.
189
+ type SkipKind = "manual" | "unverified";
190
+ type ReportSkip = (note: string, kind: SkipKind) => void;
191
+
192
+ function processNewExpression(args: readonly Node[], report: ReportSkip): CallOutcome {
170
193
  const optsArg = args[1];
171
194
  if (!optsArg) return "not-a-match";
172
195
 
173
196
  if (!Node.isObjectLiteralExpression(optsArg)) {
174
197
  report(
175
198
  "second argument (opts) is not a statically-analyzable object literal — cannot verify details.reason",
199
+ "unverified",
176
200
  );
177
201
  return "skipped";
178
202
  }
@@ -180,20 +204,103 @@ function processNewExpression(args: readonly Node[], report: (note: string) => v
180
204
  const detailsRes = resolveDetailsProperty(optsArg);
181
205
  if (detailsRes.kind === "absent") return "not-a-match";
182
206
  if (detailsRes.kind === "unresolvable") {
183
- report(detailsRes.note);
207
+ report(detailsRes.note, "unverified");
184
208
  return "skipped";
185
209
  }
186
210
 
187
211
  const removal = removeReasonProperty(detailsRes.literal);
188
212
  if (removal.kind === "absent") return "not-a-match";
189
213
  if (removal.kind === "skip") {
190
- report(removal.note);
214
+ report(removal.note, "manual");
215
+ return "skipped";
216
+ }
217
+ return "changed";
218
+ }
219
+
220
+ // `failUnprocessable(reason, details)` takes the details literal directly as
221
+ // its second argument (no `{ details }` wrapper like the ctor's opts).
222
+ function processFailUnprocessableCall(args: readonly Node[], report: ReportSkip): CallOutcome {
223
+ const detailsArg = args[1];
224
+ if (!detailsArg) return "not-a-match";
225
+
226
+ if (!Node.isObjectLiteralExpression(detailsArg)) {
227
+ report(
228
+ "second argument (details) is not a statically-analyzable object literal — cannot verify details.reason",
229
+ "unverified",
230
+ );
231
+ return "skipped";
232
+ }
233
+
234
+ const removal = removeReasonProperty(detailsArg);
235
+ if (removal.kind === "absent") return "not-a-match";
236
+ if (removal.kind === "skip") {
237
+ report(removal.note, "manual");
191
238
  return "skipped";
192
239
  }
193
240
  return "changed";
194
241
  }
195
242
 
196
- type SkipEntry = { readonly file: string; readonly line: number; readonly note: string };
243
+ type SkipEntry = {
244
+ readonly file: string;
245
+ readonly line: number;
246
+ readonly note: string;
247
+ readonly kind: SkipKind;
248
+ };
249
+
250
+ type MigratableCall = {
251
+ readonly exportName: "UnprocessableError" | "failUnprocessable";
252
+ readonly localKind: "class" | "function";
253
+ readonly process: (args: readonly Node[], report: ReportSkip) => CallOutcome;
254
+ };
255
+
256
+ const UNPROCESSABLE_ERROR_CTOR: MigratableCall = {
257
+ exportName: "UnprocessableError",
258
+ localKind: "class",
259
+ process: processNewExpression,
260
+ };
261
+ const FAIL_UNPROCESSABLE_CALL: MigratableCall = {
262
+ exportName: "failUnprocessable",
263
+ localKind: "function",
264
+ process: processFailUnprocessableCall,
265
+ };
266
+
267
+ function migrateCallSite(
268
+ sourceFile: SourceFile,
269
+ importedNames: ReadonlyMap<string, ImportedBinding>,
270
+ site: Node & { getExpression(): Node; getArguments(): Node[] },
271
+ target: MigratableCall,
272
+ skips: SkipEntry[],
273
+ ): boolean {
274
+ const callee = site.getExpression();
275
+ if (!Node.isIdentifier(callee)) return false;
276
+ const localName = callee.getText();
277
+ const line = site.getStartLineNumber();
278
+ const file = sourceFile.getFilePath();
279
+
280
+ const binding = importedNames.get(localName);
281
+ if (binding?.importedName !== target.exportName) {
282
+ if (localName !== target.exportName) return false;
283
+ const note = binding
284
+ ? `${target.exportName} is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`
285
+ : `${target.exportName} has no @cosmicdrift/kumiko-* import in this file (likely a locally defined ${target.localKind})`;
286
+ skips.push({ file, line, note, kind: "manual" });
287
+ return false;
288
+ }
289
+ if (!KUMIKO_MODULE_RE.test(binding.moduleSpecifier)) {
290
+ skips.push({
291
+ file,
292
+ line,
293
+ note: `${target.exportName} is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`,
294
+ kind: "manual",
295
+ });
296
+ return false;
297
+ }
298
+
299
+ const outcome = target.process(site.getArguments(), (note, kind) =>
300
+ skips.push({ file, line, note, kind }),
301
+ );
302
+ return outcome === "changed";
303
+ }
197
304
 
198
305
  /** Migrates one file in place. Returns how many `reason` properties were removed. */
199
306
  function migrateFile(sourceFile: SourceFile, skips: SkipEntry[]): number {
@@ -201,34 +308,14 @@ function migrateFile(sourceFile: SourceFile, skips: SkipEntry[]): number {
201
308
  let removedCount = 0;
202
309
 
203
310
  for (const newExpr of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
204
- const callee = newExpr.getExpression();
205
- if (!Node.isIdentifier(callee)) continue;
206
- const localName = callee.getText();
207
-
208
- const binding = importedNames.get(localName);
209
- const isFrameworkUnprocessableError = binding?.importedName === "UnprocessableError";
210
-
211
- if (!isFrameworkUnprocessableError) {
212
- if (localName !== "UnprocessableError") continue;
213
- const note = binding
214
- ? `UnprocessableError is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`
215
- : "UnprocessableError has no @cosmicdrift/kumiko-* import in this file (likely a locally defined class)";
216
- skips.push({ file: sourceFile.getFilePath(), line: newExpr.getStartLineNumber(), note });
217
- continue;
311
+ if (migrateCallSite(sourceFile, importedNames, newExpr, UNPROCESSABLE_ERROR_CTOR, skips)) {
312
+ removedCount++;
218
313
  }
219
- if (!KUMIKO_MODULE_RE.test(binding.moduleSpecifier)) {
220
- skips.push({
221
- file: sourceFile.getFilePath(),
222
- line: newExpr.getStartLineNumber(),
223
- note: `UnprocessableError is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`,
224
- });
225
- continue;
314
+ }
315
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
316
+ if (migrateCallSite(sourceFile, importedNames, call, FAIL_UNPROCESSABLE_CALL, skips)) {
317
+ removedCount++;
226
318
  }
227
-
228
- const outcome = processNewExpression(newExpr.getArguments(), (note) => {
229
- skips.push({ file: sourceFile.getFilePath(), line: newExpr.getStartLineNumber(), note });
230
- });
231
- if (outcome === "changed") removedCount++;
232
319
  }
233
320
 
234
321
  return removedCount;
@@ -263,9 +350,16 @@ async function main(): Promise<void> {
263
350
  console.log(
264
351
  `Touched ${touchedFiles} files, removed ${removedTotal} redundant "reason" propert(y/ies).`,
265
352
  );
266
- console.log(`Skipped ${skips.length} site(s):`);
267
- for (const s of skips) {
268
- console.log(` ${relative(rootDir, s.file)}:${s.line} — ${s.note}`);
353
+ for (const kind of ["manual", "unverified"] as const) {
354
+ const ofKind = skips.filter((s) => s.kind === kind);
355
+ console.log(
356
+ kind === "manual"
357
+ ? `Skipped ${ofKind.length} site(s) needing manual work:`
358
+ : `Unverified ${ofKind.length} site(s) (opts/details not statically analyzable, may be fine):`,
359
+ );
360
+ for (const s of ofKind) {
361
+ console.log(` ${relative(rootDir, s.file)}:${s.line} — ${s.note}`);
362
+ }
269
363
  }
270
364
  console.log();
271
365
  }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
@@ -1,109 +0,0 @@
1
- #!/usr/bin/env bun
2
- // Rewrites `import { resetPiiSubjectKmsForTests, resetBlindIndexKeyForTests }
3
- // from "@cosmicdrift/kumiko-framework/crypto"` to import both names from
4
- // "@cosmicdrift/kumiko-framework/testing" instead (fw#1631). Only the
5
- // package barrel specifier is rewritten — relative deep-imports of the
6
- // defining module are left untouched, per the changes.json migration note.
7
- // Idempotent: an already-migrated file has nothing left to move.
8
- //
9
- // Usage: bun scripts/codemod/crypto-shredding-testing-move.ts <targetDir> [--dry-run]
10
- import { readdirSync } from "node:fs";
11
- import { join, resolve } from "node:path";
12
- import { Project } from "ts-morph";
13
- const OLD_SPECIFIER = "@cosmicdrift/kumiko-framework/crypto";
14
- const NEW_SPECIFIER = "@cosmicdrift/kumiko-framework/testing";
15
- const MOVED_NAMES = new Set(["resetPiiSubjectKmsForTests", "resetBlindIndexKeyForTests"]);
16
- const EXCLUDE_DIRS = new Set(["node_modules", "dist", "build"]);
17
- function findTargetFiles(rootDir) {
18
- // Walk the tree and skip excluded dirs while descending — filtering the
19
- // absolute path after a full Glob scan silently no-ops when the repo root
20
- // itself contains "/build/" or "/node_modules/" (fw#2289).
21
- const files = [];
22
- const walk = (dir) => {
23
- let entries;
24
- try {
25
- entries = readdirSync(dir, { withFileTypes: true });
26
- }
27
- catch {
28
- // skip: unreadable directory during walk — treat as empty.
29
- return;
30
- }
31
- for (const ent of entries) {
32
- if (ent.isDirectory()) {
33
- if (EXCLUDE_DIRS.has(ent.name) || ent.name.startsWith("."))
34
- continue;
35
- walk(join(dir, ent.name));
36
- continue;
37
- }
38
- if (ent.isFile() && /\.(ts|tsx)$/.test(ent.name)) {
39
- files.push(join(dir, ent.name));
40
- }
41
- }
42
- };
43
- walk(rootDir);
44
- return files.sort();
45
- }
46
- /** Move matching value imports from crypto → testing. Returns count of names moved. */
47
- function migrateFile(sourceFile) {
48
- // Value imports only — a pre-existing `import type { … } from testing`
49
- // must not receive runtime helpers (they would be erased).
50
- const oldImports = sourceFile
51
- .getImportDeclarations()
52
- .filter((d) => d.getModuleSpecifierValue() === OLD_SPECIFIER && !d.isTypeOnly());
53
- if (oldImports.length === 0)
54
- return 0;
55
- let fileMoved = 0;
56
- for (const oldImport of oldImports) {
57
- const movedHere = oldImport.getNamedImports().filter((spec) => MOVED_NAMES.has(spec.getName()));
58
- if (movedHere.length === 0)
59
- continue;
60
- const names = movedHere.map((spec) => spec.getName());
61
- const existingNewImport = sourceFile
62
- .getImportDeclarations()
63
- .find((d) => d.getModuleSpecifierValue() === NEW_SPECIFIER && !d.isTypeOnly());
64
- if (existingNewImport) {
65
- const already = new Set(existingNewImport.getNamedImports().map((s) => s.getName()));
66
- for (const name of names) {
67
- if (!already.has(name))
68
- existingNewImport.addNamedImport(name);
69
- }
70
- }
71
- else {
72
- sourceFile.addImportDeclaration({ moduleSpecifier: NEW_SPECIFIER, namedImports: names });
73
- }
74
- for (const spec of movedHere)
75
- spec.remove();
76
- const remaining = oldImport.getNamedImports().length > 0 ||
77
- !!oldImport.getDefaultImport() ||
78
- !!oldImport.getNamespaceImport();
79
- if (!remaining)
80
- oldImport.remove();
81
- fileMoved += names.length;
82
- }
83
- return fileMoved;
84
- }
85
- async function main() {
86
- const positional = process.argv.slice(2).filter((a) => !a.startsWith("--"));
87
- const dryRun = process.argv.includes("--dry-run");
88
- const rootDir = resolve(positional[0] ?? process.cwd());
89
- const files = findTargetFiles(rootDir);
90
- const project = new Project({
91
- skipAddingFilesFromTsConfig: true,
92
- skipFileDependencyResolution: true,
93
- });
94
- let touchedFiles = 0;
95
- let movedNames = 0;
96
- for (const file of files) {
97
- const sourceFile = project.addSourceFileAtPath(file);
98
- const fileMoved = migrateFile(sourceFile);
99
- if (fileMoved === 0)
100
- continue;
101
- touchedFiles++;
102
- movedNames += fileMoved;
103
- if (!dryRun)
104
- sourceFile.saveSync();
105
- }
106
- console.log(`\nScanned ${files.length} files under ${rootDir}${dryRun ? " (dry-run)" : ""}.`);
107
- console.log(`Touched ${touchedFiles} files, moved ${movedNames} import(s) from "${OLD_SPECIFIER}" to "${NEW_SPECIFIER}".\n`);
108
- }
109
- await main();
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env bun
2
- export interface Rewrite {
3
- readonly file: string;
4
- readonly line: number;
5
- readonly before: string;
6
- readonly after: string;
7
- }
8
- export interface ManualSite {
9
- readonly file: string;
10
- readonly line: number;
11
- readonly why: string;
12
- readonly handlerName?: string;
13
- }
14
- export interface MigrateCrossTenantResult {
15
- readonly output: string;
16
- readonly rewrites: Rewrite[];
17
- readonly manual: ManualSite[];
18
- }
19
- export declare function migrateCrossTenantSource(source: string, fileName: string): MigrateCrossTenantResult;
@@ -1,266 +0,0 @@
1
- #!/usr/bin/env bun
2
- // migrate-cross-tenant — rewrites `crossTenant: true` on entity-convention
3
- // handlers to `escapeHatch: { reason }` where the handler name and verb can
4
- // be derived from the call, and reports every other site for review (fw#2915).
5
- //
6
- // Usage:
7
- // bun scripts/codemod/migrate-cross-tenant.ts [--dry-run] <path...>
8
- import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
- import { join, resolve } from "node:path";
10
- import { Node, Project, SyntaxKind, } from "ts-morph";
11
- const VERB_IN_NAME_CALLEES = {
12
- defineEntityCreateHandler: "create",
13
- defineEntityUpdateHandler: "update",
14
- defineEntityDeleteHandler: "delete",
15
- defineEntityRestoreHandler: "restore",
16
- defineEntityListHandler: "list",
17
- defineEntityDetailHandler: "detail",
18
- };
19
- const VERB_IN_ARG_CALLEES = new Set(["defineEntityWriteHandler", "defineEntityQueryHandler"]);
20
- const READ_VERBS = new Set(["list", "detail"]);
21
- function isCrossTenantAssignment(node) {
22
- return Node.isPropertyAssignment(node) && node.getName() === "crossTenant";
23
- }
24
- function getCalleeName(call) {
25
- const expr = call.getExpression();
26
- if (Node.isIdentifier(expr))
27
- return expr.getText();
28
- if (Node.isPropertyAccessExpression(expr))
29
- return expr.getName();
30
- return undefined;
31
- }
32
- function hasEscapeHatchProperty(obj) {
33
- return obj.getProperty("escapeHatch") !== undefined;
34
- }
35
- function deriveAutoRewriteTarget(objectLiteral) {
36
- const call = objectLiteral.getParentIfKind(SyntaxKind.CallExpression);
37
- if (!call)
38
- return undefined;
39
- if (!call.getArguments().includes(objectLiteral))
40
- return undefined;
41
- const calleeName = getCalleeName(call);
42
- if (!calleeName)
43
- return undefined;
44
- const firstArg = call.getArguments()[0];
45
- if (!firstArg || !Node.isStringLiteral(firstArg))
46
- return undefined;
47
- const firstArgValue = firstArg.getLiteralValue();
48
- const verbFromName = VERB_IN_NAME_CALLEES[calleeName];
49
- if (verbFromName) {
50
- return {
51
- handlerName: `${firstArgValue}:${verbFromName}`,
52
- entityName: firstArgValue,
53
- verb: verbFromName,
54
- };
55
- }
56
- if (VERB_IN_ARG_CALLEES.has(calleeName)) {
57
- const sep = firstArgValue.indexOf(":");
58
- if (sep === -1)
59
- return undefined;
60
- const entityName = firstArgValue.slice(0, sep);
61
- const verb = firstArgValue.slice(sep + 1);
62
- if (!entityName || !verb)
63
- return undefined;
64
- return { handlerName: firstArgValue, entityName, verb };
65
- }
66
- return undefined;
67
- }
68
- function deriveEntityCrudContext(objectLiteral) {
69
- const verbGroupAssignment = objectLiteral.getParent();
70
- if (!Node.isPropertyAssignment(verbGroupAssignment))
71
- return undefined;
72
- const verbGroup = verbGroupAssignment.getName();
73
- if (verbGroup !== "write" && verbGroup !== "read")
74
- return undefined;
75
- const optionsObject = verbGroupAssignment.getParentIfKind(SyntaxKind.ObjectLiteralExpression);
76
- const call = optionsObject?.getParentIfKind(SyntaxKind.CallExpression);
77
- if (!call || !optionsObject || !call.getArguments().includes(optionsObject))
78
- return undefined;
79
- const calleeName = getCalleeName(call);
80
- if (calleeName !== "registerEntityCrud" && calleeName !== "crud")
81
- return undefined;
82
- const args = call.getArguments();
83
- const entityArg = calleeName === "crud" ? args[0] : args[1];
84
- const entityName = entityArg && Node.isStringLiteral(entityArg) ? entityArg.getLiteralValue() : undefined;
85
- return `under ${calleeName}(${entityName ? `"${entityName}", ` : ""}...) → ${verbGroup}: multiple verbs share this crossTenant — set escapeHatch per verb manually, one reason each`;
86
- }
87
- function classifyManualReason(objectLiteral) {
88
- const entityCrudWhy = deriveEntityCrudContext(objectLiteral);
89
- if (entityCrudWhy)
90
- return entityCrudWhy;
91
- const call = objectLiteral.getParentIfKind(SyntaxKind.CallExpression);
92
- if (!call?.getArguments().includes(objectLiteral)) {
93
- return "crossTenant is not set directly in a handler call (e.g. a spread/shared object) — give each consuming handler its own escapeHatch reason";
94
- }
95
- const calleeName = getCalleeName(call);
96
- if (!calleeName || (!VERB_IN_NAME_CALLEES[calleeName] && !VERB_IN_ARG_CALLEES.has(calleeName))) {
97
- return `call \`${calleeName ?? "<unknown>"}\` is not a recognized entity-convention handler factory — add escapeHatch manually`;
98
- }
99
- return "handler name/verb could not be derived from the call's first argument — add escapeHatch manually";
100
- }
101
- function deriveBestEffortHandlerName(assignment) {
102
- const call = assignment.getFirstAncestorByKind(SyntaxKind.CallExpression);
103
- const firstArg = call?.getArguments()[0];
104
- return firstArg && Node.isStringLiteral(firstArg) ? firstArg.getLiteralValue() : undefined;
105
- }
106
- function buildEscapeHatchReason(target) {
107
- const verbKind = READ_VERBS.has(target.verb) ? "reads" : "writes";
108
- return `${target.handlerName} ${verbKind} ${target.entityName} rows across every tenant (migrated from crossTenant: true; state the operator use case here)`;
109
- }
110
- export function migrateCrossTenantSource(source, fileName) {
111
- const project = new Project({ useInMemoryFileSystem: true, skipFileDependencyResolution: true });
112
- const sourceFile = project.createSourceFile(fileName, source);
113
- const targets = sourceFile
114
- .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
115
- .filter(isCrossTenantAssignment);
116
- const rewrites = [];
117
- const manual = [];
118
- for (const assignment of targets) {
119
- if (assignment.wasForgotten())
120
- continue;
121
- const line = assignment.getStartLineNumber();
122
- const init = assignment.getInitializer();
123
- if (init?.getKind() === SyntaxKind.FalseKeyword)
124
- continue;
125
- if (init?.getKind() !== SyntaxKind.TrueKeyword) {
126
- manual.push({
127
- file: fileName,
128
- line,
129
- why: `crossTenant is not a literal boolean (\`${init?.getText() ?? "<missing>"}\`) — replace manually with escapeHatch: { reason }`,
130
- ...(deriveBestEffortHandlerName(assignment) !== undefined && {
131
- handlerName: deriveBestEffortHandlerName(assignment),
132
- }),
133
- });
134
- continue;
135
- }
136
- const objectLiteral = assignment.getParentIfKind(SyntaxKind.ObjectLiteralExpression);
137
- if (!objectLiteral)
138
- continue;
139
- if (hasEscapeHatchProperty(objectLiteral)) {
140
- manual.push({
141
- file: fileName,
142
- line,
143
- why: "this object already declares escapeHatch — merge the two manually and remove crossTenant",
144
- ...(deriveBestEffortHandlerName(assignment) !== undefined && {
145
- handlerName: deriveBestEffortHandlerName(assignment),
146
- }),
147
- });
148
- continue;
149
- }
150
- const target = deriveAutoRewriteTarget(objectLiteral);
151
- if (!target) {
152
- manual.push({
153
- file: fileName,
154
- line,
155
- why: classifyManualReason(objectLiteral),
156
- ...(deriveBestEffortHandlerName(assignment) !== undefined && {
157
- handlerName: deriveBestEffortHandlerName(assignment),
158
- }),
159
- });
160
- continue;
161
- }
162
- const before = assignment.getText();
163
- const after = `escapeHatch: { reason: ${JSON.stringify(buildEscapeHatchReason(target))} }`;
164
- assignment.replaceWithText(after);
165
- rewrites.push({ file: fileName, line, before, after });
166
- }
167
- return { output: sourceFile.getFullText(), rewrites, manual };
168
- }
169
- class CliError extends Error {
170
- }
171
- function parseArgs(argv) {
172
- let dryRun = false;
173
- const paths = [];
174
- for (const arg of argv) {
175
- if (arg === "--dry-run") {
176
- dryRun = true;
177
- continue;
178
- }
179
- if (arg.startsWith("--"))
180
- throw new CliError(`unknown flag: ${arg}`);
181
- paths.push(arg);
182
- }
183
- if (paths.length === 0)
184
- throw new CliError("at least one path is required");
185
- return { dryRun, paths };
186
- }
187
- function isMigratableFile(name) {
188
- return /\.(ts|tsx)$/.test(name);
189
- }
190
- function walkDir(dir, out) {
191
- for (const name of readdirSync(dir)) {
192
- if (name === "node_modules")
193
- continue;
194
- const full = join(dir, name);
195
- const stat = statSync(full);
196
- if (stat.isDirectory()) {
197
- walkDir(full, out);
198
- continue;
199
- }
200
- if (isMigratableFile(name))
201
- out.push(full);
202
- }
203
- }
204
- function collectFiles(paths) {
205
- const files = [];
206
- for (const p of paths) {
207
- const abs = resolve(p);
208
- const stat = statSync(abs);
209
- if (stat.isDirectory())
210
- walkDir(abs, files);
211
- else if (isMigratableFile(abs))
212
- files.push(abs);
213
- }
214
- return files;
215
- }
216
- async function main() {
217
- let options;
218
- try {
219
- options = parseArgs(process.argv.slice(2));
220
- }
221
- catch (err) {
222
- if (err instanceof CliError) {
223
- console.error(`[migrate-cross-tenant] ${err.message}`);
224
- process.exit(2);
225
- }
226
- throw err;
227
- }
228
- const files = collectFiles(options.paths);
229
- const allManual = [];
230
- let filesChanged = 0;
231
- let totalRewrites = 0;
232
- for (const file of files) {
233
- const before = readFileSync(file, "utf8");
234
- if (!before.includes("crossTenant"))
235
- continue;
236
- const result = migrateCrossTenantSource(before, file);
237
- allManual.push(...result.manual);
238
- if (result.rewrites.length === 0)
239
- continue;
240
- filesChanged += 1;
241
- totalRewrites += result.rewrites.length;
242
- if (options.dryRun) {
243
- console.log(`\n=== ${file} (${result.rewrites.length} rewrite(s)) ===`);
244
- for (const r of result.rewrites) {
245
- console.log(`${file}:${r.line}`);
246
- console.log(`- ${r.before}`);
247
- console.log(`+ ${r.after}`);
248
- }
249
- }
250
- else {
251
- writeFileSync(file, result.output);
252
- }
253
- }
254
- console.log(`\n[migrate-cross-tenant] ${options.dryRun ? "would change" : "changed"} ${filesChanged} file(s); ` +
255
- `rewrites: ${totalRewrites}`);
256
- if (allManual.length > 0) {
257
- console.log(`\n[migrate-cross-tenant] manual review needed (${allManual.length}):`);
258
- for (const m of allManual) {
259
- console.log(`${m.file}:${m.line} ${m.handlerName ?? "<unknown>"} ${m.why}`);
260
- }
261
- }
262
- process.exit(0);
263
- }
264
- if (import.meta.main) {
265
- await main();
266
- }
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env bun
2
- export type RewriteRule = "global-table" | "own-tenant";
3
- export interface Rewrite {
4
- readonly file: string;
5
- readonly line: number;
6
- readonly rule: RewriteRule;
7
- readonly before: string;
8
- readonly after: string;
9
- }
10
- export interface ManualSite {
11
- readonly file: string;
12
- readonly line: number;
13
- readonly enclosing: string;
14
- readonly text: string;
15
- }
16
- export interface MigrateDbRawResult {
17
- readonly output: string;
18
- readonly rewrites: Rewrite[];
19
- readonly manual: ManualSite[];
20
- }
21
- export declare function migrateDbRawSource(source: string, fileName: string, opts: {
22
- globalTables: ReadonlySet<string>;
23
- }): MigrateDbRawResult;