@cosmicdrift/kumiko-framework 0.31.0 → 0.31.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.31.0",
3
+ "version": "0.31.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>",
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { asEntityTableMeta } from "../../bun-db/query";
3
+ import { createEntity } from "../../engine/factories";
4
+ import type { ColumnMeta, IndexMeta } from "../entity-table-meta";
5
+ import { buildEntityTableMeta } from "../entity-table-meta";
6
+ import { buildEntityTable } from "../table-builder";
7
+
8
+ // Lock-step-Guard: buildEntityTable (Runtime-/Test-Stack-Pfad, Meta am
9
+ // KUMIKO_META_SYMBOL) und buildEntityTableMeta (Migrations-Pfad) müssen
10
+ // für dieselbe EntityDefinition identische Spalten + Indexes produzieren.
11
+ // Drift hier = Migration und Prod-Tabelle (bzw. collectTableMetas-Output)
12
+ // gehen auseinander — gefunden als #255-Follow-up: select/number/bigInt
13
+ // verloren ihre deklarierten defaults auf dem Builder-Pfad.
14
+
15
+ const entityWithDefaults = createEntity({
16
+ table: "read_lockstep_probe",
17
+ fields: {
18
+ title: { type: "text", required: true, default: "untitled" },
19
+ active: { type: "boolean", default: true },
20
+ status: { type: "select", options: ["open", "done"], required: true, default: "open" },
21
+ tags: { type: "multiSelect", options: ["a", "b"] },
22
+ attempt: { type: "number", required: true, default: 1 },
23
+ bytes: { type: "bigInt", default: 0 },
24
+ price: { type: "money" },
25
+ meta: { type: "embedded", fields: {} },
26
+ startedAt: { type: "timestamp", required: true },
27
+ },
28
+ });
29
+
30
+ function byName<T extends { name: string }>(items: readonly T[]): readonly T[] {
31
+ return [...items].sort((a, b) => a.name.localeCompare(b.name));
32
+ }
33
+
34
+ describe("buildEntityTable ↔ buildEntityTableMeta lock-step", () => {
35
+ const fromBuilder = asEntityTableMeta(buildEntityTable("lockstepProbe", entityWithDefaults));
36
+ const fromMeta = buildEntityTableMeta("lockstepProbe", entityWithDefaults);
37
+
38
+ test("builder table carries an EntityTableMeta", () => {
39
+ expect(fromBuilder).toBeDefined();
40
+ });
41
+
42
+ test("identical columns (incl. declared defaults)", () => {
43
+ expect(byName<ColumnMeta>(fromBuilder?.columns ?? [])).toEqual(
44
+ byName<ColumnMeta>(fromMeta.columns),
45
+ );
46
+ });
47
+
48
+ test("identical indexes", () => {
49
+ expect(byName<IndexMeta>(fromBuilder?.indexes ?? [])).toEqual(
50
+ byName<IndexMeta>(fromMeta.indexes),
51
+ );
52
+ });
53
+
54
+ test("declared defaults survive the builder path", () => {
55
+ const cols = new Map((fromBuilder?.columns ?? []).map((c) => [c.name, c]));
56
+ expect(cols.get("status")?.defaultSql).toBe("'open'");
57
+ expect(cols.get("attempt")?.defaultSql).toBe("1");
58
+ expect(cols.get("bytes")?.defaultSql).toBe("0");
59
+ expect(cols.get("title")?.defaultSql).toBe("'untitled'");
60
+ expect(cols.get("active")?.defaultSql).toBe("true");
61
+ });
62
+ });
package/src/db/dialect.ts CHANGED
@@ -272,8 +272,11 @@ export function instant(
272
272
  }
273
273
 
274
274
  // moneyAmount kept as a customType-style API but produces a bigint column.
275
+ // bigintJsMode "bigint" — money cents must round-trip as JS bigint (lock-step
276
+ // with entity-table-meta's money rendering; without it bun-db reads the
277
+ // column as number and loses precision past 2^53).
275
278
  export const moneyAmount = (name: string): ColumnBuilder<number> =>
276
- buildColumn(name, "bigint") as ColumnBuilder<number>;
279
+ buildColumn(name, "bigint", { bigintJsMode: "bigint" }) as ColumnBuilder<number>;
277
280
 
278
281
  // ---- Index + primaryKey helpers ----
279
282
 
@@ -80,7 +80,12 @@ function fieldToColumns(
80
80
  : boolean(snakeName),
81
81
  };
82
82
  case "select": {
83
- const col = text(snakeName);
83
+ // default() durchreichen — entity-table-meta rendert deklarierte
84
+ // defaults, dieser Builder MUSS lock-step bleiben (sonst trägt das
85
+ // Meta am buildEntityTable-Objekt eine andere Spalte als die
86
+ // Migration, #255-Follow-up-Befund).
87
+ const base = text(snakeName);
88
+ const col = field.default !== undefined ? base.default(field.default) : base;
84
89
  return { [name]: field.required ? col.notNull() : col };
85
90
  }
86
91
  case "multiSelect":
@@ -95,7 +100,8 @@ function fieldToColumns(
95
100
  // multi-select.
96
101
  return { [name]: jsonb(snakeName).default([]).notNull() };
97
102
  case "number": {
98
- const col = integer(snakeName);
103
+ const base = integer(snakeName);
104
+ const col = field.default !== undefined ? base.default(field.default) : base;
99
105
  return { [name]: field.required ? col.notNull() : col };
100
106
  }
101
107
  case "bigInt": {
@@ -104,7 +110,8 @@ function fieldToColumns(
104
110
  // JS-`bigint` — JSON-serialisierbar, Frontend-tauglich. Wer >2^53
105
111
  // braucht (Astronomie-Astronomie), nutzt einen Text-Field mit
106
112
  // eigenem Codec.
107
- const col = bigint(snakeName, { mode: "number" });
113
+ const base = bigint(snakeName, { mode: "number" });
114
+ const col = field.default !== undefined ? base.default(field.default) : base;
108
115
  return { [name]: field.required ? col.notNull() : col };
109
116
  }
110
117
  case "reference":