@cosmicdrift/kumiko-framework 0.334.0 → 0.335.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.
- package/dist/api/auth-routes.js +19 -4
- package/dist/api/client-ip.js +3 -6
- package/dist/api/extra-route.js +3 -2
- package/dist/api/pii-leak-guard.js +7 -4
- package/dist/api/redis-sse-broker.d.ts +2 -0
- package/dist/api/redis-sse-broker.js +6 -7
- package/dist/api/routes.js +16 -17
- package/dist/api/server.js +14 -7
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/sse-route.js +39 -6
- package/dist/bun-db/connection.js +0 -2
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +1 -0
- package/dist/bun-db/query.js +15 -4
- package/dist/crypto/kek-source.js +40 -36
- package/dist/crypto/kms-adapter.js +2 -2
- package/dist/db/apply-entity-event.js +8 -2
- package/dist/db/blind-index-cleanup.js +11 -1
- package/dist/db/entity-table-meta.js +5 -3
- package/dist/db/event-store-executor-context.js +13 -19
- package/dist/db/event-store-executor-read.js +39 -36
- package/dist/db/event-store-executor-write.js +15 -17
- package/dist/db/migrate-generator.js +27 -28
- package/dist/db/pg-error.js +9 -5
- package/dist/db/queries/backfill-pii.js +15 -5
- package/dist/db/queries/shadow-swap.js +5 -4
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/table-builder.js +7 -15
- package/dist/db/tenant-db.js +18 -2
- package/dist/derivatives/variant-key.js +3 -1
- package/dist/engine/boot-validator/access-declarations.js +12 -3
- package/dist/engine/boot-validator/entity-handler.js +13 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/index.js +6 -6
- package/dist/engine/boot-validator/nav.d.ts +2 -1
- package/dist/engine/boot-validator/nav.js +10 -20
- package/dist/engine/boot-validator/ownership.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.js +2 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
- package/dist/engine/boot-validator/pii-retention.js +5 -2
- package/dist/engine/boot-validator/projection-list-screens.js +8 -0
- package/dist/engine/boot-validator/query-output-columns.js +41 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +103 -33
- package/dist/engine/boot-validator/transfer-graph.js +9 -6
- package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
- package/dist/engine/boot-validator/zod-shape.js +17 -4
- package/dist/engine/build-app-schema.js +62 -19
- package/dist/engine/build-config-feature-schema.js +8 -1
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +38 -16
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
- package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
- package/dist/engine/feature-ast/parse.d.ts +1 -0
- package/dist/engine/feature-ast/parse.js +18 -3
- package/dist/engine/feature-ast/patch.js +80 -61
- package/dist/engine/feature-ast/patcher.d.ts +1 -0
- package/dist/engine/feature-ast/patcher.js +2 -1
- package/dist/engine/feature-ast/render.js +1 -1
- package/dist/engine/index.d.ts +4 -3
- package/dist/engine/index.js +3 -2
- package/dist/engine/ownership.d.ts +1 -0
- package/dist/engine/ownership.js +15 -2
- package/dist/engine/pattern-library/mixed-schemas.js +15 -29
- package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
- package/dist/engine/pattern-library/shared-fields.js +17 -0
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/role-assignment.d.ts +7 -1
- package/dist/engine/role-assignment.js +27 -7
- package/dist/engine/schema-builder.d.ts +1 -0
- package/dist/engine/schema-builder.js +9 -2
- package/dist/engine/screen-helpers.js +15 -4
- package/dist/engine/steps/_event-match.js +22 -3
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/index.js +21 -1
- package/dist/errors/reasons.d.ts +0 -1
- package/dist/errors/reasons.js +2 -5
- package/dist/event-store/event-store.js +6 -1
- package/dist/event-store/provenance-append.js +4 -1
- package/dist/files/file-routes.d.ts +0 -1
- package/dist/files/file-routes.js +22 -7
- package/dist/files/local-provider.js +13 -10
- package/dist/files/provider-resolver.js +20 -1
- package/dist/files/types.js +16 -1
- package/dist/i18n/request-locale.d.ts +3 -2
- package/dist/i18n/request-locale.js +16 -6
- package/dist/i18n/required-surface-keys.js +23 -35
- package/dist/i18n/settings-hub-keys.js +3 -0
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +150 -59
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +4 -0
- package/dist/migrations/pending-rebuilds.js +23 -1
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +3 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/pipeline/dispatch-query.d.ts +1 -1
- package/dist/pipeline/dispatch-query.js +2 -2
- package/dist/pipeline/dispatch-shared.d.ts +2 -3
- package/dist/pipeline/dispatch-shared.js +14 -9
- package/dist/pipeline/dispatch-stream.js +2 -5
- package/dist/pipeline/dispatch-write.js +22 -5
- package/dist/pipeline/dispatcher.js +1 -2
- package/dist/pipeline/event-dispatcher-admin.js +12 -2
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +26 -11
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/member-read-only-transaction.js +1 -14
- package/dist/pipeline/msp-rebuild.d.ts +1 -0
- package/dist/pipeline/msp-rebuild.js +1 -0
- package/dist/pipeline/system-hooks.d.ts +1 -1
- package/dist/pipeline/system-hooks.js +9 -7
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/redis/pubsub-signal.d.ts +2 -0
- package/dist/redis/pubsub-signal.js +2 -2
- package/dist/schema-cli.js +4 -14
- package/dist/search/meilisearch-adapter.js +17 -6
- package/dist/search/purge-subject.js +8 -3
- package/dist/stack/drain-jobs.js +24 -12
- package/dist/stack/table-helpers.js +10 -8
- package/dist/testing/closed-connection-error.js +2 -3
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +36 -15
- package/package.json +4 -4
- package/src/changes.json +206 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
- package/src/scripts/codemod/migrate-db-raw.ts +16 -8
- package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
- package/src/scripts/codemod/pii-personal-migration.ts +56 -19
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
- package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
- package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
- package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
- package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
- package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
- package/dist/scripts/codemod/migrate-db-raw.js +0 -345
- package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
- package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
- package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
- package/dist/scripts/codemod/pii-personal-migration.js +0 -615
- package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
- package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
|
@@ -193,42 +193,99 @@ function processNewExpression(args: readonly Node[], report: (note: string) => v
|
|
|
193
193
|
return "changed";
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// `failUnprocessable(reason, details)` takes the details literal directly as
|
|
197
|
+
// its second argument (no `{ details }` wrapper like the ctor's opts).
|
|
198
|
+
function processFailUnprocessableCall(
|
|
199
|
+
args: readonly Node[],
|
|
200
|
+
report: (note: string) => void,
|
|
201
|
+
): CallOutcome {
|
|
202
|
+
const detailsArg = args[1];
|
|
203
|
+
if (!detailsArg) return "not-a-match";
|
|
204
|
+
|
|
205
|
+
if (!Node.isObjectLiteralExpression(detailsArg)) {
|
|
206
|
+
report(
|
|
207
|
+
"second argument (details) is not a statically-analyzable object literal — cannot verify details.reason",
|
|
208
|
+
);
|
|
209
|
+
return "skipped";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const removal = removeReasonProperty(detailsArg);
|
|
213
|
+
if (removal.kind === "absent") return "not-a-match";
|
|
214
|
+
if (removal.kind === "skip") {
|
|
215
|
+
report(removal.note);
|
|
216
|
+
return "skipped";
|
|
217
|
+
}
|
|
218
|
+
return "changed";
|
|
219
|
+
}
|
|
220
|
+
|
|
196
221
|
type SkipEntry = { readonly file: string; readonly line: number; readonly note: string };
|
|
197
222
|
|
|
223
|
+
type MigratableCall = {
|
|
224
|
+
readonly exportName: "UnprocessableError" | "failUnprocessable";
|
|
225
|
+
readonly localKind: "class" | "function";
|
|
226
|
+
readonly process: (args: readonly Node[], report: (note: string) => void) => CallOutcome;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const UNPROCESSABLE_ERROR_CTOR: MigratableCall = {
|
|
230
|
+
exportName: "UnprocessableError",
|
|
231
|
+
localKind: "class",
|
|
232
|
+
process: processNewExpression,
|
|
233
|
+
};
|
|
234
|
+
const FAIL_UNPROCESSABLE_CALL: MigratableCall = {
|
|
235
|
+
exportName: "failUnprocessable",
|
|
236
|
+
localKind: "function",
|
|
237
|
+
process: processFailUnprocessableCall,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function migrateCallSite(
|
|
241
|
+
sourceFile: SourceFile,
|
|
242
|
+
importedNames: ReadonlyMap<string, ImportedBinding>,
|
|
243
|
+
site: Node & { getExpression(): Node; getArguments(): Node[] },
|
|
244
|
+
target: MigratableCall,
|
|
245
|
+
skips: SkipEntry[],
|
|
246
|
+
): boolean {
|
|
247
|
+
const callee = site.getExpression();
|
|
248
|
+
if (!Node.isIdentifier(callee)) return false;
|
|
249
|
+
const localName = callee.getText();
|
|
250
|
+
const line = site.getStartLineNumber();
|
|
251
|
+
const file = sourceFile.getFilePath();
|
|
252
|
+
|
|
253
|
+
const binding = importedNames.get(localName);
|
|
254
|
+
if (binding?.importedName !== target.exportName) {
|
|
255
|
+
if (localName !== target.exportName) return false;
|
|
256
|
+
const note = binding
|
|
257
|
+
? `${target.exportName} is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`
|
|
258
|
+
: `${target.exportName} has no @cosmicdrift/kumiko-* import in this file (likely a locally defined ${target.localKind})`;
|
|
259
|
+
skips.push({ file, line, note });
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (!KUMIKO_MODULE_RE.test(binding.moduleSpecifier)) {
|
|
263
|
+
skips.push({
|
|
264
|
+
file,
|
|
265
|
+
line,
|
|
266
|
+
note: `${target.exportName} is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`,
|
|
267
|
+
});
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const outcome = target.process(site.getArguments(), (note) => skips.push({ file, line, note }));
|
|
272
|
+
return outcome === "changed";
|
|
273
|
+
}
|
|
274
|
+
|
|
198
275
|
/** Migrates one file in place. Returns how many `reason` properties were removed. */
|
|
199
276
|
function migrateFile(sourceFile: SourceFile, skips: SkipEntry[]): number {
|
|
200
277
|
const importedNames = collectImportedNames(sourceFile);
|
|
201
278
|
let removedCount = 0;
|
|
202
279
|
|
|
203
280
|
for (const newExpr of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
|
204
|
-
|
|
205
|
-
|
|
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;
|
|
281
|
+
if (migrateCallSite(sourceFile, importedNames, newExpr, UNPROCESSABLE_ERROR_CTOR, skips)) {
|
|
282
|
+
removedCount++;
|
|
218
283
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
note: `UnprocessableError is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`,
|
|
224
|
-
});
|
|
225
|
-
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
286
|
+
if (migrateCallSite(sourceFile, importedNames, call, FAIL_UNPROCESSABLE_CALL, skips)) {
|
|
287
|
+
removedCount++;
|
|
226
288
|
}
|
|
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
289
|
}
|
|
233
290
|
|
|
234
291
|
return removedCount;
|
|
@@ -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;
|