@metaobjectsdev/runtime-ts 0.9.0-rc.1 → 0.10.0
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/dist/drivers/drizzle-driver.d.ts.map +1 -1
- package/dist/drivers/drizzle-driver.js +5 -1
- package/dist/drivers/drizzle-driver.js.map +1 -1
- package/dist/drivers/in-memory-driver.js +2 -0
- package/dist/drivers/in-memory-driver.js.map +1 -1
- package/dist/drivers/kysely-driver.js +1 -0
- package/dist/drivers/kysely-driver.js.map +1 -1
- package/dist/drizzle-fastify/index.d.ts +16 -2
- package/dist/drizzle-fastify/index.d.ts.map +1 -1
- package/dist/drizzle-fastify/index.js +45 -13
- package/dist/drizzle-fastify/index.js.map +1 -1
- package/dist/drizzle-fastify/mount-m2m.d.ts +29 -0
- package/dist/drizzle-fastify/mount-m2m.d.ts.map +1 -0
- package/dist/drizzle-fastify/mount-m2m.js +94 -0
- package/dist/drizzle-fastify/mount-m2m.js.map +1 -0
- package/dist/drizzle-fastify/util.d.ts +2 -0
- package/dist/drizzle-fastify/util.d.ts.map +1 -1
- package/dist/drizzle-fastify/util.js +5 -0
- package/dist/drizzle-fastify/util.js.map +1 -1
- package/dist/extract-object.d.ts.map +1 -1
- package/dist/extract-object.js +14 -5
- package/dist/extract-object.js.map +1 -1
- package/dist/identity-strategy.d.ts.map +1 -1
- package/dist/identity-strategy.js +2 -1
- package/dist/identity-strategy.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-recorder.d.ts +72 -0
- package/dist/llm-recorder.d.ts.map +1 -0
- package/dist/llm-recorder.js +82 -0
- package/dist/llm-recorder.js.map +1 -0
- package/dist/n2m-resolver.d.ts +7 -8
- package/dist/n2m-resolver.d.ts.map +1 -1
- package/dist/n2m-resolver.js +115 -38
- package/dist/n2m-resolver.js.map +1 -1
- package/dist/object-manager.d.ts +4 -0
- package/dist/object-manager.d.ts.map +1 -1
- package/dist/object-manager.js +71 -18
- package/dist/object-manager.js.map +1 -1
- package/dist/persistence-driver.d.ts +3 -0
- package/dist/persistence-driver.d.ts.map +1 -1
- package/dist/query-builder.d.ts +13 -3
- package/dist/query-builder.d.ts.map +1 -1
- package/dist/query-builder.js +19 -10
- package/dist/query-builder.js.map +1 -1
- package/dist/tph.d.ts +14 -0
- package/dist/tph.d.ts.map +1 -0
- package/dist/tph.js +37 -0
- package/dist/tph.js.map +1 -0
- package/dist/type-coercer.d.ts.map +1 -1
- package/dist/type-coercer.js +91 -8
- package/dist/type-coercer.js.map +1 -1
- package/dist/validator-runner.d.ts.map +1 -1
- package/dist/validator-runner.js +24 -3
- package/dist/validator-runner.js.map +1 -1
- package/package.json +62 -51
- package/src/drivers/drizzle-driver.ts +5 -0
- package/src/drivers/in-memory-driver.ts +2 -0
- package/src/drivers/kysely-driver.ts +1 -0
- package/src/drizzle-fastify/index.ts +55 -14
- package/src/drizzle-fastify/mount-m2m.ts +126 -0
- package/src/drizzle-fastify/util.ts +6 -0
- package/src/extract-object.ts +16 -6
- package/src/identity-strategy.ts +2 -1
- package/src/index.ts +7 -0
- package/src/llm-recorder.ts +166 -0
- package/src/n2m-resolver.ts +143 -57
- package/src/object-manager.ts +67 -18
- package/src/persistence-driver.ts +2 -1
- package/src/query-builder.ts +33 -8
- package/src/tph.ts +46 -0
- package/src/type-coercer.ts +94 -8
- package/src/validator-runner.ts +23 -3
package/src/type-coercer.ts
CHANGED
|
@@ -1,22 +1,107 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Read/write value coercion at the persistence boundary.
|
|
2
|
+
//
|
|
3
|
+
// - SQLite has no native boolean: booleans are stored as 0/1 ints, so we map
|
|
4
|
+
// boolean↔int on the way in/out for that dialect only.
|
|
5
|
+
// - JSONB-backed object/map fields: the driver (node-postgres) does NOT
|
|
6
|
+
// serialize a plain JS object to a jsonb column — it must arrive as a JSON
|
|
7
|
+
// string, or the write fails / writes "[object Object]". A `field.object`
|
|
8
|
+
// (default + `@storage: jsonb`) and any field carrying `@dbColumnType: jsonb`
|
|
9
|
+
// whose value is an object are JSON.stringify'd on write. (Read-back parsing
|
|
10
|
+
// is handled by node-pg's jsonb parser, which returns a parsed object.)
|
|
11
|
+
//
|
|
12
|
+
// Date/timestamp coercion (ISO string ↔ Date) is deferred: pg accepts ISO
|
|
13
|
+
// strings for temporal columns directly, and the temporal read parsers pin the
|
|
14
|
+
// wire form.
|
|
2
15
|
|
|
3
16
|
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
4
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
TYPE_FIELD,
|
|
19
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
20
|
+
FIELD_SUBTYPE_OBJECT,
|
|
21
|
+
FIELD_ATTR_STORAGE,
|
|
22
|
+
FIELD_ATTR_DB_COLUMN_TYPE,
|
|
23
|
+
STORAGE_JSONB,
|
|
24
|
+
STORAGE_FLATTENED,
|
|
25
|
+
DB_COLUMN_TYPE_JSONB,
|
|
26
|
+
} from "@metaobjectsdev/metadata";
|
|
5
27
|
import type { Dialect, Row } from "./persistence-driver.js";
|
|
6
28
|
|
|
7
29
|
export function coerceRowOnRead(entity: MetaData, row: Row, dialect: Dialect): Row {
|
|
8
|
-
|
|
9
|
-
|
|
30
|
+
const hydrated = deserializeJsonbObjectFields(entity, row);
|
|
31
|
+
if (dialect !== "sqlite") return hydrated;
|
|
32
|
+
return mapBooleansFromInt(entity, hydrated);
|
|
10
33
|
}
|
|
11
34
|
|
|
12
35
|
export function coerceRowOnWrite(entity: MetaData, row: Row, dialect: Dialect): Row {
|
|
13
|
-
|
|
14
|
-
|
|
36
|
+
const jsonbColumned = serializeJsonbColumns(entity, row);
|
|
37
|
+
if (dialect !== "sqlite") return jsonbColumned;
|
|
38
|
+
return mapBooleansToInt(entity, jsonbColumned);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A `field.object` lands in a single JSONB column unless its `@storage` is
|
|
43
|
+
* `flattened` (then it expands to prefixed columns and has no column of its own).
|
|
44
|
+
* Default (no `@storage`) and `@storage: jsonb` both store one jsonb column.
|
|
45
|
+
*/
|
|
46
|
+
function isJsonbObjectField(child: MetaData): boolean {
|
|
47
|
+
if (child.subType !== FIELD_SUBTYPE_OBJECT) return false;
|
|
48
|
+
return child.ownAttr(FIELD_ATTR_STORAGE) !== STORAGE_FLATTENED;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A field explicitly pinned to a JSONB physical column via `@dbColumnType`. */
|
|
52
|
+
function isJsonbColumnTypeField(child: MetaData): boolean {
|
|
53
|
+
return child.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function serializeJsonbColumns(entity: MetaData, row: Row): Row {
|
|
57
|
+
let out: Row | null = null;
|
|
58
|
+
for (const child of entity.ownChildren()) {
|
|
59
|
+
if (child.type !== TYPE_FIELD) continue;
|
|
60
|
+
if (!isJsonbObjectField(child) && !isJsonbColumnTypeField(child)) continue;
|
|
61
|
+
if (!(child.name in row)) continue;
|
|
62
|
+
const v = row[child.name];
|
|
63
|
+
// Only objects/arrays need stringifying; an already-serialized string passes
|
|
64
|
+
// through unchanged (the existing Asset.payload contract), as does null.
|
|
65
|
+
if (v === null || v === undefined || typeof v !== "object") continue;
|
|
66
|
+
out ??= { ...row };
|
|
67
|
+
out[child.name] = JSON.stringify(v);
|
|
68
|
+
}
|
|
69
|
+
return out ?? row;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read-side complement of `serializeJsonbColumns`: a `field.object` jsonb column
|
|
74
|
+
* is the structured-VO storage and must round-trip as a native object (ADR-0019),
|
|
75
|
+
* not the serialized string. node-pg's jsonb parser already returns a parsed
|
|
76
|
+
* object for the postgres driver, so this only acts on a still-stringified value
|
|
77
|
+
* (the in-memory test driver, or any driver that does not parse jsonb) — an
|
|
78
|
+
* already-parsed object/array passes through untouched. Only `field.object`
|
|
79
|
+
* columns are hydrated; a `field.string @dbColumnType: jsonb` stays the raw
|
|
80
|
+
* string it is declared to be. A non-JSON string is left as-is.
|
|
81
|
+
*/
|
|
82
|
+
function deserializeJsonbObjectFields(entity: MetaData, row: Row): Row {
|
|
83
|
+
let out: Row | null = null;
|
|
84
|
+
for (const child of entity.ownChildren()) {
|
|
85
|
+
if (child.type !== TYPE_FIELD) continue;
|
|
86
|
+
if (!isJsonbObjectField(child)) continue;
|
|
87
|
+
if (!(child.name in row)) continue;
|
|
88
|
+
const v = row[child.name];
|
|
89
|
+
if (typeof v !== "string") continue;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(v);
|
|
92
|
+
out ??= { ...row };
|
|
93
|
+
out[child.name] = parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
// Not JSON — leave the raw string in place.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out ?? row;
|
|
15
99
|
}
|
|
16
100
|
|
|
17
101
|
function mapBooleansFromInt(entity: MetaData, row: Row): Row {
|
|
18
102
|
const out: Row = { ...row };
|
|
19
|
-
|
|
103
|
+
// Effective children so a TPH subtype coerces inherited boolean fields too.
|
|
104
|
+
for (const child of entity.children()) {
|
|
20
105
|
if (child.type !== TYPE_FIELD) continue;
|
|
21
106
|
if (child.subType !== FIELD_SUBTYPE_BOOLEAN) continue;
|
|
22
107
|
const v = out[child.name];
|
|
@@ -28,7 +113,8 @@ function mapBooleansFromInt(entity: MetaData, row: Row): Row {
|
|
|
28
113
|
|
|
29
114
|
function mapBooleansToInt(entity: MetaData, row: Row): Row {
|
|
30
115
|
const out: Row = { ...row };
|
|
31
|
-
|
|
116
|
+
// Effective children so a TPH subtype coerces inherited boolean fields too.
|
|
117
|
+
for (const child of entity.children()) {
|
|
32
118
|
if (child.type !== TYPE_FIELD) continue;
|
|
33
119
|
if (child.subType !== FIELD_SUBTYPE_BOOLEAN) continue;
|
|
34
120
|
const v = out[child.name];
|
package/src/validator-runner.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TYPE_FIELD, TYPE_VALIDATOR,
|
|
7
7
|
VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX,
|
|
8
8
|
FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG,
|
|
9
|
-
|
|
9
|
+
FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_CURRENCY,
|
|
10
10
|
FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_UUID,
|
|
11
11
|
FIELD_ATTR_REQUIRED, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT,
|
|
12
12
|
VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_PATTERN,
|
|
@@ -17,11 +17,25 @@ export type ValidationResult =
|
|
|
17
17
|
| { ok: true }
|
|
18
18
|
| { ok: false; errors: ValidationFailure[] };
|
|
19
19
|
|
|
20
|
+
// JS-safe numeric fields: must arrive as a JS `number` (they fit in 2^53).
|
|
20
21
|
const NUMERIC_FIELD_SUBTYPES = new Set<string>([
|
|
21
|
-
FIELD_SUBTYPE_INT,
|
|
22
|
+
FIELD_SUBTYPE_INT,
|
|
22
23
|
FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
|
|
23
24
|
]);
|
|
24
25
|
|
|
26
|
+
// 64-bit integer fields (BIGINT on the wire). A full int64 (> 2^53) cannot
|
|
27
|
+
// survive a JS `number`, so the write contract additionally accepts a numeric
|
|
28
|
+
// `string` or a `bigint` for these — the runtime passes them through unchanged
|
|
29
|
+
// so the BIGINT round-trips exactly (read-back is BIGINT→string per
|
|
30
|
+
// normalization.md / ADR-0019). `field.currency` is integer minor units → BIGINT.
|
|
31
|
+
const INT64_FIELD_SUBTYPES = new Set<string>([
|
|
32
|
+
FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// A base-10 signed integer literal with no fractional/exponent part — the only
|
|
36
|
+
// string shape accepted for an int64 field (a "1.5" or "1e3" is rejected).
|
|
37
|
+
const INT64_STRING_RE = /^-?\d+$/;
|
|
38
|
+
|
|
25
39
|
export interface RunValidatorsOpts {
|
|
26
40
|
/** Partial-update mode: required-checks only fire for fields whose key is present in `data`. */
|
|
27
41
|
partial?: boolean;
|
|
@@ -34,7 +48,8 @@ export function runValidators(
|
|
|
34
48
|
): ValidationResult {
|
|
35
49
|
const errors: ValidationFailure[] = [];
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
// Effective children so a TPH subtype validates inherited base fields too.
|
|
52
|
+
for (const field of entity.children()) {
|
|
38
53
|
if (field.type !== TYPE_FIELD) continue;
|
|
39
54
|
const present = Object.prototype.hasOwnProperty.call(data, field.name);
|
|
40
55
|
const value = data[field.name];
|
|
@@ -161,6 +176,11 @@ function checkType(subType: string, value: unknown): string | null {
|
|
|
161
176
|
if (typeof value !== "string") return `expected string`;
|
|
162
177
|
} else if (NUMERIC_FIELD_SUBTYPES.has(subType)) {
|
|
163
178
|
if (typeof value !== "number") return `expected number`;
|
|
179
|
+
} else if (INT64_FIELD_SUBTYPES.has(subType)) {
|
|
180
|
+
// number (in-band) | bigint | base-10 integer string (full int64 fidelity).
|
|
181
|
+
if (typeof value === "number" || typeof value === "bigint") return null;
|
|
182
|
+
if (typeof value === "string" && INT64_STRING_RE.test(value)) return null;
|
|
183
|
+
return `expected a 64-bit integer (number, bigint, or numeric string)`;
|
|
164
184
|
} else if (subType === FIELD_SUBTYPE_BOOLEAN) {
|
|
165
185
|
if (typeof value !== "boolean") return `expected boolean`;
|
|
166
186
|
}
|