@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.
@@ -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
- { version: 3 },
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
- { version: 2 },
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.eventMigration("bad", 1, 2, (p) => p);
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
- test("migration declared but no defineEvent rejected", () => {
396
- const orphan = defineFeature("orphans", (r) => {
397
- r.entity("orph-order", orderEntity);
398
- r.eventMigration("ghost", 1, 2, (p) => p);
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() }), { version: 1 });
407
- r.eventMigration("early", 1, 2, (p) => p);
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() }), { version: 4 });
416
- r.eventMigration("jumpy", 1, 2, (p) => p);
417
- r.eventMigration("jumpy", 3, 4, (p) => p);
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("r.eventMigration rejects multi-step jumps", () => {
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() }), { version: 3 });
429
- r.eventMigration("biz", 1, 3, (p) => p);
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("r.eventMigration rejects duplicate step", () => {
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() }), { version: 2 });
439
- r.eventMigration("dup", 1, 2, (p) => p);
440
- r.eventMigration("dup", 1, 2, (p) => p);
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 registered/);
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
- { version: 2 },
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",