@cosmicdrift/kumiko-framework 0.153.0 → 0.154.1
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__/anonymous-access.integration.test.ts +25 -0
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator.test.ts +7 -4
- package/src/engine/__tests__/event-migration-declarative.test.ts +9 -2
- package/src/engine/boot-validator/action-wiring.ts +99 -0
- package/src/engine/boot-validator/index.ts +7 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +7 -21
- package/src/engine/feature-ast/__tests__/parse.test.ts +33 -6
- package/src/engine/feature-ast/__tests__/patch.test.ts +8 -13
- package/src/engine/feature-ast/__tests__/patcher.test.ts +4 -14
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +37 -1
- package/src/engine/feature-ast/extractors/index.ts +0 -1
- package/src/engine/feature-ast/extractors/round4.ts +92 -136
- package/src/engine/feature-ast/index.ts +0 -2
- package/src/engine/feature-ast/parse.ts +0 -3
- package/src/engine/feature-ast/patch.ts +0 -41
- package/src/engine/feature-ast/patcher.ts +14 -20
- package/src/engine/feature-ast/patterns.ts +10 -19
- package/src/engine/feature-ast/render.ts +10 -13
- package/src/engine/feature-config-events-jobs.ts +77 -51
- package/src/engine/pattern-library/__tests__/library.test.ts +0 -10
- package/src/engine/pattern-library/library.ts +0 -2
- package/src/engine/pattern-library/mixed-schemas.ts +8 -35
- package/src/engine/registry-validate.ts +6 -6
- package/src/engine/types/feature.ts +18 -17
- package/src/engine/types/handlers.ts +6 -3
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/stack/redis.ts +8 -0
- package/src/stack/test-stack.ts +156 -136
|
@@ -114,7 +114,7 @@ function stripLocations(value: unknown): unknown {
|
|
|
114
114
|
out[k] = { raw: normalizeBodyRaw((v as { raw: string }).raw) };
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
|
-
if (k === "opaqueProps" && v && typeof v === "object") {
|
|
117
|
+
if ((k === "opaqueProps" || k === "migrations") && v && typeof v === "object") {
|
|
118
118
|
const op: Record<string, unknown> = {};
|
|
119
119
|
for (const [pk, pv] of Object.entries(v as Record<string, unknown>)) {
|
|
120
120
|
if (pv && typeof pv === "object" && "raw" in pv) {
|
|
@@ -400,3 +400,39 @@ describe("render → parse roundtrip — r.screen({ nav }) inline sugar", () =>
|
|
|
400
400
|
}
|
|
401
401
|
});
|
|
402
402
|
});
|
|
403
|
+
|
|
404
|
+
const DEFINE_EVENT_WITH_MIGRATIONS_FEATURE = `
|
|
405
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
406
|
+
|
|
407
|
+
defineFeature("billing", (r) => {
|
|
408
|
+
r.defineEvent("invoicePaid", z.object({ totalCents: z.number() }), {
|
|
409
|
+
version: 2,
|
|
410
|
+
migrations: [
|
|
411
|
+
{
|
|
412
|
+
fromVersion: 1,
|
|
413
|
+
toVersion: 2,
|
|
414
|
+
transform: (payload) => ({ totalCents: Math.round(payload.total * 100) }),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
describe("render → parse roundtrip — r.defineEvent({ migrations }) fold", () => {
|
|
422
|
+
test("migrations array survives parse → render → parse unchanged", () => {
|
|
423
|
+
const initial = parse(DEFINE_EVENT_WITH_MIGRATIONS_FEATURE);
|
|
424
|
+
const rendered = renderFeatureFile({
|
|
425
|
+
featureName: initial.featureName ?? "",
|
|
426
|
+
patterns: initial.patterns,
|
|
427
|
+
});
|
|
428
|
+
const reparsed = parse(rendered);
|
|
429
|
+
expect(reparsed.patterns.map(stripLocations)).toEqual(initial.patterns.map(stripLocations));
|
|
430
|
+
|
|
431
|
+
const eventPattern = reparsed.patterns.find((p) => p.kind === "defineEvent");
|
|
432
|
+
expect(eventPattern?.kind).toBe("defineEvent");
|
|
433
|
+
if (eventPattern?.kind === "defineEvent") {
|
|
434
|
+
expect(eventPattern.version).toBe(2);
|
|
435
|
+
expect(Object.keys(eventPattern.migrations ?? {})).toEqual(["1"]);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
AuthClaimsPattern,
|
|
12
12
|
DefineEventPattern,
|
|
13
13
|
EntityHookPattern,
|
|
14
|
-
EventMigrationPattern,
|
|
15
14
|
HookPattern,
|
|
16
15
|
HttpRoutePattern,
|
|
17
16
|
JobPattern,
|
|
@@ -805,6 +804,74 @@ export function extractHttpRoute(
|
|
|
805
804
|
});
|
|
806
805
|
}
|
|
807
806
|
|
|
807
|
+
// Reads defineEvent's `migrations` option — either the array-of-steps shape
|
|
808
|
+
// used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
|
|
809
|
+
// toVersion is redundant on-disk since it is always fromVersion + 1) or the
|
|
810
|
+
// keyed-object canonical shape the Designer renders (`{ "1": transform }`).
|
|
811
|
+
// Keyed by fromVersion (as a string) → the transform closure's location.
|
|
812
|
+
// Skips malformed entries rather than failing the whole defineEvent extract
|
|
813
|
+
// — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
|
|
814
|
+
function extractEventMigrationsFromArray(
|
|
815
|
+
arr: Node,
|
|
816
|
+
sourceFile: SourceFile,
|
|
817
|
+
): Record<string, SourceLocation> {
|
|
818
|
+
const result: Record<string, SourceLocation> = {};
|
|
819
|
+
const arrLit = arr.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
820
|
+
for (const el of arrLit.getElements()) {
|
|
821
|
+
const obj = el.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
822
|
+
if (!obj) continue;
|
|
823
|
+
const fromInit = obj
|
|
824
|
+
.getProperty("fromVersion")
|
|
825
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
826
|
+
?.getInitializer();
|
|
827
|
+
const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
|
|
828
|
+
if (typeof fromVersion !== "number") continue;
|
|
829
|
+
const transformInit = obj
|
|
830
|
+
.getProperty("transform")
|
|
831
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
832
|
+
?.getInitializer();
|
|
833
|
+
const fn = transformInit ? findFunctionLiteral(transformInit) : undefined;
|
|
834
|
+
if (!fn) continue;
|
|
835
|
+
result[String(fromVersion)] = sourceLocationFromNode(fn, sourceFile);
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function extractEventMigrationsFromKeyedObject(
|
|
841
|
+
objLit: Node,
|
|
842
|
+
sourceFile: SourceFile,
|
|
843
|
+
): Record<string, SourceLocation> {
|
|
844
|
+
const result: Record<string, SourceLocation> = {};
|
|
845
|
+
const obj = objLit.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
846
|
+
for (const prop of obj.getProperties()) {
|
|
847
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
848
|
+
const initializer = propAssign?.getInitializer();
|
|
849
|
+
const fn = initializer ? findFunctionLiteral(initializer) : undefined;
|
|
850
|
+
if (!propAssign || !fn) continue;
|
|
851
|
+
result[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
|
|
852
|
+
}
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Reads defineEvent's `migrations` option — either the array-of-steps shape
|
|
857
|
+
// used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
|
|
858
|
+
// toVersion is redundant on-disk since it is always fromVersion + 1) or the
|
|
859
|
+
// keyed-object canonical shape the Designer renders (`{ "1": transform }`).
|
|
860
|
+
// Keyed by fromVersion (as a string) → the transform closure's location.
|
|
861
|
+
// Skips malformed entries rather than failing the whole defineEvent extract
|
|
862
|
+
// — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
|
|
863
|
+
function extractEventMigrationsField(
|
|
864
|
+
node: Node,
|
|
865
|
+
sourceFile: SourceFile,
|
|
866
|
+
): Readonly<Record<string, SourceLocation>> | undefined {
|
|
867
|
+
const result = node.asKind(SyntaxKind.ArrayLiteralExpression)
|
|
868
|
+
? extractEventMigrationsFromArray(node, sourceFile)
|
|
869
|
+
: node.asKind(SyntaxKind.ObjectLiteralExpression)
|
|
870
|
+
? extractEventMigrationsFromKeyedObject(node, sourceFile)
|
|
871
|
+
: undefined;
|
|
872
|
+
return result && Object.keys(result).length > 0 ? result : undefined;
|
|
873
|
+
}
|
|
874
|
+
|
|
808
875
|
export function extractDefineEvent(
|
|
809
876
|
call: CallExpression,
|
|
810
877
|
sourceFile: SourceFile,
|
|
@@ -853,12 +920,20 @@ export function extractDefineEvent(
|
|
|
853
920
|
const v = readDataLiteralNode(versionInit);
|
|
854
921
|
if (typeof v === "number") version = v;
|
|
855
922
|
}
|
|
923
|
+
const migrationsInit = obj
|
|
924
|
+
.getProperty("migrations")
|
|
925
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
926
|
+
?.getInitializer();
|
|
927
|
+
const migrations = migrationsInit
|
|
928
|
+
? extractEventMigrationsField(migrationsInit, sourceFile)
|
|
929
|
+
: undefined;
|
|
856
930
|
return ok({
|
|
857
931
|
kind: "defineEvent",
|
|
858
932
|
source: sourceLocationFromNode(call, sourceFile),
|
|
859
933
|
eventName: nameInit.getLiteralValue(),
|
|
860
934
|
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
861
935
|
...(version !== undefined && { version }),
|
|
936
|
+
...(migrations !== undefined && { migrations }),
|
|
862
937
|
});
|
|
863
938
|
}
|
|
864
939
|
|
|
@@ -879,152 +954,33 @@ export function extractDefineEvent(
|
|
|
879
954
|
);
|
|
880
955
|
}
|
|
881
956
|
let version: number | undefined;
|
|
957
|
+
let migrations: Readonly<Record<string, SourceLocation>> | undefined;
|
|
882
958
|
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")
|
|
959
|
+
const optionsObj = optionsArg?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
960
|
+
if (optionsObj) {
|
|
961
|
+
const versionInit = optionsObj
|
|
962
|
+
.getProperty("version")
|
|
940
963
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
941
964
|
?.getInitializer();
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
"eventMigration",
|
|
946
|
-
sourceLocationFromNode(call, sourceFile),
|
|
947
|
-
"toVersion must be a numeric literal",
|
|
948
|
-
);
|
|
965
|
+
if (versionInit) {
|
|
966
|
+
const v = readDataLiteralNode(versionInit);
|
|
967
|
+
if (typeof v === "number") version = v;
|
|
949
968
|
}
|
|
950
|
-
const
|
|
951
|
-
.getProperty("
|
|
969
|
+
const migrationsInit = optionsObj
|
|
970
|
+
.getProperty("migrations")
|
|
952
971
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
953
972
|
?.getInitializer();
|
|
954
|
-
if (
|
|
955
|
-
|
|
956
|
-
"eventMigration",
|
|
957
|
-
sourceLocationFromNode(call, sourceFile),
|
|
958
|
-
"object form requires a `transform` property",
|
|
959
|
-
);
|
|
973
|
+
if (migrationsInit) {
|
|
974
|
+
migrations = extractEventMigrationsField(migrationsInit, sourceFile);
|
|
960
975
|
}
|
|
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
976
|
}
|
|
1021
977
|
return ok({
|
|
1022
|
-
kind: "
|
|
978
|
+
kind: "defineEvent",
|
|
1023
979
|
source: sourceLocationFromNode(call, sourceFile),
|
|
1024
980
|
eventName: nameArg.getLiteralValue(),
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
981
|
+
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
982
|
+
...(version !== undefined && { version }),
|
|
983
|
+
...(migrations !== undefined && { migrations }),
|
|
1028
984
|
});
|
|
1029
985
|
}
|
|
1030
986
|
|
|
@@ -28,7 +28,6 @@ export type {
|
|
|
28
28
|
AddDefineEventArgs,
|
|
29
29
|
AddEntityArgs,
|
|
30
30
|
AddEntityHookArgs,
|
|
31
|
-
AddEventMigrationArgs,
|
|
32
31
|
AddHookArgs,
|
|
33
32
|
AddHttpRouteArgs,
|
|
34
33
|
AddJobArgs,
|
|
@@ -62,7 +61,6 @@ export type {
|
|
|
62
61
|
EntityHookPattern,
|
|
63
62
|
// Static patterns
|
|
64
63
|
EntityPattern,
|
|
65
|
-
EventMigrationPattern,
|
|
66
64
|
ExtendsRegistrarPattern,
|
|
67
65
|
FeaturePattern,
|
|
68
66
|
FeaturePatternKind,
|
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
extractEntity,
|
|
41
41
|
extractEntityHook,
|
|
42
42
|
extractEnvSchema,
|
|
43
|
-
extractEventMigration,
|
|
44
43
|
extractExposesApi,
|
|
45
44
|
extractExtendsRegistrar,
|
|
46
45
|
extractHook,
|
|
@@ -457,8 +456,6 @@ function dispatchExtractor(
|
|
|
457
456
|
return extractHttpRoute(call, sourceFile);
|
|
458
457
|
case "defineEvent":
|
|
459
458
|
return extractDefineEvent(call, sourceFile);
|
|
460
|
-
case "eventMigration":
|
|
461
|
-
return extractEventMigration(call, sourceFile);
|
|
462
459
|
case "notification":
|
|
463
460
|
return extractNotification(call, sourceFile);
|
|
464
461
|
case "projection":
|
|
@@ -66,12 +66,6 @@ export type PatternId =
|
|
|
66
66
|
| { readonly kind: "projection"; readonly name: string }
|
|
67
67
|
| { readonly kind: "multiStreamProjection"; readonly name: string }
|
|
68
68
|
| { 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
69
|
| { readonly kind: "extendsRegistrar"; readonly extensionName: string }
|
|
76
70
|
// Singleton patterns — only one per feature, kind alone identifies them.
|
|
77
71
|
| { readonly kind: "requires" }
|
|
@@ -419,20 +413,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
419
413
|
return (
|
|
420
414
|
matchFirstArgString(call, id.eventName) || matchObjectProperty(call, "name", id.eventName)
|
|
421
415
|
);
|
|
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
416
|
case "extendsRegistrar":
|
|
437
417
|
return matchFirstArgString(call, id.extensionName);
|
|
438
418
|
default: {
|
|
@@ -459,27 +439,6 @@ function matchObjectProperty(call: CallExpression, propName: string, expected: s
|
|
|
459
439
|
return init?.getLiteralValue() === expected;
|
|
460
440
|
}
|
|
461
441
|
|
|
462
|
-
function matchObjectNumericProperty(
|
|
463
|
-
call: CallExpression,
|
|
464
|
-
propName: string,
|
|
465
|
-
expected: number,
|
|
466
|
-
): boolean {
|
|
467
|
-
const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
468
|
-
if (!obj) return false;
|
|
469
|
-
const init = obj
|
|
470
|
-
.getProperty(propName)
|
|
471
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
472
|
-
?.getInitializer()
|
|
473
|
-
?.asKind(SyntaxKind.NumericLiteral);
|
|
474
|
-
return init !== undefined && Number(init.getText()) === expected;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function numericArg(call: CallExpression, idx: number): number | undefined {
|
|
478
|
-
const lit = call.getArguments()[idx]?.asKind(SyntaxKind.NumericLiteral);
|
|
479
|
-
if (!lit) return undefined;
|
|
480
|
-
return Number(lit.getText());
|
|
481
|
-
}
|
|
482
|
-
|
|
483
442
|
// =============================================================================
|
|
484
443
|
// Format helpers — line boundaries / blank-line collapse
|
|
485
444
|
// (indent / PATTERN_INDENT live in render.ts and are imported above.)
|
|
@@ -144,13 +144,10 @@ export type AddDefineEventArgs = {
|
|
|
144
144
|
readonly name: string;
|
|
145
145
|
readonly schemaSource: string;
|
|
146
146
|
readonly version?: number;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
readonly
|
|
151
|
-
readonly fromVersion: number;
|
|
152
|
-
readonly toVersion: number;
|
|
153
|
-
readonly transformSource: string;
|
|
147
|
+
// Keyed by fromVersion (as a string) → the transform source text for the
|
|
148
|
+
// fromVersion -> fromVersion+1 step. Folded in from the former
|
|
149
|
+
// addEventMigration (#1082 step 8).
|
|
150
|
+
readonly migrations?: Readonly<Record<string, string>>;
|
|
154
151
|
};
|
|
155
152
|
|
|
156
153
|
export type AddProjectionArgs = {
|
|
@@ -249,7 +246,6 @@ export type FeaturePatcher = {
|
|
|
249
246
|
readonly addProjection: (args: AddProjectionArgs) => void;
|
|
250
247
|
readonly addMultiStreamProjection: (args: AddMultiStreamProjectionArgs) => void;
|
|
251
248
|
readonly addDefineEvent: (args: AddDefineEventArgs) => void;
|
|
252
|
-
readonly addEventMigration: (args: AddEventMigrationArgs) => void;
|
|
253
249
|
// --- Symmetric ops (id-driven) ---
|
|
254
250
|
readonly replace: (id: PatternId, pattern: FeaturePattern) => void;
|
|
255
251
|
readonly remove: (id: PatternId) => void;
|
|
@@ -495,24 +491,22 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
495
491
|
});
|
|
496
492
|
},
|
|
497
493
|
|
|
498
|
-
addDefineEvent({ name, schemaSource, version }) {
|
|
494
|
+
addDefineEvent({ name, schemaSource, version, migrations }) {
|
|
495
|
+
const migrationLocs = migrations
|
|
496
|
+
? Object.fromEntries(
|
|
497
|
+
Object.entries(migrations).map(([fromVersion, source]) => [
|
|
498
|
+
fromVersion,
|
|
499
|
+
rawLoc(source),
|
|
500
|
+
]),
|
|
501
|
+
)
|
|
502
|
+
: undefined;
|
|
499
503
|
add({
|
|
500
504
|
kind: "defineEvent",
|
|
501
505
|
source: SYNTHETIC_LOC,
|
|
502
506
|
eventName: name,
|
|
503
507
|
schemaSource: rawLoc(schemaSource),
|
|
504
508
|
...(version !== undefined && { version }),
|
|
505
|
-
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
addEventMigration({ event, fromVersion, toVersion, transformSource }) {
|
|
509
|
-
add({
|
|
510
|
-
kind: "eventMigration",
|
|
511
|
-
source: SYNTHETIC_LOC,
|
|
512
|
-
eventName: event,
|
|
513
|
-
fromVersion,
|
|
514
|
-
toVersion,
|
|
515
|
-
transformBody: rawLoc(transformSource),
|
|
509
|
+
...(migrationLocs !== undefined && { migrations: migrationLocs }),
|
|
516
510
|
});
|
|
517
511
|
},
|
|
518
512
|
|
|
@@ -501,29 +501,22 @@ export type MultiStreamProjectionPattern = {
|
|
|
501
501
|
// `r.defineEvent(name, schema, options?)` — registers an event payload
|
|
502
502
|
// shape and returns the qualified `EventDef`, so callers pass `.name` to
|
|
503
503
|
// `ctx.appendEvent` instead of hand-building `<feature>:event:<short>`.
|
|
504
|
-
// `options.version` declares the CURRENT schema generation (default 1)
|
|
505
|
-
//
|
|
506
|
-
//
|
|
504
|
+
// `options.version` declares the CURRENT schema generation (default 1).
|
|
505
|
+
// `options.migrations` is a step-wise upcast chain — keyed by fromVersion
|
|
506
|
+
// (toVersion is always fromVersion + 1, enforced at registration, so it is
|
|
507
|
+
// not stored separately); the framework refuses to boot if the chain from
|
|
508
|
+
// 1 to `version` has gaps. Formerly a separate `r.eventMigration()` call
|
|
509
|
+
// per step, folded in here (#1082 step 8) — an event and its schema
|
|
510
|
+
// evolution are one lifecycle, not two registrar concepts.
|
|
507
511
|
export type DefineEventPattern = {
|
|
508
512
|
readonly kind: "defineEvent";
|
|
509
513
|
readonly source: SourceLocation;
|
|
510
514
|
readonly eventName: string;
|
|
511
515
|
readonly schemaSource: SourceLocation;
|
|
512
516
|
readonly version?: number;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// registers a step-wise payload upcast for event-schema evolution.
|
|
517
|
-
// `toVersion` must be `fromVersion + 1`; chain larger jumps step by step.
|
|
518
|
-
// Transforms are pure old-payload-in/new-payload-out functions and run once
|
|
519
|
-
// per READ, not once per event persisted — keep them cheap.
|
|
520
|
-
export type EventMigrationPattern = {
|
|
521
|
-
readonly kind: "eventMigration";
|
|
522
|
-
readonly source: SourceLocation;
|
|
523
|
-
readonly eventName: string;
|
|
524
|
-
readonly fromVersion: number;
|
|
525
|
-
readonly toVersion: number;
|
|
526
|
-
readonly transformBody: SourceLocation;
|
|
517
|
+
// Map fromVersion (as string, e.g. "1") → SourceLocation of the transform
|
|
518
|
+
// closure for the fromVersion → fromVersion+1 step.
|
|
519
|
+
readonly migrations?: Readonly<Record<string, SourceLocation>>;
|
|
527
520
|
};
|
|
528
521
|
|
|
529
522
|
// `r.extendsRegistrar(extensionName, def)` — declares a named, globally
|
|
@@ -618,7 +611,6 @@ export type FeaturePattern =
|
|
|
618
611
|
| ProjectionPattern
|
|
619
612
|
| MultiStreamProjectionPattern
|
|
620
613
|
| DefineEventPattern
|
|
621
|
-
| EventMigrationPattern
|
|
622
614
|
| ExtendsRegistrarPattern
|
|
623
615
|
| EnvSchemaPattern
|
|
624
616
|
// Catch-all
|
|
@@ -674,7 +666,6 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
674
666
|
case "projection":
|
|
675
667
|
case "multiStreamProjection":
|
|
676
668
|
case "defineEvent":
|
|
677
|
-
case "eventMigration":
|
|
678
669
|
return "mixed";
|
|
679
670
|
case "authClaims":
|
|
680
671
|
case "extendsRegistrar":
|
|
@@ -26,7 +26,6 @@ import type {
|
|
|
26
26
|
EntityHookPattern,
|
|
27
27
|
EntityPattern,
|
|
28
28
|
EnvSchemaPattern,
|
|
29
|
-
EventMigrationPattern,
|
|
30
29
|
ExposesApiPattern,
|
|
31
30
|
ExtendsRegistrarPattern,
|
|
32
31
|
FeaturePattern,
|
|
@@ -129,8 +128,6 @@ export function renderPattern(pattern: FeaturePattern): string {
|
|
|
129
128
|
return renderMultiStreamProjection(pattern);
|
|
130
129
|
case "defineEvent":
|
|
131
130
|
return renderDefineEvent(pattern);
|
|
132
|
-
case "eventMigration":
|
|
133
|
-
return renderEventMigration(pattern);
|
|
134
131
|
case "extendsRegistrar":
|
|
135
132
|
return renderExtendsRegistrar(pattern);
|
|
136
133
|
case "usesApi":
|
|
@@ -505,16 +502,16 @@ function renderDefineEvent(p: DefineEventPattern): string {
|
|
|
505
502
|
lines.push(` name: ${JSON.stringify(p.eventName)},`);
|
|
506
503
|
lines.push(` schema: ${p.schemaSource.raw},`);
|
|
507
504
|
if (p.version !== undefined) lines.push(` version: ${p.version},`);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
505
|
+
if (p.migrations !== undefined) {
|
|
506
|
+
const entries = Object.entries(p.migrations);
|
|
507
|
+
if (entries.length > 0) {
|
|
508
|
+
lines.push(" migrations: {");
|
|
509
|
+
for (const [fromVersion, transformBody] of entries) {
|
|
510
|
+
lines.push(` "${fromVersion}": ${transformBody.raw},`);
|
|
511
|
+
}
|
|
512
|
+
lines.push(" },");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
518
515
|
lines.push("});");
|
|
519
516
|
return lines.join("\n");
|
|
520
517
|
}
|