@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.
|
|
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
|
|
package/src/db/table-builder.ts
CHANGED
|
@@ -80,7 +80,12 @@ function fieldToColumns(
|
|
|
80
80
|
: boolean(snakeName),
|
|
81
81
|
};
|
|
82
82
|
case "select": {
|
|
83
|
-
|
|
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
|
|
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
|
|
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":
|