@cosmicdrift/kumiko-framework 0.146.4 → 0.147.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/package.json +6 -2
- package/src/api/auth-routes.ts +32 -67
- package/src/api/routes.ts +1 -3
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +29 -3
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/event-store-executor-context.ts +398 -0
- package/src/db/event-store-executor-read.ts +276 -0
- package/src/db/event-store-executor-write.ts +615 -0
- package/src/db/event-store-executor.ts +29 -1166
- package/src/db/queries/shadow-swap.ts +3 -1
- package/src/db/schema-inspection.ts +1 -1
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +10 -0
- package/src/engine/__tests__/engine.test.ts +45 -1
- package/src/engine/__tests__/event-migration-declarative.test.ts +6 -0
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/screen.test.ts +26 -0
- package/src/engine/boot-validator/entity-list-screens.ts +1 -1
- package/src/engine/boot-validator/gdpr-storage.ts +1 -1
- package/src/engine/boot-validator/index.ts +12 -8
- package/src/engine/boot-validator/nav.ts +120 -0
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +12 -190
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/define-feature.ts +77 -1011
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +291 -2
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +73 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round4.ts +45 -10
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/parse.ts +123 -10
- package/src/engine/feature-ast/patterns.ts +14 -7
- package/src/engine/feature-ast/render.ts +28 -7
- package/src/engine/feature-builder-state.ts +165 -0
- package/src/engine/feature-config-events-jobs.ts +303 -0
- package/src/engine/feature-entity-handlers.ts +161 -0
- package/src/engine/feature-ui-extensions.ts +413 -0
- package/src/engine/index.ts +0 -2
- package/src/engine/pattern-library/library.ts +44 -1131
- package/src/engine/pattern-library/mixed-schemas.ts +484 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +80 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/registry-facade.ts +349 -0
- package/src/engine/registry-ingest.ts +473 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +660 -0
- package/src/engine/registry.ts +79 -1695
- package/src/engine/types/screen.ts +4 -2
- package/src/engine/types/tree-node.ts +2 -5
- package/src/event-store/snapshot.ts +7 -7
- package/src/i18n/required-surface-keys.ts +2 -0
- package/src/jobs/job-runner.ts +1 -1
- package/src/observability/standard-metrics.ts +4 -3
- package/src/pipeline/dispatch-batch.ts +3 -3
- package/src/pipeline/dispatch-shared.ts +17 -10
- package/src/pipeline/event-dispatcher-admin.ts +289 -0
- package/src/pipeline/event-dispatcher-delivery.ts +264 -0
- package/src/pipeline/event-dispatcher.ts +28 -540
- package/src/pipeline/projection-rebuild.ts +1 -1
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +95 -0
- package/src/stack/test-stack.ts +46 -1
- package/src/testing/boot-validator-fixture.ts +1 -2
- package/src/engine/__tests__/deep-link.test.ts +0 -35
- package/src/engine/deep-link.ts +0 -23
|
@@ -47,6 +47,25 @@ export function readBooleanProperty(
|
|
|
47
47
|
return undefined;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Marks a value the parser could not resolve into a literal (an Identifier
|
|
52
|
+
* reference, a call expression, ...) while preserving its exact source text.
|
|
53
|
+
* `renderValue` re-emits `__raw` verbatim instead of re-serializing the
|
|
54
|
+
* value, so round-tripping a reference like `eventEntity` never expands it
|
|
55
|
+
* into an inlined object literal.
|
|
56
|
+
*/
|
|
57
|
+
export type RawRefSentinel = { readonly __raw: string };
|
|
58
|
+
|
|
59
|
+
export function isRawRefSentinel(value: unknown): value is RawRefSentinel {
|
|
60
|
+
return (
|
|
61
|
+
value !== null &&
|
|
62
|
+
typeof value === "object" &&
|
|
63
|
+
!Array.isArray(value) &&
|
|
64
|
+
"__raw" in value &&
|
|
65
|
+
typeof (value as { __raw: unknown }).__raw === "string"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
export function readDataLiteralNode(node: Node): unknown {
|
|
51
70
|
const kind = node.getKind();
|
|
52
71
|
switch (kind) {
|
|
@@ -103,11 +122,50 @@ export function readDataLiteralNode(node: Node): unknown {
|
|
|
103
122
|
return readDataLiteralNode(
|
|
104
123
|
node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression(),
|
|
105
124
|
);
|
|
125
|
+
case SyntaxKind.Identifier:
|
|
126
|
+
case SyntaxKind.CallExpression:
|
|
127
|
+
case SyntaxKind.PropertyAccessExpression:
|
|
128
|
+
// Unresolvable reference (const identifier, factory call, member access).
|
|
129
|
+
// Keep the exact source text so renderValue can re-emit it verbatim —
|
|
130
|
+
// resolving/inlining it would silently rewrite the source on the next
|
|
131
|
+
// parse->render roundtrip (see readDataLiteralNode doc above).
|
|
132
|
+
return { __raw: node.getText() } satisfies RawRefSentinel;
|
|
106
133
|
default:
|
|
107
134
|
return undefined;
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
|
|
138
|
+
/**
|
|
139
|
+
* A node's literal string value, or the raw-ref sentinel when it resolves to
|
|
140
|
+
* an unresolvable identifier/factory-call/member-access (see
|
|
141
|
+
* readDataLiteralNode). undefined for anything else (number, boolean, ...).
|
|
142
|
+
*/
|
|
143
|
+
export function readStringOrRaw(node: Node): string | RawRefSentinel | undefined {
|
|
144
|
+
const value = readDataLiteralNode(node);
|
|
145
|
+
if (typeof value === "string") return value;
|
|
146
|
+
if (isRawRefSentinel(value)) return value;
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Like readStringLiteralArgs, but an argument that resolves to a raw-ref
|
|
152
|
+
* sentinel is kept as that sentinel instead of failing the whole call. Used
|
|
153
|
+
* where a value stands in for a single name/key and inlining its resolved
|
|
154
|
+
* string would corrupt the render roundtrip (e.g. `r.requires(someFeature.name)`
|
|
155
|
+
* — see #1009).
|
|
156
|
+
*/
|
|
157
|
+
export function readStringOrRawArgs(
|
|
158
|
+
call: CallExpression,
|
|
159
|
+
): readonly (string | RawRefSentinel)[] | undefined {
|
|
160
|
+
const out: (string | RawRefSentinel)[] = [];
|
|
161
|
+
for (const arg of call.getArguments()) {
|
|
162
|
+
const value = readStringOrRaw(arg);
|
|
163
|
+
if (value === undefined) return undefined;
|
|
164
|
+
out.push(value);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
111
169
|
export { isPlainObject } from "../../../utils/is-plain-object";
|
|
112
170
|
|
|
113
171
|
export function readPropertyKey(propAssign: import("ts-morph").PropertyAssignment): string {
|
|
@@ -150,7 +208,7 @@ export function readNameOrRefOrList(node: Node): string | readonly string[] | un
|
|
|
150
208
|
export function readVarargsOrArrayProp(
|
|
151
209
|
call: CallExpression,
|
|
152
210
|
arrayPropName: "features" | "keys",
|
|
153
|
-
): readonly string[] | undefined {
|
|
211
|
+
): readonly (string | RawRefSentinel)[] | undefined {
|
|
154
212
|
const args = call.getArguments();
|
|
155
213
|
if (args.length === 1) {
|
|
156
214
|
const obj = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
@@ -162,15 +220,15 @@ export function readVarargsOrArrayProp(
|
|
|
162
220
|
if (propInit) {
|
|
163
221
|
const arr = propInit.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
164
222
|
if (!arr) return undefined;
|
|
165
|
-
const out: string[] = [];
|
|
223
|
+
const out: (string | RawRefSentinel)[] = [];
|
|
166
224
|
for (const el of arr.getElements()) {
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
out.push(
|
|
225
|
+
const value = readStringOrRaw(el);
|
|
226
|
+
if (value === undefined) return undefined;
|
|
227
|
+
out.push(value);
|
|
170
228
|
}
|
|
171
229
|
return out;
|
|
172
230
|
}
|
|
173
231
|
}
|
|
174
232
|
}
|
|
175
|
-
return
|
|
233
|
+
return readStringOrRawArgs(call);
|
|
176
234
|
}
|
|
@@ -21,7 +21,13 @@
|
|
|
21
21
|
// Per-pattern extractors fill in iteratively (C1.5) — each round adds
|
|
22
22
|
// one extractor + a focused test.
|
|
23
23
|
|
|
24
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
ArrowFunction,
|
|
26
|
+
CallExpression,
|
|
27
|
+
Node,
|
|
28
|
+
ParameterDeclaration,
|
|
29
|
+
SourceFile,
|
|
30
|
+
} from "ts-morph";
|
|
25
31
|
import { Project, SyntaxKind } from "ts-morph";
|
|
26
32
|
|
|
27
33
|
import {
|
|
@@ -63,6 +69,7 @@ import {
|
|
|
63
69
|
extractUsesApi,
|
|
64
70
|
extractWorkspace,
|
|
65
71
|
extractWriteHandler,
|
|
72
|
+
findFunctionLiteral,
|
|
66
73
|
} from "./extractors";
|
|
67
74
|
import type { FeaturePattern, UnknownPattern } from "./patterns";
|
|
68
75
|
import { type SourceLocation, sourceLocationFromNode } from "./source-location";
|
|
@@ -145,7 +152,7 @@ export function parseSourceFile(sourceFile: SourceFile): ParseResult {
|
|
|
145
152
|
const patterns: FeaturePattern[] = [];
|
|
146
153
|
const errors: ParseError[] = [];
|
|
147
154
|
|
|
148
|
-
walkSetupCallback(setupCallback, registrarParamName, sourceFile, patterns, errors);
|
|
155
|
+
walkSetupCallback(setupCallback.getBody(), registrarParamName, sourceFile, patterns, errors);
|
|
149
156
|
|
|
150
157
|
return { featureName, patterns, errors };
|
|
151
158
|
}
|
|
@@ -228,25 +235,131 @@ function extractRegistrarParamName(setup: ArrowFunction): string | undefined {
|
|
|
228
235
|
* structurally impossible.
|
|
229
236
|
*/
|
|
230
237
|
function walkSetupCallback(
|
|
231
|
-
|
|
238
|
+
body: Node,
|
|
232
239
|
registrarParamName: string,
|
|
233
240
|
sourceFile: SourceFile,
|
|
234
241
|
patterns: FeaturePattern[],
|
|
235
242
|
errors: ParseError[],
|
|
243
|
+
visitedWrapperDecls: Set<Node> = new Set(),
|
|
236
244
|
): void {
|
|
237
|
-
const body = setup.getBody();
|
|
238
245
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
239
246
|
const methodName = extractRegistrarMethodName(call, registrarParamName);
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
if (methodName) {
|
|
248
|
+
const result = dispatchExtractor(methodName, call, sourceFile);
|
|
249
|
+
if (result.kind === "pattern") {
|
|
250
|
+
patterns.push(result.pattern);
|
|
251
|
+
} else {
|
|
252
|
+
errors.push(result.error);
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
246
255
|
}
|
|
256
|
+
|
|
257
|
+
// Not a direct `<registrarParam>.<method>(...)` call. Check whether it's
|
|
258
|
+
// a registrar-wrapper call — a function that receives the registrar as
|
|
259
|
+
// a bare argument, e.g. `registerShowPonyScreens(r)`. Such wrappers are
|
|
260
|
+
// invisible to a receiver-shape match, so we resolve the callee (same
|
|
261
|
+
// file, or the declaring file of a named import — #1008) and recurse
|
|
262
|
+
// into its body with its own parameter name for the registrar slot.
|
|
263
|
+
const wrapper = resolveRegistrarWrapperCall(call, registrarParamName);
|
|
264
|
+
if (!wrapper) continue; // Free function call unrelated to the registrar — ignore.
|
|
265
|
+
if (visitedWrapperDecls.has(wrapper.declNode)) continue; // Cycle guard.
|
|
266
|
+
visitedWrapperDecls.add(wrapper.declNode);
|
|
267
|
+
// wrapper.sourceFile, not the outer `sourceFile` param: a cross-file
|
|
268
|
+
// wrapper's body lives in a different file, and sourceLocationFromNode
|
|
269
|
+
// uses whichever SourceFile it's given for both the `file` path and
|
|
270
|
+
// the line/column table — passing the wrong one silently corrupts
|
|
271
|
+
// both for every pattern found inside the wrapper.
|
|
272
|
+
walkSetupCallback(
|
|
273
|
+
wrapper.body,
|
|
274
|
+
wrapper.paramName,
|
|
275
|
+
wrapper.sourceFile,
|
|
276
|
+
patterns,
|
|
277
|
+
errors,
|
|
278
|
+
visitedWrapperDecls,
|
|
279
|
+
);
|
|
247
280
|
}
|
|
248
281
|
}
|
|
249
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Resolves a bare call like `registerShowPonyScreens(r)` to the callee's
|
|
285
|
+
* body + its own parameter name for the registrar slot. The callee may be
|
|
286
|
+
* declared in the same file (function declaration, or a const initialized
|
|
287
|
+
* with an arrow/function expression), or imported by name from another
|
|
288
|
+
* file — resolved via the module specifier's SourceFile (#1008; only
|
|
289
|
+
* works against a real-filesystem Project, e.g. parseFeatureFile's. The
|
|
290
|
+
* in-memory Designer Project has no files to resolve against and
|
|
291
|
+
* getModuleSpecifierSourceFile() returns undefined there, same as any
|
|
292
|
+
* other unresolvable reference).
|
|
293
|
+
*/
|
|
294
|
+
function resolveRegistrarWrapperCall(
|
|
295
|
+
call: CallExpression,
|
|
296
|
+
registrarParamName: string,
|
|
297
|
+
):
|
|
298
|
+
| {
|
|
299
|
+
readonly body: Node;
|
|
300
|
+
readonly paramName: string;
|
|
301
|
+
readonly declNode: Node;
|
|
302
|
+
readonly sourceFile: SourceFile;
|
|
303
|
+
}
|
|
304
|
+
| undefined {
|
|
305
|
+
const callee = call.getExpression().asKind(SyntaxKind.Identifier);
|
|
306
|
+
if (!callee) return undefined;
|
|
307
|
+
|
|
308
|
+
const args = call.getArguments();
|
|
309
|
+
const argIndex = args.findIndex((arg) => arg.getText() === registrarParamName);
|
|
310
|
+
if (argIndex === -1) return undefined; // Registrar not passed to this call.
|
|
311
|
+
|
|
312
|
+
const callSourceFile = call.getSourceFile();
|
|
313
|
+
const name = callee.getText();
|
|
314
|
+
|
|
315
|
+
const local = resolveWrapperDeclarationInFile(callSourceFile, name, argIndex);
|
|
316
|
+
if (local) return { ...local, sourceFile: callSourceFile };
|
|
317
|
+
|
|
318
|
+
const importDecl = callSourceFile
|
|
319
|
+
.getImportDeclarations()
|
|
320
|
+
.find((d) =>
|
|
321
|
+
d.getNamedImports().some((ni) => (ni.getAliasNode()?.getText() ?? ni.getName()) === name),
|
|
322
|
+
);
|
|
323
|
+
if (!importDecl) return undefined;
|
|
324
|
+
const exportedName =
|
|
325
|
+
importDecl
|
|
326
|
+
.getNamedImports()
|
|
327
|
+
.find((ni) => (ni.getAliasNode()?.getText() ?? ni.getName()) === name)
|
|
328
|
+
?.getName() ?? name;
|
|
329
|
+
const targetFile = importDecl.getModuleSpecifierSourceFile();
|
|
330
|
+
if (!targetFile) return undefined; // Unresolvable: in-memory project, external package, or missing file.
|
|
331
|
+
|
|
332
|
+
const imported = resolveWrapperDeclarationInFile(targetFile, exportedName, argIndex);
|
|
333
|
+
return imported ? { ...imported, sourceFile: targetFile } : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function resolveWrapperDeclarationInFile(
|
|
337
|
+
sourceFile: SourceFile,
|
|
338
|
+
name: string,
|
|
339
|
+
argIndex: number,
|
|
340
|
+
): { readonly body: Node; readonly paramName: string; readonly declNode: Node } | undefined {
|
|
341
|
+
const fnDecl = sourceFile.getFunction(name);
|
|
342
|
+
if (fnDecl) {
|
|
343
|
+
const param = fnDecl.getParameters()[argIndex];
|
|
344
|
+
const body = fnDecl.getBody();
|
|
345
|
+
if (!param || !body) return undefined;
|
|
346
|
+
return { body, paramName: param.getName(), declNode: fnDecl };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const varDecl = sourceFile.getVariableDeclaration(name);
|
|
350
|
+
const init = varDecl?.getInitializer();
|
|
351
|
+
const literal = init ? findFunctionLiteral(init) : undefined;
|
|
352
|
+
const fnLiteral =
|
|
353
|
+
literal?.asKind(SyntaxKind.ArrowFunction) ?? literal?.asKind(SyntaxKind.FunctionExpression);
|
|
354
|
+
if (fnLiteral) {
|
|
355
|
+
const param = fnLiteral.getParameters()[argIndex];
|
|
356
|
+
if (!param) return undefined;
|
|
357
|
+
return { body: fnLiteral.getBody(), paramName: param.getName(), declNode: fnLiteral };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
250
363
|
/**
|
|
251
364
|
* Returns the method name when the call has the form
|
|
252
365
|
* `<registrarParamName>.<method>(...)`, otherwise undefined.
|
|
@@ -341,14 +341,20 @@ export type ScreenPattern = {
|
|
|
341
341
|
export type WriteHandlerPattern = {
|
|
342
342
|
readonly kind: "writeHandler";
|
|
343
343
|
readonly source: SourceLocation;
|
|
344
|
-
|
|
344
|
+
// handlerName/schemaSource/handlerBody are set together, or all left
|
|
345
|
+
// undefined for a single reference standing in for the whole handler
|
|
346
|
+
// (`r.writeHandler(handlerRef)`) that couldn't be resolved to an object
|
|
347
|
+
// literal — see #1007. An opaque pattern re-emits `source.raw` verbatim
|
|
348
|
+
// and, like UnknownPattern, has no PatternId variant (not individually
|
|
349
|
+
// patchable).
|
|
350
|
+
readonly handlerName?: string;
|
|
345
351
|
// schemaSource: the Zod call as source text (e.g. "z.object({...})").
|
|
346
352
|
// We keep it as an opaque block instead of decoding it back to JSON
|
|
347
353
|
// schema — Zod's full surface is too rich for a faithful round-trip.
|
|
348
|
-
readonly schemaSource
|
|
354
|
+
readonly schemaSource?: SourceLocation;
|
|
349
355
|
// handlerBody: the closure body as source text. Always opaque — AI
|
|
350
356
|
// generates raw TypeScript, no DSL interpretation.
|
|
351
|
-
readonly handlerBody
|
|
357
|
+
readonly handlerBody?: SourceLocation;
|
|
352
358
|
readonly access?: AccessRule;
|
|
353
359
|
readonly rateLimit?: RateLimitOption;
|
|
354
360
|
readonly unsafeSkipTransitionGuard?: boolean;
|
|
@@ -356,13 +362,14 @@ export type WriteHandlerPattern = {
|
|
|
356
362
|
|
|
357
363
|
// `r.queryHandler(...)` — registers a read handler: name, Zod input schema,
|
|
358
364
|
// handler closure, plus optional `access` and `rateLimit` rules. Read-side
|
|
359
|
-
// counterpart of `r.writeHandler` with the same header/body split.
|
|
365
|
+
// counterpart of `r.writeHandler` with the same header/body split. Opaque
|
|
366
|
+
// (no handlerName) for the same single-reference case as WriteHandlerPattern.
|
|
360
367
|
export type QueryHandlerPattern = {
|
|
361
368
|
readonly kind: "queryHandler";
|
|
362
369
|
readonly source: SourceLocation;
|
|
363
|
-
readonly handlerName
|
|
364
|
-
readonly schemaSource
|
|
365
|
-
readonly handlerBody
|
|
370
|
+
readonly handlerName?: string;
|
|
371
|
+
readonly schemaSource?: SourceLocation;
|
|
372
|
+
readonly handlerBody?: SourceLocation;
|
|
366
373
|
readonly access?: AccessRule;
|
|
367
374
|
readonly rateLimit?: RateLimitOption;
|
|
368
375
|
};
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// `// kumiko-feature-version: 1`. Future format bumps run a dedicated
|
|
17
17
|
// migrator over the version comment.
|
|
18
18
|
|
|
19
|
+
import { isRawRefSentinel } from "./extractors/shared";
|
|
19
20
|
import type {
|
|
20
21
|
AuthClaimsPattern,
|
|
21
22
|
ClaimKeyPattern,
|
|
@@ -174,6 +175,7 @@ const SINGLE_LINE_WIDTH = 80;
|
|
|
174
175
|
* multi-line otherwise — biome-stable in both branches.
|
|
175
176
|
*/
|
|
176
177
|
export function renderValue(value: unknown, indent = 0): string {
|
|
178
|
+
if (isRawRefSentinel(value)) return value.__raw;
|
|
177
179
|
if (value === null) return "null";
|
|
178
180
|
if (typeof value === "string") return JSON.stringify(value);
|
|
179
181
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
@@ -240,6 +242,13 @@ function renderDescribe(p: DescribePattern): string {
|
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
function renderEntity(p: EntityPattern): string {
|
|
245
|
+
// A whole-value raw-ref (`r.entity("x", eventEntity)`) can't be spread
|
|
246
|
+
// into the merged Object-Form without losing `name` to the sentinel
|
|
247
|
+
// shortcut in renderValue — fall back to the classic positional form,
|
|
248
|
+
// which keeps the reference verbatim alongside the name.
|
|
249
|
+
if (isRawRefSentinel(p.definition)) {
|
|
250
|
+
return `r.entity(${JSON.stringify(p.entityName)}, ${p.definition.__raw});`;
|
|
251
|
+
}
|
|
243
252
|
// Inline `name` into the definition object — canonical Object-Form
|
|
244
253
|
// is a single arg with name-as-property.
|
|
245
254
|
const merged = { name: p.entityName, ...p.definition };
|
|
@@ -247,6 +256,9 @@ function renderEntity(p: EntityPattern): string {
|
|
|
247
256
|
}
|
|
248
257
|
|
|
249
258
|
function renderRelation(p: RelationPattern): string {
|
|
259
|
+
if (isRawRefSentinel(p.definition)) {
|
|
260
|
+
return `r.relation(${JSON.stringify(p.entityName)}, ${JSON.stringify(p.relationName)}, ${p.definition.__raw});`;
|
|
261
|
+
}
|
|
250
262
|
const merged = { entity: p.entityName, name: p.relationName, ...p.definition };
|
|
251
263
|
return `r.relation(${renderValue(merged)});`;
|
|
252
264
|
}
|
|
@@ -268,11 +280,17 @@ function renderTranslations(p: TranslationsPattern): string {
|
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
function renderMetric(p: MetricPattern): string {
|
|
283
|
+
if (isRawRefSentinel(p.options)) {
|
|
284
|
+
return `r.metric(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
|
|
285
|
+
}
|
|
271
286
|
const merged = { name: p.shortName, ...p.options };
|
|
272
287
|
return `r.metric(${renderValue(merged)});`;
|
|
273
288
|
}
|
|
274
289
|
|
|
275
290
|
function renderSecret(p: SecretPattern): string {
|
|
291
|
+
if (isRawRefSentinel(p.options)) {
|
|
292
|
+
return `r.secret(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
|
|
293
|
+
}
|
|
276
294
|
const merged = { name: p.shortName, ...p.options };
|
|
277
295
|
return `r.secret(${renderValue(merged)});`;
|
|
278
296
|
}
|
|
@@ -291,6 +309,9 @@ function renderReferenceData(p: ReferenceDataPattern): string {
|
|
|
291
309
|
}
|
|
292
310
|
|
|
293
311
|
function renderUseExtension(p: UseExtensionPattern): string {
|
|
312
|
+
if (isRawRefSentinel(p.options)) {
|
|
313
|
+
return `r.useExtension(${JSON.stringify(p.extensionName)}, ${JSON.stringify(p.entityName)}, ${p.options.__raw});`;
|
|
314
|
+
}
|
|
294
315
|
const merged: Record<string, unknown> = {
|
|
295
316
|
name: p.extensionName,
|
|
296
317
|
entity: p.entityName,
|
|
@@ -356,10 +377,11 @@ function reindentBody(raw: string, newIndent: string): string {
|
|
|
356
377
|
}
|
|
357
378
|
|
|
358
379
|
function renderWriteHandler(p: WriteHandlerPattern): string {
|
|
380
|
+
if (p.handlerName === undefined) return p.source.raw;
|
|
359
381
|
const lines: string[] = ["r.writeHandler({"];
|
|
360
382
|
lines.push(` name: ${JSON.stringify(p.handlerName)},`);
|
|
361
|
-
lines.push(` schema: ${reindentBody(p.schemaSource
|
|
362
|
-
lines.push(` handler: ${reindentBody(p.handlerBody
|
|
383
|
+
lines.push(` schema: ${reindentBody(p.schemaSource?.raw ?? "", PATTERN_INDENT)},`);
|
|
384
|
+
lines.push(` handler: ${reindentBody(p.handlerBody?.raw ?? "", PATTERN_INDENT)},`);
|
|
363
385
|
if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
|
|
364
386
|
if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
|
|
365
387
|
if (p.unsafeSkipTransitionGuard === true) lines.push(" unsafeSkipTransitionGuard: true,");
|
|
@@ -368,10 +390,11 @@ function renderWriteHandler(p: WriteHandlerPattern): string {
|
|
|
368
390
|
}
|
|
369
391
|
|
|
370
392
|
function renderQueryHandler(p: QueryHandlerPattern): string {
|
|
393
|
+
if (p.handlerName === undefined) return p.source.raw;
|
|
371
394
|
const lines: string[] = ["r.queryHandler({"];
|
|
372
395
|
lines.push(` name: ${JSON.stringify(p.handlerName)},`);
|
|
373
|
-
lines.push(` schema: ${reindentBody(p.schemaSource
|
|
374
|
-
lines.push(` handler: ${reindentBody(p.handlerBody
|
|
396
|
+
lines.push(` schema: ${reindentBody(p.schemaSource?.raw ?? "", PATTERN_INDENT)},`);
|
|
397
|
+
lines.push(` handler: ${reindentBody(p.handlerBody?.raw ?? "", PATTERN_INDENT)},`);
|
|
375
398
|
if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
|
|
376
399
|
if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
|
|
377
400
|
lines.push("});");
|
|
@@ -563,9 +586,7 @@ function weaveOpaque(
|
|
|
563
586
|
}
|
|
564
587
|
|
|
565
588
|
function renderValueWithRawSlots(value: WovenValue, indent: number): string {
|
|
566
|
-
if (value
|
|
567
|
-
return (value as { __raw: string }).__raw;
|
|
568
|
-
}
|
|
589
|
+
if (isRawRefSentinel(value)) return value.__raw;
|
|
569
590
|
if (Array.isArray(value)) {
|
|
570
591
|
if (value.length === 0) return "[]";
|
|
571
592
|
const inner = value
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { ZodType, z } from "zod";
|
|
2
|
+
import { LifecycleHookTypes } from "./constants";
|
|
3
|
+
import type {
|
|
4
|
+
AuthClaimsFn,
|
|
5
|
+
ClaimKeyDefinition,
|
|
6
|
+
ConfigKeyDefinition,
|
|
7
|
+
ConfigSeedDef,
|
|
8
|
+
EntityDefinition,
|
|
9
|
+
EntityProjectionExtension,
|
|
10
|
+
EventMigrationDef,
|
|
11
|
+
ExtensionSelectorDef,
|
|
12
|
+
FeatureMetricDef,
|
|
13
|
+
JobDefinition,
|
|
14
|
+
LifecycleHookFn,
|
|
15
|
+
MultiStreamProjectionDefinition,
|
|
16
|
+
NotificationDefinition,
|
|
17
|
+
OwnedFn,
|
|
18
|
+
PhasedHook,
|
|
19
|
+
PostDeleteHookFn,
|
|
20
|
+
PostQueryHookFn,
|
|
21
|
+
PostSaveHookFn,
|
|
22
|
+
PreDeleteHookFn,
|
|
23
|
+
ProjectionDefinition,
|
|
24
|
+
QueryHandlerDef,
|
|
25
|
+
RawTableEntry,
|
|
26
|
+
ReferenceDataDef,
|
|
27
|
+
RegistrarExtensionDef,
|
|
28
|
+
RegistrarExtensionRegistration,
|
|
29
|
+
RelationDefinition,
|
|
30
|
+
SearchPayloadContributorFn,
|
|
31
|
+
SecretKeyDefinition,
|
|
32
|
+
TranslationKeys,
|
|
33
|
+
TreeActionDef,
|
|
34
|
+
UiHints,
|
|
35
|
+
UnmanagedTableEntry,
|
|
36
|
+
ValidationHookFn,
|
|
37
|
+
WriteHandlerDef,
|
|
38
|
+
} from "./types";
|
|
39
|
+
import type { HttpRouteDefinition } from "./types/http-route";
|
|
40
|
+
import type { NavDefinition } from "./types/nav";
|
|
41
|
+
import type { ScreenDefinition } from "./types/screen";
|
|
42
|
+
import type { WorkspaceDefinition } from "./types/workspace";
|
|
43
|
+
|
|
44
|
+
const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
|
|
45
|
+
|
|
46
|
+
// Bundles every Record/Set/array/scalar defineFeature populates while the
|
|
47
|
+
// registrar's ~40 methods run — hoisted out of defineFeature's closure so a
|
|
48
|
+
// future move-diff (extracting each method into its own module) can thread
|
|
49
|
+
// it explicitly. Every field is held BY REFERENCE — no destructured copies.
|
|
50
|
+
export type FeatureBuilderState = {
|
|
51
|
+
requires: string[];
|
|
52
|
+
optionalRequires: string[];
|
|
53
|
+
requiredProjections: Set<string>;
|
|
54
|
+
requiredSteps: Set<string>;
|
|
55
|
+
entities: Record<string, EntityDefinition>;
|
|
56
|
+
entityTables: Record<string, unknown>;
|
|
57
|
+
relations: Record<string, Record<string, RelationDefinition>>;
|
|
58
|
+
writeHandlers: Record<string, WriteHandlerDef>;
|
|
59
|
+
queryHandlers: Record<string, QueryHandlerDef>;
|
|
60
|
+
validationHooks: Record<string, ValidationHookFn>;
|
|
61
|
+
lifecycleHooks: Record<string, Record<string, OwnedFn<LifecycleHookFn>[]>>;
|
|
62
|
+
phasedLifecycleHooks: Record<
|
|
63
|
+
"postSave" | "preDelete" | "postDelete",
|
|
64
|
+
Record<string, PhasedHook<LifecycleHookFn>[]>
|
|
65
|
+
>;
|
|
66
|
+
configKeys: Record<string, ConfigKeyDefinition>;
|
|
67
|
+
configSeeds: ConfigSeedDef[];
|
|
68
|
+
jobs: Record<string, JobDefinition>;
|
|
69
|
+
events: Record<string, { name: string; schema: ZodType; version: number }>;
|
|
70
|
+
eventMigrations: Record<string, EventMigrationDef[]>;
|
|
71
|
+
configReads: string[];
|
|
72
|
+
entityPostSave: Record<string, PhasedHook<PostSaveHookFn>[]>;
|
|
73
|
+
entityPreDelete: Record<string, PhasedHook<PreDeleteHookFn>[]>;
|
|
74
|
+
entityPostDelete: Record<string, PhasedHook<PostDeleteHookFn>[]>;
|
|
75
|
+
entityPostQuery: Record<string, OwnedFn<PostQueryHookFn>[]>;
|
|
76
|
+
searchPayloadExtensions: Record<string, OwnedFn<SearchPayloadContributorFn>[]>;
|
|
77
|
+
notifications: Record<string, NotificationDefinition>;
|
|
78
|
+
registrarExtensions: Record<string, RegistrarExtensionDef>;
|
|
79
|
+
extensionUsages: RegistrarExtensionRegistration[];
|
|
80
|
+
extensionSelectors: ExtensionSelectorDef[];
|
|
81
|
+
exposedApis: Set<string>;
|
|
82
|
+
usedApis: Set<string>;
|
|
83
|
+
referenceData: ReferenceDataDef[];
|
|
84
|
+
handlerEntityMappings: Record<string, string>;
|
|
85
|
+
metrics: Record<string, FeatureMetricDef>;
|
|
86
|
+
secretKeys: Record<string, SecretKeyDefinition>;
|
|
87
|
+
projections: Record<string, ProjectionDefinition>;
|
|
88
|
+
multiStreamProjections: Record<string, MultiStreamProjectionDefinition>;
|
|
89
|
+
entityProjectionExtensions: Record<string, EntityProjectionExtension[]>;
|
|
90
|
+
rawTables: Record<string, RawTableEntry>;
|
|
91
|
+
unmanagedTables: Record<string, UnmanagedTableEntry>;
|
|
92
|
+
authClaimsHooks: AuthClaimsFn[];
|
|
93
|
+
claimKeys: Record<string, ClaimKeyDefinition>;
|
|
94
|
+
screens: Record<string, ScreenDefinition>;
|
|
95
|
+
navs: Record<string, NavDefinition>;
|
|
96
|
+
workspaces: Record<string, WorkspaceDefinition>;
|
|
97
|
+
httpRoutes: Record<string, HttpRouteDefinition>;
|
|
98
|
+
translations: TranslationKeys;
|
|
99
|
+
isSystemScoped: boolean;
|
|
100
|
+
toggleableDefault: boolean | undefined;
|
|
101
|
+
description: string | undefined;
|
|
102
|
+
uiHints: UiHints | undefined;
|
|
103
|
+
treeActions: Readonly<Record<string, TreeActionDef>> | undefined;
|
|
104
|
+
envSchema: z.ZodObject<z.ZodRawShape> | undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function createInitialFeatureBuilderState(): FeatureBuilderState {
|
|
108
|
+
const lifecycleHooks: Record<string, Record<string, OwnedFn<LifecycleHookFn>[]>> = {};
|
|
109
|
+
for (const t of LIFECYCLE_TYPES) {
|
|
110
|
+
lifecycleHooks[t] = {};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
requires: [],
|
|
114
|
+
optionalRequires: [],
|
|
115
|
+
requiredProjections: new Set<string>(),
|
|
116
|
+
requiredSteps: new Set<string>(),
|
|
117
|
+
entities: {},
|
|
118
|
+
entityTables: {},
|
|
119
|
+
relations: {},
|
|
120
|
+
writeHandlers: {},
|
|
121
|
+
queryHandlers: {},
|
|
122
|
+
validationHooks: {},
|
|
123
|
+
lifecycleHooks,
|
|
124
|
+
phasedLifecycleHooks: { postSave: {}, preDelete: {}, postDelete: {} },
|
|
125
|
+
configKeys: {},
|
|
126
|
+
configSeeds: [],
|
|
127
|
+
jobs: {},
|
|
128
|
+
events: {},
|
|
129
|
+
eventMigrations: {},
|
|
130
|
+
configReads: [],
|
|
131
|
+
entityPostSave: {},
|
|
132
|
+
entityPreDelete: {},
|
|
133
|
+
entityPostDelete: {},
|
|
134
|
+
entityPostQuery: {},
|
|
135
|
+
searchPayloadExtensions: {},
|
|
136
|
+
notifications: {},
|
|
137
|
+
registrarExtensions: {},
|
|
138
|
+
extensionUsages: [],
|
|
139
|
+
extensionSelectors: [],
|
|
140
|
+
exposedApis: new Set(),
|
|
141
|
+
usedApis: new Set(),
|
|
142
|
+
referenceData: [],
|
|
143
|
+
handlerEntityMappings: {},
|
|
144
|
+
metrics: {},
|
|
145
|
+
secretKeys: {},
|
|
146
|
+
projections: {},
|
|
147
|
+
multiStreamProjections: {},
|
|
148
|
+
entityProjectionExtensions: {},
|
|
149
|
+
rawTables: {},
|
|
150
|
+
unmanagedTables: {},
|
|
151
|
+
authClaimsHooks: [],
|
|
152
|
+
claimKeys: {},
|
|
153
|
+
screens: {},
|
|
154
|
+
navs: {},
|
|
155
|
+
workspaces: {},
|
|
156
|
+
httpRoutes: {},
|
|
157
|
+
translations: {},
|
|
158
|
+
isSystemScoped: false,
|
|
159
|
+
toggleableDefault: undefined,
|
|
160
|
+
description: undefined,
|
|
161
|
+
uiHints: undefined,
|
|
162
|
+
treeActions: undefined,
|
|
163
|
+
envSchema: undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|