@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
|
@@ -56,21 +56,32 @@ const orderFeature = defineFeature("upcastshop", (r) => {
|
|
|
56
56
|
const orderPriced = r.defineEvent(
|
|
57
57
|
"priced",
|
|
58
58
|
z.object({ totalCents: z.number().int(), currency: z.string() }),
|
|
59
|
-
{
|
|
59
|
+
{
|
|
60
|
+
version: 3,
|
|
61
|
+
migrations: [
|
|
62
|
+
// v1 → v2: renamed totalEuros → total (kept as string for this step)
|
|
63
|
+
{
|
|
64
|
+
fromVersion: 1,
|
|
65
|
+
toVersion: 2,
|
|
66
|
+
transform: (payload) => {
|
|
67
|
+
const p = payload as { totalEuros: string };
|
|
68
|
+
return { total: p.totalEuros, currency: "EUR" };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
// v2 → v3: parse "total" string into integer cents
|
|
72
|
+
{
|
|
73
|
+
fromVersion: 2,
|
|
74
|
+
toVersion: 3,
|
|
75
|
+
transform: (payload) => {
|
|
76
|
+
const p = payload as { total: string; currency: string };
|
|
77
|
+
const euros = Number.parseFloat(p.total);
|
|
78
|
+
return { totalCents: Math.round(euros * 100), currency: p.currency };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
60
83
|
);
|
|
61
84
|
|
|
62
|
-
// v1 → v2: renamed totalEuros → total (kept as string for this step)
|
|
63
|
-
r.eventMigration("priced", 1, 2, (payload) => {
|
|
64
|
-
const p = payload as { totalEuros: string };
|
|
65
|
-
return { total: p.totalEuros, currency: "EUR" };
|
|
66
|
-
});
|
|
67
|
-
// v2 → v3: parse "total" string into integer cents
|
|
68
|
-
r.eventMigration("priced", 2, 3, (payload) => {
|
|
69
|
-
const p = payload as { total: string; currency: string };
|
|
70
|
-
const euros = Number.parseFloat(p.total);
|
|
71
|
-
return { totalCents: Math.round(euros * 100), currency: p.currency };
|
|
72
|
-
});
|
|
73
|
-
|
|
74
85
|
r.projection({
|
|
75
86
|
name: "order-summary",
|
|
76
87
|
source: "upcast-order",
|
|
@@ -309,18 +320,27 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
|
|
|
309
320
|
const placed = r.defineEvent(
|
|
310
321
|
"placed",
|
|
311
322
|
z.object({ customerId: z.string(), segment: z.string() }),
|
|
312
|
-
{
|
|
323
|
+
{
|
|
324
|
+
version: 2,
|
|
325
|
+
migrations: [
|
|
326
|
+
{
|
|
327
|
+
fromVersion: 1,
|
|
328
|
+
toVersion: 2,
|
|
329
|
+
transform: async (payload, ctx) => {
|
|
330
|
+
const p = payload as { customerId: string };
|
|
331
|
+
const [row] = await selectMany(ctx.db, customerSegments, {
|
|
332
|
+
customerId: p.customerId,
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
customerId: p.customerId,
|
|
336
|
+
segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
313
342
|
);
|
|
314
343
|
|
|
315
|
-
r.eventMigration("placed", 1, 2, async (payload, ctx) => {
|
|
316
|
-
const p = payload as { customerId: string };
|
|
317
|
-
const [row] = await selectMany(ctx.db, customerSegments, { customerId: p.customerId });
|
|
318
|
-
return {
|
|
319
|
-
customerId: p.customerId,
|
|
320
|
-
segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
|
-
|
|
324
344
|
r.projection({
|
|
325
345
|
name: "async-summary",
|
|
326
346
|
source: "upcast-async-order",
|
|
@@ -385,26 +405,28 @@ describe("upcaster: boot-time validation", () => {
|
|
|
385
405
|
test("defineEvent with version=N and only partial migrations fails at registry build", () => {
|
|
386
406
|
const incomplete = defineFeature("holes", (r) => {
|
|
387
407
|
r.entity("hole-order", orderEntity);
|
|
388
|
-
r.defineEvent("bad", z.object({ v3: z.string() }), { version: 3 });
|
|
389
408
|
// Only 1→2 registered — the 2→3 gap must be rejected.
|
|
390
|
-
r.
|
|
409
|
+
r.defineEvent("bad", z.object({ v3: z.string() }), {
|
|
410
|
+
version: 3,
|
|
411
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
|
|
412
|
+
});
|
|
391
413
|
});
|
|
392
414
|
expect(() => createRegistry([incomplete])).toThrow(/v2.*v3|covers the step v2/);
|
|
393
415
|
});
|
|
394
416
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
expect(() => createRegistry([orphan])).toThrow(/no r\.defineEvent/i);
|
|
401
|
-
});
|
|
417
|
+
// "migration declared but no defineEvent" is now structurally impossible:
|
|
418
|
+
// migrations live in defineEvent's `migrations` option, always scoped to
|
|
419
|
+
// the event being defined in that same call — there is no longer a
|
|
420
|
+
// registrar shape that can express a dangling migration for an
|
|
421
|
+
// undefined event (formerly the "ghost" registry-validate error path).
|
|
402
422
|
|
|
403
423
|
test("migration toVersion > defineEvent version → rejected", () => {
|
|
404
424
|
const future = defineFeature("future", (r) => {
|
|
405
425
|
r.entity("future-order", orderEntity);
|
|
406
|
-
r.defineEvent("early", z.object({ x: z.number() }), {
|
|
407
|
-
|
|
426
|
+
r.defineEvent("early", z.object({ x: z.number() }), {
|
|
427
|
+
version: 1,
|
|
428
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
|
|
429
|
+
});
|
|
408
430
|
});
|
|
409
431
|
expect(() => createRegistry([future])).toThrow(/declares only version 1/);
|
|
410
432
|
});
|
|
@@ -412,34 +434,44 @@ describe("upcaster: boot-time validation", () => {
|
|
|
412
434
|
test("non-contiguous (1→2 and 3→4 without 2→3) → rejected", () => {
|
|
413
435
|
const gaps = defineFeature("gaps", (r) => {
|
|
414
436
|
r.entity("gap-order", orderEntity);
|
|
415
|
-
r.defineEvent("jumpy", z.object({ v: z.number() }), {
|
|
416
|
-
|
|
417
|
-
|
|
437
|
+
r.defineEvent("jumpy", z.object({ v: z.number() }), {
|
|
438
|
+
version: 4,
|
|
439
|
+
migrations: [
|
|
440
|
+
{ fromVersion: 1, toVersion: 2, transform: (p) => p },
|
|
441
|
+
{ fromVersion: 3, toVersion: 4, transform: (p) => p },
|
|
442
|
+
],
|
|
443
|
+
});
|
|
418
444
|
});
|
|
419
445
|
expect(() => createRegistry([gaps])).toThrow(/v2.*v3|covers the step v2/);
|
|
420
446
|
});
|
|
421
447
|
});
|
|
422
448
|
|
|
423
449
|
describe("upcaster: registrar input validation", () => {
|
|
424
|
-
test("
|
|
450
|
+
test("defineEvent migrations reject multi-step jumps", () => {
|
|
425
451
|
expect(() =>
|
|
426
452
|
defineFeature("bigstep", (r) => {
|
|
427
453
|
r.entity("bigstep-order", orderEntity);
|
|
428
|
-
r.defineEvent("biz", z.object({ x: z.number() }), {
|
|
429
|
-
|
|
454
|
+
r.defineEvent("biz", z.object({ x: z.number() }), {
|
|
455
|
+
version: 3,
|
|
456
|
+
migrations: [{ fromVersion: 1, toVersion: 3, transform: (p) => p }],
|
|
457
|
+
});
|
|
430
458
|
}),
|
|
431
459
|
).toThrow(/single-step/);
|
|
432
460
|
});
|
|
433
461
|
|
|
434
|
-
test("
|
|
462
|
+
test("defineEvent migrations reject duplicate step", () => {
|
|
435
463
|
expect(() =>
|
|
436
464
|
defineFeature("dupestep", (r) => {
|
|
437
465
|
r.entity("dup-order", orderEntity);
|
|
438
|
-
r.defineEvent("dup", z.object({ x: z.number() }), {
|
|
439
|
-
|
|
440
|
-
|
|
466
|
+
r.defineEvent("dup", z.object({ x: z.number() }), {
|
|
467
|
+
version: 2,
|
|
468
|
+
migrations: [
|
|
469
|
+
{ fromVersion: 1, toVersion: 2, transform: (p) => p },
|
|
470
|
+
{ fromVersion: 1, toVersion: 2, transform: (p) => p },
|
|
471
|
+
],
|
|
472
|
+
});
|
|
441
473
|
}),
|
|
442
|
-
).toThrow(/already
|
|
474
|
+
).toThrow(/already declared/);
|
|
443
475
|
});
|
|
444
476
|
|
|
445
477
|
test("r.defineEvent rejects non-positive version", () => {
|
|
@@ -45,15 +45,23 @@ const asOfFeature = defineFeature("asoftest", (r) => {
|
|
|
45
45
|
const approved = r.defineEvent(
|
|
46
46
|
"approved",
|
|
47
47
|
z.object({ amount: z.number().int(), approvedBy: z.string() }),
|
|
48
|
-
{
|
|
48
|
+
{
|
|
49
|
+
version: 2,
|
|
50
|
+
migrations: [
|
|
51
|
+
{
|
|
52
|
+
fromVersion: 1,
|
|
53
|
+
toVersion: 2,
|
|
54
|
+
transform: (payload) => {
|
|
55
|
+
const p = payload as { amount: string; approvedBy: string };
|
|
56
|
+
return {
|
|
57
|
+
amount: Math.round(Number.parseFloat(p.amount) * 100),
|
|
58
|
+
approvedBy: p.approvedBy,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
49
64
|
);
|
|
50
|
-
r.eventMigration("approved", 1, 2, (payload) => {
|
|
51
|
-
const p = payload as { amount: string; approvedBy: string };
|
|
52
|
-
return {
|
|
53
|
-
amount: Math.round(Number.parseFloat(p.amount) * 100),
|
|
54
|
-
approvedBy: p.approvedBy,
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
65
|
|
|
58
66
|
const executor = createEventStoreExecutor(invoiceTable, invoiceEntity, {
|
|
59
67
|
entityName: "asof-invoice",
|