@cosmicdrift/kumiko-framework 0.154.0 → 0.154.2
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 +2 -2
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/api/__tests__/batch.integration.test.ts +9 -9
- package/src/engine/__tests__/engine.test.ts +16 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +9 -2
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/define-feature.ts +22 -4
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +15 -28
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +55 -14
- package/src/engine/feature-ast/__tests__/patch.test.ts +8 -13
- package/src/engine/feature-ast/__tests__/patcher.test.ts +12 -22
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +247 -5
- package/src/engine/feature-ast/extractors/index.ts +0 -3
- package/src/engine/feature-ast/extractors/round4.ts +113 -287
- package/src/engine/feature-ast/index.ts +0 -4
- package/src/engine/feature-ast/parse.ts +0 -6
- package/src/engine/feature-ast/patch.ts +35 -45
- package/src/engine/feature-ast/patcher.ts +18 -40
- package/src/engine/feature-ast/patterns.ts +20 -41
- package/src/engine/feature-ast/render.ts +17 -27
- package/src/engine/feature-config-events-jobs.ts +150 -74
- package/src/engine/feature-entity-handlers.ts +24 -6
- package/src/engine/feature-ui-extensions.ts +116 -45
- package/src/engine/object-form.ts +12 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +0 -19
- package/src/engine/pattern-library/library.ts +0 -4
- package/src/engine/pattern-library/mixed-schemas.ts +9 -80
- package/src/engine/pattern-library/shared-fields.ts +1 -6
- package/src/engine/registry-ingest.ts +7 -2
- package/src/engine/registry-validate.ts +6 -6
- package/src/engine/types/feature.ts +76 -45
- package/src/engine/types/handlers.ts +6 -3
- package/src/engine/types/nav.ts +4 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/jobs/__tests__/jobs.integration.test.ts +66 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +1 -1
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +200 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/meilisearch-adapter.ts +13 -12
|
@@ -10,8 +10,6 @@ import type { ScreenDefinition } from "../../types/screen";
|
|
|
10
10
|
import type {
|
|
11
11
|
AuthClaimsPattern,
|
|
12
12
|
DefineEventPattern,
|
|
13
|
-
EntityHookPattern,
|
|
14
|
-
EventMigrationPattern,
|
|
15
13
|
HookPattern,
|
|
16
14
|
HttpRoutePattern,
|
|
17
15
|
JobPattern,
|
|
@@ -91,6 +89,25 @@ export function readOptionalRateLimit(value: unknown): RateLimitOption | undefin
|
|
|
91
89
|
return value as unknown as RateLimitOption;
|
|
92
90
|
}
|
|
93
91
|
|
|
92
|
+
// r.hook's target: a NameOrRef, a list of them, or an entity-wide
|
|
93
|
+
// `{ allOf: entityRef }` (replaces the old r.entityHook(type, entity, fn)).
|
|
94
|
+
// Checked first so a malformed `{ allOf }` doesn't silently fall through
|
|
95
|
+
// to being read as some other object shape.
|
|
96
|
+
function readHookTarget(
|
|
97
|
+
node: Node,
|
|
98
|
+
): string | readonly string[] | { readonly allOf: string } | undefined {
|
|
99
|
+
const obj = node.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
100
|
+
if (obj) {
|
|
101
|
+
const allOfProp = obj.getProperty("allOf")?.asKind(SyntaxKind.PropertyAssignment);
|
|
102
|
+
if (allOfProp) {
|
|
103
|
+
const initializer = allOfProp.getInitializer();
|
|
104
|
+
const entityName = initializer && readNameOrRef(initializer);
|
|
105
|
+
return entityName ? { allOf: entityName } : undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return readNameOrRefOrList(node);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
export function extractHook(
|
|
95
112
|
call: CallExpression,
|
|
96
113
|
sourceFile: SourceFile,
|
|
@@ -134,7 +151,7 @@ export function extractHook(
|
|
|
134
151
|
"object form requires a `target` property",
|
|
135
152
|
);
|
|
136
153
|
}
|
|
137
|
-
const target =
|
|
154
|
+
const target = readHookTarget(targetInit);
|
|
138
155
|
if (!target) {
|
|
139
156
|
return fail(
|
|
140
157
|
"hook",
|
|
@@ -196,7 +213,7 @@ export function extractHook(
|
|
|
196
213
|
"expected a target (NameOrRef or array) as second argument",
|
|
197
214
|
);
|
|
198
215
|
}
|
|
199
|
-
const target =
|
|
216
|
+
const target = readHookTarget(targetArg);
|
|
200
217
|
if (!target) {
|
|
201
218
|
return fail(
|
|
202
219
|
"hook",
|
|
@@ -231,154 +248,6 @@ export function extractHook(
|
|
|
231
248
|
});
|
|
232
249
|
}
|
|
233
250
|
|
|
234
|
-
export function isEntityHookType(value: string): value is "postSave" | "preDelete" | "postDelete" {
|
|
235
|
-
return value === "postSave" || value === "preDelete" || value === "postDelete";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function extractEntityHook(
|
|
239
|
-
call: CallExpression,
|
|
240
|
-
sourceFile: SourceFile,
|
|
241
|
-
): ExtractOutput<EntityHookPattern> {
|
|
242
|
-
const args = call.getArguments();
|
|
243
|
-
const first = args[0];
|
|
244
|
-
if (!first) {
|
|
245
|
-
return fail(
|
|
246
|
-
"entityHook",
|
|
247
|
-
sourceLocationFromNode(call, sourceFile),
|
|
248
|
-
"expected at least one argument",
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
253
|
-
if (obj && args.length === 1) {
|
|
254
|
-
const typeInit = obj
|
|
255
|
-
.getProperty("type")
|
|
256
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
257
|
-
?.getInitializer()
|
|
258
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
259
|
-
if (!typeInit) {
|
|
260
|
-
return fail(
|
|
261
|
-
"entityHook",
|
|
262
|
-
sourceLocationFromNode(call, sourceFile),
|
|
263
|
-
"object form requires a string-literal `type` property",
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
const hookType = typeInit.getLiteralValue();
|
|
267
|
-
if (!isEntityHookType(hookType)) {
|
|
268
|
-
return fail(
|
|
269
|
-
"entityHook",
|
|
270
|
-
sourceLocationFromNode(call, sourceFile),
|
|
271
|
-
`entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
const entityInit = obj
|
|
275
|
-
.getProperty("entity")
|
|
276
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
277
|
-
?.getInitializer();
|
|
278
|
-
if (!entityInit) {
|
|
279
|
-
return fail(
|
|
280
|
-
"entityHook",
|
|
281
|
-
sourceLocationFromNode(call, sourceFile),
|
|
282
|
-
"object form requires an `entity` property",
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
const entityName = readNameOrRef(entityInit);
|
|
286
|
-
if (!entityName) {
|
|
287
|
-
return fail(
|
|
288
|
-
"entityHook",
|
|
289
|
-
sourceLocationFromNode(call, sourceFile),
|
|
290
|
-
"`entity` must be a string literal or inline { name } object",
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
const handlerInit = obj
|
|
294
|
-
.getProperty("handler")
|
|
295
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
296
|
-
?.getInitializer();
|
|
297
|
-
if (!handlerInit) {
|
|
298
|
-
return fail(
|
|
299
|
-
"entityHook",
|
|
300
|
-
sourceLocationFromNode(call, sourceFile),
|
|
301
|
-
"object form requires a `handler` property",
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
const fn = findFunctionLiteral(handlerInit);
|
|
305
|
-
if (!fn) {
|
|
306
|
-
return fail(
|
|
307
|
-
"entityHook",
|
|
308
|
-
sourceLocationFromNode(call, sourceFile),
|
|
309
|
-
"handler must be an inline arrow function or function expression",
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
const phase = readOptionalPhase(obj);
|
|
313
|
-
return ok({
|
|
314
|
-
kind: "entityHook",
|
|
315
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
316
|
-
hookType,
|
|
317
|
-
entityName,
|
|
318
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
319
|
-
...(phase !== undefined && { phase }),
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const typeArg = first.asKind(SyntaxKind.StringLiteral);
|
|
324
|
-
if (!typeArg) {
|
|
325
|
-
return fail(
|
|
326
|
-
"entityHook",
|
|
327
|
-
sourceLocationFromNode(call, sourceFile),
|
|
328
|
-
"first argument must be a string literal hook type (or use the object form)",
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
const hookType = typeArg.getLiteralValue();
|
|
332
|
-
if (!isEntityHookType(hookType)) {
|
|
333
|
-
return fail(
|
|
334
|
-
"entityHook",
|
|
335
|
-
sourceLocationFromNode(call, sourceFile),
|
|
336
|
-
`entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
const entityArg = args[1];
|
|
340
|
-
if (!entityArg) {
|
|
341
|
-
return fail(
|
|
342
|
-
"entityHook",
|
|
343
|
-
sourceLocationFromNode(call, sourceFile),
|
|
344
|
-
"expected an entity reference as second argument",
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
const entityName = readNameOrRef(entityArg);
|
|
348
|
-
if (!entityName) {
|
|
349
|
-
return fail(
|
|
350
|
-
"entityHook",
|
|
351
|
-
sourceLocationFromNode(call, sourceFile),
|
|
352
|
-
"second argument must be a string literal or inline { name } object",
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
const fnArg = args[2];
|
|
356
|
-
if (!fnArg) {
|
|
357
|
-
return fail(
|
|
358
|
-
"entityHook",
|
|
359
|
-
sourceLocationFromNode(call, sourceFile),
|
|
360
|
-
"expected a hook function as third argument",
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
const fn = findFunctionLiteral(fnArg);
|
|
364
|
-
if (!fn) {
|
|
365
|
-
return fail(
|
|
366
|
-
"entityHook",
|
|
367
|
-
sourceLocationFromNode(call, sourceFile),
|
|
368
|
-
"third argument must be an inline arrow function or function expression",
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
const phase = readOptionalPhase(args[3]);
|
|
372
|
-
return ok({
|
|
373
|
-
kind: "entityHook",
|
|
374
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
375
|
-
hookType,
|
|
376
|
-
entityName,
|
|
377
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
378
|
-
...(phase !== undefined && { phase }),
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
251
|
// guard:dup-ok — intentionale Parallele zu extractTree (round6); verschiedene Feature-AST-Extraktoren by design
|
|
383
252
|
export function extractAuthClaims(
|
|
384
253
|
call: CallExpression,
|
|
@@ -805,6 +674,74 @@ export function extractHttpRoute(
|
|
|
805
674
|
});
|
|
806
675
|
}
|
|
807
676
|
|
|
677
|
+
// Reads defineEvent's `migrations` option — either the array-of-steps shape
|
|
678
|
+
// used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
|
|
679
|
+
// toVersion is redundant on-disk since it is always fromVersion + 1) or the
|
|
680
|
+
// keyed-object canonical shape the Designer renders (`{ "1": transform }`).
|
|
681
|
+
// Keyed by fromVersion (as a string) → the transform closure's location.
|
|
682
|
+
// Skips malformed entries rather than failing the whole defineEvent extract
|
|
683
|
+
// — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
|
|
684
|
+
function extractEventMigrationsFromArray(
|
|
685
|
+
arr: Node,
|
|
686
|
+
sourceFile: SourceFile,
|
|
687
|
+
): Record<string, SourceLocation> {
|
|
688
|
+
const result: Record<string, SourceLocation> = {};
|
|
689
|
+
const arrLit = arr.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
690
|
+
for (const el of arrLit.getElements()) {
|
|
691
|
+
const obj = el.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
692
|
+
if (!obj) continue;
|
|
693
|
+
const fromInit = obj
|
|
694
|
+
.getProperty("fromVersion")
|
|
695
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
696
|
+
?.getInitializer();
|
|
697
|
+
const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
|
|
698
|
+
if (typeof fromVersion !== "number") continue;
|
|
699
|
+
const transformInit = obj
|
|
700
|
+
.getProperty("transform")
|
|
701
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
702
|
+
?.getInitializer();
|
|
703
|
+
const fn = transformInit ? findFunctionLiteral(transformInit) : undefined;
|
|
704
|
+
if (!fn) continue;
|
|
705
|
+
result[String(fromVersion)] = sourceLocationFromNode(fn, sourceFile);
|
|
706
|
+
}
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function extractEventMigrationsFromKeyedObject(
|
|
711
|
+
objLit: Node,
|
|
712
|
+
sourceFile: SourceFile,
|
|
713
|
+
): Record<string, SourceLocation> {
|
|
714
|
+
const result: Record<string, SourceLocation> = {};
|
|
715
|
+
const obj = objLit.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
716
|
+
for (const prop of obj.getProperties()) {
|
|
717
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
718
|
+
const initializer = propAssign?.getInitializer();
|
|
719
|
+
const fn = initializer ? findFunctionLiteral(initializer) : undefined;
|
|
720
|
+
if (!propAssign || !fn) continue;
|
|
721
|
+
result[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Reads defineEvent's `migrations` option — either the array-of-steps shape
|
|
727
|
+
// used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
|
|
728
|
+
// toVersion is redundant on-disk since it is always fromVersion + 1) or the
|
|
729
|
+
// keyed-object canonical shape the Designer renders (`{ "1": transform }`).
|
|
730
|
+
// Keyed by fromVersion (as a string) → the transform closure's location.
|
|
731
|
+
// Skips malformed entries rather than failing the whole defineEvent extract
|
|
732
|
+
// — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
|
|
733
|
+
function extractEventMigrationsField(
|
|
734
|
+
node: Node,
|
|
735
|
+
sourceFile: SourceFile,
|
|
736
|
+
): Readonly<Record<string, SourceLocation>> | undefined {
|
|
737
|
+
const result = node.asKind(SyntaxKind.ArrayLiteralExpression)
|
|
738
|
+
? extractEventMigrationsFromArray(node, sourceFile)
|
|
739
|
+
: node.asKind(SyntaxKind.ObjectLiteralExpression)
|
|
740
|
+
? extractEventMigrationsFromKeyedObject(node, sourceFile)
|
|
741
|
+
: undefined;
|
|
742
|
+
return result && Object.keys(result).length > 0 ? result : undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
808
745
|
export function extractDefineEvent(
|
|
809
746
|
call: CallExpression,
|
|
810
747
|
sourceFile: SourceFile,
|
|
@@ -853,12 +790,20 @@ export function extractDefineEvent(
|
|
|
853
790
|
const v = readDataLiteralNode(versionInit);
|
|
854
791
|
if (typeof v === "number") version = v;
|
|
855
792
|
}
|
|
793
|
+
const migrationsInit = obj
|
|
794
|
+
.getProperty("migrations")
|
|
795
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
796
|
+
?.getInitializer();
|
|
797
|
+
const migrations = migrationsInit
|
|
798
|
+
? extractEventMigrationsField(migrationsInit, sourceFile)
|
|
799
|
+
: undefined;
|
|
856
800
|
return ok({
|
|
857
801
|
kind: "defineEvent",
|
|
858
802
|
source: sourceLocationFromNode(call, sourceFile),
|
|
859
803
|
eventName: nameInit.getLiteralValue(),
|
|
860
804
|
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
861
805
|
...(version !== undefined && { version }),
|
|
806
|
+
...(migrations !== undefined && { migrations }),
|
|
862
807
|
});
|
|
863
808
|
}
|
|
864
809
|
|
|
@@ -879,152 +824,33 @@ export function extractDefineEvent(
|
|
|
879
824
|
);
|
|
880
825
|
}
|
|
881
826
|
let version: number | undefined;
|
|
827
|
+
let migrations: Readonly<Record<string, SourceLocation>> | undefined;
|
|
882
828
|
const optionsArg = args[2];
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
return ok({
|
|
890
|
-
kind: "defineEvent",
|
|
891
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
892
|
-
eventName: nameArg.getLiteralValue(),
|
|
893
|
-
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
894
|
-
...(version !== undefined && { version }),
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
export function extractEventMigration(
|
|
899
|
-
call: CallExpression,
|
|
900
|
-
sourceFile: SourceFile,
|
|
901
|
-
): ExtractOutput<EventMigrationPattern> {
|
|
902
|
-
const args = call.getArguments();
|
|
903
|
-
const first = args[0];
|
|
904
|
-
if (!first) {
|
|
905
|
-
return fail(
|
|
906
|
-
"eventMigration",
|
|
907
|
-
sourceLocationFromNode(call, sourceFile),
|
|
908
|
-
"expected at least one argument",
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
913
|
-
if (obj && args.length === 1) {
|
|
914
|
-
const eventInit = obj
|
|
915
|
-
.getProperty("event")
|
|
916
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
917
|
-
?.getInitializer()
|
|
918
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
919
|
-
if (!eventInit) {
|
|
920
|
-
return fail(
|
|
921
|
-
"eventMigration",
|
|
922
|
-
sourceLocationFromNode(call, sourceFile),
|
|
923
|
-
"object form requires a string-literal `event` property",
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
const fromInit = obj
|
|
927
|
-
.getProperty("fromVersion")
|
|
928
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
929
|
-
?.getInitializer();
|
|
930
|
-
const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
|
|
931
|
-
if (typeof fromVersion !== "number") {
|
|
932
|
-
return fail(
|
|
933
|
-
"eventMigration",
|
|
934
|
-
sourceLocationFromNode(call, sourceFile),
|
|
935
|
-
"fromVersion must be a numeric literal",
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
const toInit = obj
|
|
939
|
-
.getProperty("toVersion")
|
|
829
|
+
const optionsObj = optionsArg?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
830
|
+
if (optionsObj) {
|
|
831
|
+
const versionInit = optionsObj
|
|
832
|
+
.getProperty("version")
|
|
940
833
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
941
834
|
?.getInitializer();
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
"eventMigration",
|
|
946
|
-
sourceLocationFromNode(call, sourceFile),
|
|
947
|
-
"toVersion must be a numeric literal",
|
|
948
|
-
);
|
|
835
|
+
if (versionInit) {
|
|
836
|
+
const v = readDataLiteralNode(versionInit);
|
|
837
|
+
if (typeof v === "number") version = v;
|
|
949
838
|
}
|
|
950
|
-
const
|
|
951
|
-
.getProperty("
|
|
839
|
+
const migrationsInit = optionsObj
|
|
840
|
+
.getProperty("migrations")
|
|
952
841
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
953
842
|
?.getInitializer();
|
|
954
|
-
if (
|
|
955
|
-
|
|
956
|
-
"eventMigration",
|
|
957
|
-
sourceLocationFromNode(call, sourceFile),
|
|
958
|
-
"object form requires a `transform` property",
|
|
959
|
-
);
|
|
843
|
+
if (migrationsInit) {
|
|
844
|
+
migrations = extractEventMigrationsField(migrationsInit, sourceFile);
|
|
960
845
|
}
|
|
961
|
-
const fn = findFunctionLiteral(transformInit);
|
|
962
|
-
if (!fn) {
|
|
963
|
-
return fail(
|
|
964
|
-
"eventMigration",
|
|
965
|
-
sourceLocationFromNode(call, sourceFile),
|
|
966
|
-
"transform must be an inline arrow function or function expression",
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
return ok({
|
|
970
|
-
kind: "eventMigration",
|
|
971
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
972
|
-
eventName: eventInit.getLiteralValue(),
|
|
973
|
-
fromVersion,
|
|
974
|
-
toVersion,
|
|
975
|
-
transformBody: sourceLocationFromNode(fn, sourceFile),
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
980
|
-
if (!nameArg) {
|
|
981
|
-
return fail(
|
|
982
|
-
"eventMigration",
|
|
983
|
-
sourceLocationFromNode(call, sourceFile),
|
|
984
|
-
"first argument must be a string literal event name (or use the object form)",
|
|
985
|
-
);
|
|
986
|
-
}
|
|
987
|
-
const fromArg = args[1];
|
|
988
|
-
const fromVersion = fromArg ? readDataLiteralNode(fromArg) : undefined;
|
|
989
|
-
if (typeof fromVersion !== "number") {
|
|
990
|
-
return fail(
|
|
991
|
-
"eventMigration",
|
|
992
|
-
sourceLocationFromNode(call, sourceFile),
|
|
993
|
-
"fromVersion must be a numeric literal",
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
const toArg = args[2];
|
|
997
|
-
const toVersion = toArg ? readDataLiteralNode(toArg) : undefined;
|
|
998
|
-
if (typeof toVersion !== "number") {
|
|
999
|
-
return fail(
|
|
1000
|
-
"eventMigration",
|
|
1001
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1002
|
-
"toVersion must be a numeric literal",
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
const transformArg = args[3];
|
|
1006
|
-
if (!transformArg) {
|
|
1007
|
-
return fail(
|
|
1008
|
-
"eventMigration",
|
|
1009
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1010
|
-
"expected a transform function as fourth argument",
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
const fn = findFunctionLiteral(transformArg);
|
|
1014
|
-
if (!fn) {
|
|
1015
|
-
return fail(
|
|
1016
|
-
"eventMigration",
|
|
1017
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1018
|
-
"transform must be an inline arrow function or function expression",
|
|
1019
|
-
);
|
|
1020
846
|
}
|
|
1021
847
|
return ok({
|
|
1022
|
-
kind: "
|
|
848
|
+
kind: "defineEvent",
|
|
1023
849
|
source: sourceLocationFromNode(call, sourceFile),
|
|
1024
850
|
eventName: nameArg.getLiteralValue(),
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
851
|
+
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
852
|
+
...(version !== undefined && { version }),
|
|
853
|
+
...(migrations !== undefined && { migrations }),
|
|
1028
854
|
});
|
|
1029
855
|
}
|
|
1030
856
|
|
|
@@ -27,8 +27,6 @@ export type {
|
|
|
27
27
|
AddConfigArgs,
|
|
28
28
|
AddDefineEventArgs,
|
|
29
29
|
AddEntityArgs,
|
|
30
|
-
AddEntityHookArgs,
|
|
31
|
-
AddEventMigrationArgs,
|
|
32
30
|
AddHookArgs,
|
|
33
31
|
AddHttpRouteArgs,
|
|
34
32
|
AddJobArgs,
|
|
@@ -59,10 +57,8 @@ export type {
|
|
|
59
57
|
ConfigPattern,
|
|
60
58
|
DefineEventPattern,
|
|
61
59
|
Editability,
|
|
62
|
-
EntityHookPattern,
|
|
63
60
|
// Static patterns
|
|
64
61
|
EntityPattern,
|
|
65
|
-
EventMigrationPattern,
|
|
66
62
|
ExtendsRegistrarPattern,
|
|
67
63
|
FeaturePattern,
|
|
68
64
|
FeaturePatternKind,
|
|
@@ -38,9 +38,7 @@ import {
|
|
|
38
38
|
extractDefineEvent,
|
|
39
39
|
extractDescribe,
|
|
40
40
|
extractEntity,
|
|
41
|
-
extractEntityHook,
|
|
42
41
|
extractEnvSchema,
|
|
43
|
-
extractEventMigration,
|
|
44
42
|
extractExposesApi,
|
|
45
43
|
extractExtendsRegistrar,
|
|
46
44
|
extractHook,
|
|
@@ -443,8 +441,6 @@ function dispatchExtractor(
|
|
|
443
441
|
// Round 4 — mixed (header + body) patterns
|
|
444
442
|
case "hook":
|
|
445
443
|
return extractHook(call, sourceFile);
|
|
446
|
-
case "entityHook":
|
|
447
|
-
return extractEntityHook(call, sourceFile);
|
|
448
444
|
case "authClaims":
|
|
449
445
|
return extractAuthClaims(call, sourceFile);
|
|
450
446
|
case "writeHandler":
|
|
@@ -457,8 +453,6 @@ function dispatchExtractor(
|
|
|
457
453
|
return extractHttpRoute(call, sourceFile);
|
|
458
454
|
case "defineEvent":
|
|
459
455
|
return extractDefineEvent(call, sourceFile);
|
|
460
|
-
case "eventMigration":
|
|
461
|
-
return extractEventMigration(call, sourceFile);
|
|
462
456
|
case "notification":
|
|
463
457
|
return extractNotification(call, sourceFile);
|
|
464
458
|
case "projection":
|
|
@@ -53,8 +53,11 @@ export type PatternId =
|
|
|
53
53
|
| { readonly kind: "screen"; readonly id: string }
|
|
54
54
|
| { readonly kind: "writeHandler"; readonly handlerName: string }
|
|
55
55
|
| { readonly kind: "queryHandler"; readonly handlerName: string }
|
|
56
|
-
| {
|
|
57
|
-
|
|
56
|
+
| {
|
|
57
|
+
readonly kind: "hook";
|
|
58
|
+
readonly hookType: string;
|
|
59
|
+
readonly target: string | { readonly allOf: string };
|
|
60
|
+
}
|
|
58
61
|
| { readonly kind: "metric"; readonly shortName: string }
|
|
59
62
|
| { readonly kind: "secret"; readonly shortName: string }
|
|
60
63
|
| { readonly kind: "claimKey"; readonly shortName: string }
|
|
@@ -66,12 +69,6 @@ export type PatternId =
|
|
|
66
69
|
| { readonly kind: "projection"; readonly name: string }
|
|
67
70
|
| { readonly kind: "multiStreamProjection"; readonly name: string }
|
|
68
71
|
| { readonly kind: "defineEvent"; readonly eventName: string }
|
|
69
|
-
| {
|
|
70
|
-
readonly kind: "eventMigration";
|
|
71
|
-
readonly eventName: string;
|
|
72
|
-
readonly fromVersion: number;
|
|
73
|
-
readonly toVersion: number;
|
|
74
|
-
}
|
|
75
72
|
| { readonly kind: "extendsRegistrar"; readonly extensionName: string }
|
|
76
73
|
// Singleton patterns — only one per feature, kind alone identifies them.
|
|
77
74
|
| { readonly kind: "requires" }
|
|
@@ -362,6 +359,18 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
362
359
|
);
|
|
363
360
|
case "hook":
|
|
364
361
|
// Positional: r.hook(type, target, fn) | Object: { type, target }
|
|
362
|
+
if (typeof id.target === "object") {
|
|
363
|
+
// Entity-wide { allOf } target — only ever appears as the object
|
|
364
|
+
// literal { allOf: entity } (positional or nested in the object
|
|
365
|
+
// form), never as a bare string, so check both call shapes.
|
|
366
|
+
if (matchFirstArgString(call, id.hookType)) {
|
|
367
|
+
return matchAllOfArg(call.getArguments()[1], id.target.allOf);
|
|
368
|
+
}
|
|
369
|
+
return (
|
|
370
|
+
matchObjectProperty(call, "type", id.hookType) &&
|
|
371
|
+
matchObjectAllOfProperty(call, "target", id.target.allOf)
|
|
372
|
+
);
|
|
373
|
+
}
|
|
365
374
|
if (matchFirstArgString(call, id.hookType)) {
|
|
366
375
|
const target = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
367
376
|
return target === id.target;
|
|
@@ -370,15 +379,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
370
379
|
matchObjectProperty(call, "type", id.hookType) &&
|
|
371
380
|
matchObjectProperty(call, "target", id.target)
|
|
372
381
|
);
|
|
373
|
-
case "entityHook":
|
|
374
|
-
if (matchFirstArgString(call, id.hookType)) {
|
|
375
|
-
const ent = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
376
|
-
return ent === id.entityName;
|
|
377
|
-
}
|
|
378
|
-
return (
|
|
379
|
-
matchObjectProperty(call, "type", id.hookType) &&
|
|
380
|
-
matchObjectProperty(call, "entity", id.entityName)
|
|
381
|
-
);
|
|
382
382
|
case "metric":
|
|
383
383
|
case "secret":
|
|
384
384
|
case "claimKey":
|
|
@@ -419,20 +419,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
419
419
|
return (
|
|
420
420
|
matchFirstArgString(call, id.eventName) || matchObjectProperty(call, "name", id.eventName)
|
|
421
421
|
);
|
|
422
|
-
case "eventMigration": {
|
|
423
|
-
// Positional: r.eventMigration(name, from, to, fn)
|
|
424
|
-
if (matchFirstArgString(call, id.eventName)) {
|
|
425
|
-
const from = numericArg(call, 1);
|
|
426
|
-
const to = numericArg(call, 2);
|
|
427
|
-
return from === id.fromVersion && to === id.toVersion;
|
|
428
|
-
}
|
|
429
|
-
// Object: { event, fromVersion, toVersion }
|
|
430
|
-
return (
|
|
431
|
-
matchObjectProperty(call, "event", id.eventName) &&
|
|
432
|
-
matchObjectNumericProperty(call, "fromVersion", id.fromVersion) &&
|
|
433
|
-
matchObjectNumericProperty(call, "toVersion", id.toVersion)
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
422
|
case "extendsRegistrar":
|
|
437
423
|
return matchFirstArgString(call, id.extensionName);
|
|
438
424
|
default: {
|
|
@@ -459,25 +445,29 @@ function matchObjectProperty(call: CallExpression, propName: string, expected: s
|
|
|
459
445
|
return init?.getLiteralValue() === expected;
|
|
460
446
|
}
|
|
461
447
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
): boolean {
|
|
467
|
-
const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
468
|
-
if (!obj) return false;
|
|
448
|
+
// Matches an r.hook `{ allOf: entity }` target — as the arg node directly
|
|
449
|
+
// (positional call form) or as a nested object property (object call form).
|
|
450
|
+
function matchAllOfArg(node: Node | undefined, expectedEntity: string): boolean {
|
|
451
|
+
const obj = node?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
469
452
|
const init = obj
|
|
470
|
-
|
|
453
|
+
?.getProperty("allOf")
|
|
471
454
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
472
455
|
?.getInitializer()
|
|
473
|
-
?.asKind(SyntaxKind.
|
|
474
|
-
return init
|
|
456
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
457
|
+
return init?.getLiteralValue() === expectedEntity;
|
|
475
458
|
}
|
|
476
459
|
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
460
|
+
function matchObjectAllOfProperty(
|
|
461
|
+
call: CallExpression,
|
|
462
|
+
propName: string,
|
|
463
|
+
expectedEntity: string,
|
|
464
|
+
): boolean {
|
|
465
|
+
const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
466
|
+
const propInit = obj
|
|
467
|
+
?.getProperty(propName)
|
|
468
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
469
|
+
?.getInitializer();
|
|
470
|
+
return matchAllOfArg(propInit, expectedEntity);
|
|
481
471
|
}
|
|
482
472
|
|
|
483
473
|
// =============================================================================
|