@cosmicdrift/kumiko-framework 0.154.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/engine/__tests__/event-migration-declarative.test.ts +9 -2
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.154.
|
|
3
|
+
"version": "0.154.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
"zod": "^4.4.3"
|
|
194
194
|
},
|
|
195
195
|
"devDependencies": {
|
|
196
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.154.
|
|
196
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.154.1",
|
|
197
197
|
"bun-types": "^1.3.13",
|
|
198
198
|
"pino-pretty": "^13.1.3"
|
|
199
199
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { defineFeature } from "../define-feature";
|
|
3
4
|
import type { DeclarativeEventMigration, EventUpcastCtx } from "../types";
|
|
4
5
|
|
|
@@ -7,7 +8,10 @@ const upcastCtx = {} as EventUpcastCtx;
|
|
|
7
8
|
|
|
8
9
|
function compile(spec: DeclarativeEventMigration) {
|
|
9
10
|
const feature = defineFeature("billing", (r) => {
|
|
10
|
-
r.
|
|
11
|
+
r.defineEvent("invoicePaid", z.unknown(), {
|
|
12
|
+
version: 2,
|
|
13
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: spec }],
|
|
14
|
+
});
|
|
11
15
|
});
|
|
12
16
|
const def = feature.eventMigrations["invoicePaid"]?.[0];
|
|
13
17
|
if (!def) throw new Error("migration not registered");
|
|
@@ -51,7 +55,10 @@ describe("declarative eventMigration", () => {
|
|
|
51
55
|
test("imperative function variant is stored untouched", () => {
|
|
52
56
|
const fn = (payload: unknown) => payload;
|
|
53
57
|
const feature = defineFeature("billing", (r) => {
|
|
54
|
-
r.
|
|
58
|
+
r.defineEvent("invoicePaid", z.unknown(), {
|
|
59
|
+
version: 2,
|
|
60
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: fn }],
|
|
61
|
+
});
|
|
55
62
|
});
|
|
56
63
|
expect(feature.eventMigrations["invoicePaid"]?.[0]?.transform).toBe(fn);
|
|
57
64
|
});
|
|
@@ -56,7 +56,10 @@ defineFeature("todoList", (r) => {
|
|
|
56
56
|
r.defineEvent({
|
|
57
57
|
name: "taskCompleted",
|
|
58
58
|
schema: z.object({ id: z.string() }),
|
|
59
|
-
version:
|
|
59
|
+
version: 2,
|
|
60
|
+
migrations: {
|
|
61
|
+
"1": (old) => ({ ...old, done: true }),
|
|
62
|
+
},
|
|
60
63
|
});
|
|
61
64
|
|
|
62
65
|
r.writeHandler({
|
|
@@ -127,13 +130,6 @@ defineFeature("todoList", (r) => {
|
|
|
127
130
|
|
|
128
131
|
r.useExtension({ name: "auditLog", entity: "task" });
|
|
129
132
|
|
|
130
|
-
r.eventMigration({
|
|
131
|
-
event: "taskCompleted",
|
|
132
|
-
fromVersion: 1,
|
|
133
|
-
toVersion: 2,
|
|
134
|
-
transform: (old) => ({ ...old, done: true }),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
133
|
r.job({
|
|
138
134
|
name: "cleanupExpired",
|
|
139
135
|
schedule: { cron: "0 3 * * *" },
|
|
@@ -232,7 +228,6 @@ describe("Canonical Object-Form — parser akzeptiert + extrahiert", () => {
|
|
|
232
228
|
"projection",
|
|
233
229
|
"multiStreamProjection",
|
|
234
230
|
"defineEvent",
|
|
235
|
-
"eventMigration",
|
|
236
231
|
];
|
|
237
232
|
for (const e of expected) {
|
|
238
233
|
expect(kinds.has(e), `expected kind "${e}"`).toBe(true);
|
|
@@ -283,12 +278,13 @@ describe("Canonical Object-Form — parser akzeptiert + extrahiert", () => {
|
|
|
283
278
|
expect(claim).toMatchObject({ kind: "claimKey", shortName: "teamId" });
|
|
284
279
|
});
|
|
285
280
|
|
|
286
|
-
test("defineEvent: name + version aus Object-Form", () => {
|
|
281
|
+
test("defineEvent: name + version + migrations aus Object-Form", () => {
|
|
287
282
|
const ev = result.patterns.find((p) => p.kind === "defineEvent");
|
|
288
283
|
expect(ev).toMatchObject({
|
|
289
284
|
kind: "defineEvent",
|
|
290
285
|
eventName: "taskCompleted",
|
|
291
|
-
version:
|
|
286
|
+
version: 2,
|
|
287
|
+
migrations: { "1": expect.anything() },
|
|
292
288
|
});
|
|
293
289
|
});
|
|
294
290
|
|
|
@@ -334,16 +330,6 @@ describe("Canonical Object-Form — parser akzeptiert + extrahiert", () => {
|
|
|
334
330
|
});
|
|
335
331
|
});
|
|
336
332
|
|
|
337
|
-
test("eventMigration: event + fromVersion + toVersion aus Object-Form", () => {
|
|
338
|
-
const em = result.patterns.find((p) => p.kind === "eventMigration");
|
|
339
|
-
expect(em).toMatchObject({
|
|
340
|
-
kind: "eventMigration",
|
|
341
|
-
eventName: "taskCompleted",
|
|
342
|
-
fromVersion: 1,
|
|
343
|
-
toVersion: 2,
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
333
|
test("job: name + handlerBody.raw enthält den Closure-Body", () => {
|
|
348
334
|
const job = result.patterns.find((p) => p.kind === "job");
|
|
349
335
|
expect(job).toMatchObject({ kind: "job", jobName: "cleanupExpired" });
|
|
@@ -961,19 +961,46 @@ defineFeature("f", (r) => {
|
|
|
961
961
|
});
|
|
962
962
|
});
|
|
963
963
|
|
|
964
|
-
describe("extractEventMigration", () => {
|
|
965
|
-
test("
|
|
964
|
+
describe("extractDefineEvent — migrations (formerly extractEventMigration)", () => {
|
|
965
|
+
test("positional-form migrations array captures the fromVersion→transform step", () => {
|
|
966
966
|
const result = parseInline(`
|
|
967
967
|
defineFeature("f", (r) => {
|
|
968
|
-
r.
|
|
968
|
+
r.defineEvent("incidentOpened", z.object({ id: z.string() }), {
|
|
969
|
+
version: 2,
|
|
970
|
+
migrations: [
|
|
971
|
+
{ fromVersion: 1, toVersion: 2, transform: (payload) => ({ ...payload, severity: "low" }) },
|
|
972
|
+
],
|
|
973
|
+
});
|
|
969
974
|
});
|
|
970
975
|
`);
|
|
971
976
|
|
|
972
977
|
expect(result.patterns[0]).toMatchObject({
|
|
973
|
-
kind: "
|
|
978
|
+
kind: "defineEvent",
|
|
974
979
|
eventName: "incidentOpened",
|
|
975
|
-
|
|
976
|
-
|
|
980
|
+
version: 2,
|
|
981
|
+
migrations: { "1": expect.anything() },
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("object-form migrations map captures the fromVersion→transform step", () => {
|
|
986
|
+
const result = parseInline(`
|
|
987
|
+
defineFeature("f", (r) => {
|
|
988
|
+
r.defineEvent({
|
|
989
|
+
name: "incidentOpened",
|
|
990
|
+
schema: z.object({ id: z.string() }),
|
|
991
|
+
version: 2,
|
|
992
|
+
migrations: {
|
|
993
|
+
"1": (payload) => ({ ...payload, severity: "low" }),
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
`);
|
|
998
|
+
|
|
999
|
+
expect(result.patterns[0]).toMatchObject({
|
|
1000
|
+
kind: "defineEvent",
|
|
1001
|
+
eventName: "incidentOpened",
|
|
1002
|
+
version: 2,
|
|
1003
|
+
migrations: { "1": expect.anything() },
|
|
977
1004
|
});
|
|
978
1005
|
});
|
|
979
1006
|
});
|
|
@@ -301,21 +301,16 @@ describe("patch coverage for the remaining pattern-kinds", () => {
|
|
|
301
301
|
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "authClaims")).toBeUndefined();
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
-
test("add + remove via PatternId for
|
|
304
|
+
test("add + remove via PatternId for defineEvent with migrations", () => {
|
|
305
305
|
const sf = makeSourceFile(STARTER);
|
|
306
|
-
createFeaturePatcher(sf).
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
312
|
-
removePattern(sf, {
|
|
313
|
-
kind: "eventMigration",
|
|
314
|
-
eventName: "itemCreated",
|
|
315
|
-
fromVersion: 1,
|
|
316
|
-
toVersion: 2,
|
|
306
|
+
createFeaturePatcher(sf).addDefineEvent({
|
|
307
|
+
name: "itemCreated",
|
|
308
|
+
schemaSource: "z.object({ id: z.string() })",
|
|
309
|
+
version: 2,
|
|
310
|
+
migrations: { "1": "(old) => old" },
|
|
317
311
|
});
|
|
318
|
-
|
|
312
|
+
removePattern(sf, { kind: "defineEvent", eventName: "itemCreated" });
|
|
313
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "defineEvent")).toBeUndefined();
|
|
319
314
|
});
|
|
320
315
|
});
|
|
321
316
|
|
|
@@ -172,33 +172,23 @@ describe("FeaturePatcher — typed add helpers for mixed (closure-bearing) patte
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
test("addDefineEvent
|
|
175
|
+
test("addDefineEvent with migrations forms a complete event-versioning chain", () => {
|
|
176
176
|
const sf = makeSourceFile(STARTER);
|
|
177
177
|
const p = createFeaturePatcher(sf);
|
|
178
178
|
p.addDefineEvent({
|
|
179
179
|
name: "stepCompleted",
|
|
180
180
|
schemaSource: "z.object({ id: z.string() })",
|
|
181
181
|
version: 2,
|
|
182
|
-
|
|
183
|
-
p.addEventMigration({
|
|
184
|
-
event: "stepCompleted",
|
|
185
|
-
fromVersion: 1,
|
|
186
|
-
toVersion: 2,
|
|
187
|
-
transformSource: '(old) => ({ id: old.id ?? "" })',
|
|
182
|
+
migrations: { "1": '(old) => ({ id: old.id ?? "" })' },
|
|
188
183
|
});
|
|
189
184
|
const result = parseSourceFile(sf);
|
|
190
185
|
expect(result.errors).toEqual([]);
|
|
191
|
-
expect(result.patterns).toHaveLength(
|
|
186
|
+
expect(result.patterns).toHaveLength(1);
|
|
192
187
|
expect(result.patterns[0]).toMatchObject({
|
|
193
188
|
kind: "defineEvent",
|
|
194
189
|
eventName: "stepCompleted",
|
|
195
190
|
version: 2,
|
|
196
|
-
|
|
197
|
-
expect(result.patterns[1]).toMatchObject({
|
|
198
|
-
kind: "eventMigration",
|
|
199
|
-
eventName: "stepCompleted",
|
|
200
|
-
fromVersion: 1,
|
|
201
|
-
toVersion: 2,
|
|
191
|
+
migrations: { "1": expect.anything() },
|
|
202
192
|
});
|
|
203
193
|
});
|
|
204
194
|
});
|
|
@@ -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.)
|