@cosmicdrift/kumiko-framework 0.371.1 → 0.372.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/AGENTS.md +4 -1
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/embedded-entity-form.md +1 -1
- package/agents/examples/pdf-audit-export.md +1 -1
- package/agents/examples/user-data-rights.md +176 -0
- package/dist/api/jwt.js +2 -1
- package/dist/api/observability-middleware.d.ts +1 -2
- package/dist/api/observability-middleware.js +9 -8
- package/dist/api/pii-leak-guard.js +2 -1
- package/dist/api/route-label.d.ts +3 -0
- package/dist/api/route-label.js +17 -0
- package/dist/api/server.d.ts +1 -2
- package/dist/api/server.js +3 -5
- package/dist/config/index.d.ts +5 -0
- package/dist/db/event-store-executor.d.ts +2 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/located-timestamp.d.ts +7 -0
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +8 -2
- package/dist/db/table-builder.d.ts +45 -11
- package/dist/db/tenant-db.js +3 -1
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/factories.d.ts +2 -1
- package/dist/engine/factories.js +2 -0
- package/dist/engine/membership-roles.d.ts +1 -0
- package/dist/engine/membership-roles.js +8 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +30 -2
- package/dist/entrypoint/index.d.ts +1 -0
- package/dist/entrypoint/index.js +0 -2
- package/dist/files/assert-file-refs-belong-to-tenant.js +27 -1
- package/dist/files/create-file-ref-from-bytes.js +12 -6
- package/dist/files/create-file-ref.d.ts +20 -2
- package/dist/files/create-file-ref.js +10 -1
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +25 -0
- package/dist/files/file-handle.d.ts +4 -1
- package/dist/files/file-handle.js +30 -4
- package/dist/files/file-ref-entity.d.ts +7 -7
- package/dist/files/file-ref-table.d.ts +7 -7
- package/dist/files/file-routes.js +6 -1
- package/dist/files/index.d.ts +3 -1
- package/dist/files/index.js +3 -1
- package/dist/files/provider-resolver.js +3 -1
- package/dist/files/sweep-orphaned-uploads.d.ts +16 -0
- package/dist/files/sweep-orphaned-uploads.js +53 -0
- package/dist/files/upload-ledger.d.ts +10 -0
- package/dist/files/upload-ledger.js +47 -0
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/request-locale.d.ts +15 -4
- package/dist/i18n/request-locale.js +26 -9
- package/dist/jobs/job-runner.js +2 -4
- package/dist/pipeline/dispatch-shared.js +2 -4
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/rate-limit/index.d.ts +1 -0
- package/dist/rate-limit/index.js +1 -0
- package/dist/rate-limit/unlimited-resolver.d.ts +2 -0
- package/dist/rate-limit/unlimited-resolver.js +21 -0
- package/dist/schema-cli.js +46 -5
- package/package.json +5 -5
- package/src/changes.json +42 -0
- package/src/scripts/codemod/README.md +22 -0
- package/src/scripts/codemod/remove-engine-payload-casts.ts +318 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// remove-engine-payload-casts — executor.detail()/list() now return the entity's own row type
|
|
3
|
+
// (EntityRow<E>), so casts that used to narrow the untyped Record are redundant. This removes
|
|
4
|
+
// `row as T` / `row["f"] as T` / `(await x.detail(...)) as unknown as T` (and the
|
|
5
|
+
// `@cast-boundary` marker of that cast) where the operand derives from an executor read AND
|
|
6
|
+
// the checker says its type is assignable to T. Casts the row type does not satisfy stay and
|
|
7
|
+
// are listed as manual. A file whose rewrite would add type errors is left untouched.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// bun scripts/codemod/remove-engine-payload-casts.ts <targetDir...> [--check | --dry-run | --write] [--no-format]
|
|
11
|
+
//
|
|
12
|
+
// Needs a tsconfig.json at or above the target: the assignability check runs on the real
|
|
13
|
+
// program, so the framework version the app compiles against decides the row type.
|
|
14
|
+
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import { type AsExpression, Node, Project, type SourceFile, SyntaxKind, ts } from "ts-morph";
|
|
17
|
+
import { type CodemodResult, runCodemodCli } from "./codemod-support.ts";
|
|
18
|
+
|
|
19
|
+
const CAST_MARKER = "@cast-boundary";
|
|
20
|
+
const EXECUTOR_TYPES_FILE = "/event-store-executor-types.ts";
|
|
21
|
+
const READ_VERBS: ReadonlySet<string> = new Set(["detail", "list"]);
|
|
22
|
+
const READ_CALL_PREFILTER = /\.(detail|list)\(/;
|
|
23
|
+
const CAST_PREFILTER = /\bas\s/;
|
|
24
|
+
const MAX_DERIVATION_DEPTH = 8;
|
|
25
|
+
const MAX_TYPE_TEXT_LENGTH = 120;
|
|
26
|
+
|
|
27
|
+
const SKIPPED_PATH_SUFFIXES: readonly string[] = [
|
|
28
|
+
"packages/framework/src/scripts/codemod/remove-engine-payload-casts.ts",
|
|
29
|
+
"packages/framework/src/__tests__/remove-engine-payload-casts-codemod.test.ts",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
interface Edit {
|
|
33
|
+
readonly start: number;
|
|
34
|
+
readonly end: number;
|
|
35
|
+
readonly text: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stripWrappers(node: Node): Node {
|
|
39
|
+
let current = node;
|
|
40
|
+
for (;;) {
|
|
41
|
+
if (Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
42
|
+
current = current.getExpression();
|
|
43
|
+
} else if (Node.isAwaitExpression(current)) {
|
|
44
|
+
current = current.getExpression();
|
|
45
|
+
} else {
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isCastTo(node: Node, keyword: SyntaxKind): boolean {
|
|
52
|
+
return Node.isAsExpression(node) && node.getTypeNode()?.getKind() === keyword;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isExecutorReadCall(node: Node): boolean {
|
|
56
|
+
if (!Node.isCallExpression(node)) return false;
|
|
57
|
+
const callee = node.getExpression();
|
|
58
|
+
if (!Node.isPropertyAccessExpression(callee) || !READ_VERBS.has(callee.getName())) return false;
|
|
59
|
+
const declarations = callee.getNameNode().getSymbol()?.getDeclarations() ?? [];
|
|
60
|
+
return declarations.some((declaration) =>
|
|
61
|
+
declaration.getSourceFile().getFilePath().endsWith(EXECUTOR_TYPES_FILE),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Follows property/element access, `??` / `||` and single-assignment variables back to an
|
|
66
|
+
// executor read. Anything else (parameters, callbacks, reassigned lets) is not recognised.
|
|
67
|
+
function derivesFromExecutorRead(node: Node, depth = 0): boolean {
|
|
68
|
+
if (depth > MAX_DERIVATION_DEPTH) return false;
|
|
69
|
+
const current = stripWrappers(node);
|
|
70
|
+
if (Node.isCallExpression(current)) return isExecutorReadCall(current);
|
|
71
|
+
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) {
|
|
72
|
+
return derivesFromExecutorRead(current.getExpression(), depth + 1);
|
|
73
|
+
}
|
|
74
|
+
if (Node.isBinaryExpression(current)) {
|
|
75
|
+
const operator = current.getOperatorToken().getKind();
|
|
76
|
+
return (
|
|
77
|
+
(operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) &&
|
|
78
|
+
derivesFromExecutorRead(current.getLeft(), depth + 1)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (Node.isIdentifier(current)) {
|
|
82
|
+
const declarations = current.getDefinitionNodes();
|
|
83
|
+
const declaration = declarations.length === 1 ? declarations[0] : undefined;
|
|
84
|
+
if (declaration === undefined || !Node.isVariableDeclaration(declaration)) return false;
|
|
85
|
+
const initializer = declaration.getInitializer();
|
|
86
|
+
return initializer !== undefined && derivesFromExecutorRead(initializer, depth + 1);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function innermostOperand(cast: AsExpression): Node {
|
|
92
|
+
let operand = cast.getExpression();
|
|
93
|
+
while (isCastTo(stripParenthesesOnly(operand), SyntaxKind.UnknownKeyword)) {
|
|
94
|
+
const inner = stripParenthesesOnly(operand);
|
|
95
|
+
if (!Node.isAsExpression(inner)) break;
|
|
96
|
+
operand = inner.getExpression();
|
|
97
|
+
}
|
|
98
|
+
return operand;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function stripParenthesesOnly(node: Node): Node {
|
|
102
|
+
let current = node;
|
|
103
|
+
while (Node.isParenthesizedExpression(current)) current = current.getExpression();
|
|
104
|
+
return current;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function enclosingStatement(node: Node): Node | undefined {
|
|
108
|
+
return node.getFirstAncestor((ancestor) => {
|
|
109
|
+
const parent = ancestor.getParent();
|
|
110
|
+
return (
|
|
111
|
+
parent !== undefined &&
|
|
112
|
+
(Node.isBlock(parent) ||
|
|
113
|
+
Node.isSourceFile(parent) ||
|
|
114
|
+
Node.isModuleBlock(parent) ||
|
|
115
|
+
Node.isCaseClause(parent) ||
|
|
116
|
+
Node.isDefaultClause(parent))
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isRemovableCastKind(cast: AsExpression): boolean {
|
|
122
|
+
return !isCastTo(cast, SyntaxKind.ConstKeyword);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function remainingCasts(statement: Node, removed: ReadonlySet<Node>): number {
|
|
126
|
+
return statement
|
|
127
|
+
.getDescendantsOfKind(SyntaxKind.AsExpression)
|
|
128
|
+
.filter((candidate) => isRemovableCastKind(candidate) && !removed.has(candidate)).length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function lineEnd(text: string, from: number): number {
|
|
132
|
+
const newline = text.indexOf("\n", from);
|
|
133
|
+
return newline === -1 ? text.length : newline;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The marker sits as trailing line comment of the cast's line or of the statement's last line,
|
|
137
|
+
// or as its own comment line(s) directly above the statement.
|
|
138
|
+
function markerEdits(source: string, statement: Node, cast: AsExpression): Edit[] {
|
|
139
|
+
const edits: Edit[] = [];
|
|
140
|
+
for (const from of new Set([cast.getEnd(), statement.getEnd()])) {
|
|
141
|
+
const end = lineEnd(source, from);
|
|
142
|
+
const rest = source.slice(from, end);
|
|
143
|
+
const commentStart = rest.indexOf("//");
|
|
144
|
+
if (commentStart === -1 || !rest.slice(commentStart).includes(CAST_MARKER)) continue;
|
|
145
|
+
const start = from + commentStart;
|
|
146
|
+
const spaces = /[ \t]*$/.exec(source.slice(0, start))?.[0].length ?? 0;
|
|
147
|
+
edits.push({ start: start - spaces, end, text: "" });
|
|
148
|
+
}
|
|
149
|
+
for (const range of statement.getLeadingCommentRanges()) {
|
|
150
|
+
if (!range.getText().includes(CAST_MARKER)) continue;
|
|
151
|
+
const start = source.lastIndexOf("\n", range.getPos() - 1) + 1;
|
|
152
|
+
const end = source[range.getEnd()] === "\n" ? range.getEnd() + 1 : range.getEnd();
|
|
153
|
+
edits.push({ start, end, text: "" });
|
|
154
|
+
}
|
|
155
|
+
return edits;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function shortTypeText(text: string): string {
|
|
159
|
+
const oneLine = text.replace(/\s+/g, " ");
|
|
160
|
+
return oneLine.length <= MAX_TYPE_TEXT_LENGTH
|
|
161
|
+
? oneLine
|
|
162
|
+
: `${oneLine.slice(0, MAX_TYPE_TEXT_LENGTH)}...`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyEdits(source: string, edits: readonly Edit[]): string {
|
|
166
|
+
const unique = new Map<number, Edit>();
|
|
167
|
+
for (const edit of edits) unique.set(edit.start, edit);
|
|
168
|
+
let output = source;
|
|
169
|
+
for (const edit of [...unique.values()].sort((a, b) => b.start - a.start)) {
|
|
170
|
+
output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
171
|
+
}
|
|
172
|
+
return output;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function diagnosticCount(sourceFile: SourceFile): number {
|
|
176
|
+
return sourceFile.getProject().getProgram().getSemanticDiagnostics(sourceFile).length;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function addsTypeErrors(sourceFile: SourceFile, source: string, output: string): boolean {
|
|
180
|
+
const errorsBefore = diagnosticCount(sourceFile);
|
|
181
|
+
sourceFile.replaceWithText(output);
|
|
182
|
+
const errorsAfter = diagnosticCount(sourceFile);
|
|
183
|
+
sourceFile.replaceWithText(source);
|
|
184
|
+
return errorsAfter > errorsBefore;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function markerEditsForTouched(
|
|
188
|
+
source: string,
|
|
189
|
+
touchedStatements: ReadonlyMap<Node, readonly AsExpression[]>,
|
|
190
|
+
removed: ReadonlySet<Node>,
|
|
191
|
+
): Edit[] {
|
|
192
|
+
const edits: Edit[] = [];
|
|
193
|
+
for (const [statement, casts] of touchedStatements) {
|
|
194
|
+
const [first] = casts;
|
|
195
|
+
if (first !== undefined && remainingCasts(statement, removed) === 0) {
|
|
196
|
+
edits.push(...markerEdits(source, statement, first));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return edits;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function skippedForTypeErrors(
|
|
203
|
+
source: string,
|
|
204
|
+
rewrites: CodemodResult["rewrites"],
|
|
205
|
+
manual: CodemodResult["rewrites"],
|
|
206
|
+
): CodemodResult {
|
|
207
|
+
return {
|
|
208
|
+
output: source,
|
|
209
|
+
rewrites: [],
|
|
210
|
+
manual: [
|
|
211
|
+
...manual,
|
|
212
|
+
...rewrites.map((site) => ({
|
|
213
|
+
...site,
|
|
214
|
+
description: `${site.description} — skipped: removing it adds type errors in this file`,
|
|
215
|
+
})),
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function transformEnginePayloadCastsFile(
|
|
221
|
+
sourceFile: SourceFile,
|
|
222
|
+
filePath: string,
|
|
223
|
+
): CodemodResult {
|
|
224
|
+
const source = sourceFile.getFullText();
|
|
225
|
+
const result: CodemodResult = { output: source, rewrites: [], manual: [] };
|
|
226
|
+
const rewrites = [...result.rewrites];
|
|
227
|
+
const manual = [...result.manual];
|
|
228
|
+
const edits: Edit[] = [];
|
|
229
|
+
const removed = new Set<Node>();
|
|
230
|
+
const touchedStatements = new Map<Node, AsExpression[]>();
|
|
231
|
+
|
|
232
|
+
for (const cast of sourceFile.getDescendantsOfKind(SyntaxKind.AsExpression)) {
|
|
233
|
+
const typeNode = cast.getTypeNode();
|
|
234
|
+
if (typeNode === undefined || !isRemovableCastKind(cast)) continue;
|
|
235
|
+
if (isCastTo(cast, SyntaxKind.UnknownKeyword)) continue;
|
|
236
|
+
const operand = innermostOperand(cast);
|
|
237
|
+
if (!derivesFromExecutorRead(operand)) continue;
|
|
238
|
+
const line = cast.getStartLineNumber();
|
|
239
|
+
const operandType = operand.getType();
|
|
240
|
+
if (!operandType.isAssignableTo(typeNode.getType())) {
|
|
241
|
+
manual.push({
|
|
242
|
+
file: filePath,
|
|
243
|
+
line,
|
|
244
|
+
description: `cast to ${shortTypeText(typeNode.getText())} kept: ${shortTypeText(operandType.getText(cast))} is not assignable to it`,
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
edits.push({ start: cast.getStart(), end: cast.getEnd(), text: operand.getText() });
|
|
249
|
+
removed.add(cast);
|
|
250
|
+
// The inner `as unknown` of a double cast disappears with the outer cast.
|
|
251
|
+
for (const inner of cast.getDescendantsOfKind(SyntaxKind.AsExpression)) removed.add(inner);
|
|
252
|
+
const statement = enclosingStatement(cast);
|
|
253
|
+
if (statement !== undefined) {
|
|
254
|
+
touchedStatements.set(statement, [...(touchedStatements.get(statement) ?? []), cast]);
|
|
255
|
+
}
|
|
256
|
+
rewrites.push({
|
|
257
|
+
file: filePath,
|
|
258
|
+
line,
|
|
259
|
+
description: `removed cast to ${shortTypeText(typeNode.getText())}, the executor row type already satisfies it`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
edits.push(...markerEditsForTouched(source, touchedStatements, removed));
|
|
264
|
+
|
|
265
|
+
if (edits.length === 0) return { output: source, rewrites, manual };
|
|
266
|
+
|
|
267
|
+
const output = applyEdits(source, edits);
|
|
268
|
+
if (addsTypeErrors(sourceFile, source, output)) {
|
|
269
|
+
return skippedForTypeErrors(source, rewrites, manual);
|
|
270
|
+
}
|
|
271
|
+
return { output, rewrites, manual };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const projectsByTsconfig = new Map<string, Project>();
|
|
275
|
+
|
|
276
|
+
function projectFor(filePath: string): Project | undefined {
|
|
277
|
+
const tsconfig = ts.findConfigFile(dirname(resolve(filePath)), ts.sys.fileExists);
|
|
278
|
+
if (tsconfig === undefined) return undefined;
|
|
279
|
+
const cached = projectsByTsconfig.get(tsconfig);
|
|
280
|
+
if (cached !== undefined) return cached;
|
|
281
|
+
const project = new Project({ tsConfigFilePath: tsconfig, skipAddingFilesFromTsConfig: true });
|
|
282
|
+
projectsByTsconfig.set(tsconfig, project);
|
|
283
|
+
return project;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// `project` decides which program answers the assignability question; without one the tsconfig
|
|
287
|
+
// next to the file is used. A file without any tsconfig is reported as manual, not guessed at.
|
|
288
|
+
export function transformEnginePayloadCastsSource(
|
|
289
|
+
source: string,
|
|
290
|
+
filePath = "source.ts",
|
|
291
|
+
project?: Project,
|
|
292
|
+
): CodemodResult {
|
|
293
|
+
const unchanged: CodemodResult = { output: source, rewrites: [], manual: [] };
|
|
294
|
+
if (!READ_CALL_PREFILTER.test(source) || !CAST_PREFILTER.test(source)) return unchanged;
|
|
295
|
+
const absolutePath = resolve(filePath);
|
|
296
|
+
const program = project ?? projectFor(absolutePath);
|
|
297
|
+
if (program === undefined) {
|
|
298
|
+
return {
|
|
299
|
+
...unchanged,
|
|
300
|
+
manual: [
|
|
301
|
+
{ file: filePath, line: 1, description: "no tsconfig.json found, cannot type-check" },
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const sourceFile = program.createSourceFile(absolutePath, source, { overwrite: true });
|
|
306
|
+
return transformEnginePayloadCastsFile(sourceFile, filePath);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (import.meta.main) {
|
|
310
|
+
await runCodemodCli(
|
|
311
|
+
{
|
|
312
|
+
name: "remove-engine-payload-casts",
|
|
313
|
+
transform: (source, filePath) => transformEnginePayloadCastsSource(source, filePath),
|
|
314
|
+
isSkippedPath: (file) => SKIPPED_PATH_SUFFIXES.some((suffix) => file.endsWith(suffix)),
|
|
315
|
+
},
|
|
316
|
+
process.argv.slice(2),
|
|
317
|
+
);
|
|
318
|
+
}
|