@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.
Files changed (75) hide show
  1. package/dist/drivers/drizzle-driver.d.ts.map +1 -1
  2. package/dist/drivers/drizzle-driver.js +5 -1
  3. package/dist/drivers/drizzle-driver.js.map +1 -1
  4. package/dist/drivers/in-memory-driver.js +2 -0
  5. package/dist/drivers/in-memory-driver.js.map +1 -1
  6. package/dist/drivers/kysely-driver.js +1 -0
  7. package/dist/drivers/kysely-driver.js.map +1 -1
  8. package/dist/drizzle-fastify/index.d.ts +16 -2
  9. package/dist/drizzle-fastify/index.d.ts.map +1 -1
  10. package/dist/drizzle-fastify/index.js +45 -13
  11. package/dist/drizzle-fastify/index.js.map +1 -1
  12. package/dist/drizzle-fastify/mount-m2m.d.ts +29 -0
  13. package/dist/drizzle-fastify/mount-m2m.d.ts.map +1 -0
  14. package/dist/drizzle-fastify/mount-m2m.js +94 -0
  15. package/dist/drizzle-fastify/mount-m2m.js.map +1 -0
  16. package/dist/drizzle-fastify/util.d.ts +2 -0
  17. package/dist/drizzle-fastify/util.d.ts.map +1 -1
  18. package/dist/drizzle-fastify/util.js +5 -0
  19. package/dist/drizzle-fastify/util.js.map +1 -1
  20. package/dist/extract-object.d.ts.map +1 -1
  21. package/dist/extract-object.js +14 -5
  22. package/dist/extract-object.js.map +1 -1
  23. package/dist/identity-strategy.d.ts.map +1 -1
  24. package/dist/identity-strategy.js +2 -1
  25. package/dist/identity-strategy.js.map +1 -1
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/llm-recorder.d.ts +72 -0
  31. package/dist/llm-recorder.d.ts.map +1 -0
  32. package/dist/llm-recorder.js +82 -0
  33. package/dist/llm-recorder.js.map +1 -0
  34. package/dist/n2m-resolver.d.ts +7 -8
  35. package/dist/n2m-resolver.d.ts.map +1 -1
  36. package/dist/n2m-resolver.js +115 -38
  37. package/dist/n2m-resolver.js.map +1 -1
  38. package/dist/object-manager.d.ts +4 -0
  39. package/dist/object-manager.d.ts.map +1 -1
  40. package/dist/object-manager.js +71 -18
  41. package/dist/object-manager.js.map +1 -1
  42. package/dist/persistence-driver.d.ts +3 -0
  43. package/dist/persistence-driver.d.ts.map +1 -1
  44. package/dist/query-builder.d.ts +13 -3
  45. package/dist/query-builder.d.ts.map +1 -1
  46. package/dist/query-builder.js +19 -10
  47. package/dist/query-builder.js.map +1 -1
  48. package/dist/tph.d.ts +14 -0
  49. package/dist/tph.d.ts.map +1 -0
  50. package/dist/tph.js +37 -0
  51. package/dist/tph.js.map +1 -0
  52. package/dist/type-coercer.d.ts.map +1 -1
  53. package/dist/type-coercer.js +91 -8
  54. package/dist/type-coercer.js.map +1 -1
  55. package/dist/validator-runner.d.ts.map +1 -1
  56. package/dist/validator-runner.js +24 -3
  57. package/dist/validator-runner.js.map +1 -1
  58. package/package.json +62 -51
  59. package/src/drivers/drizzle-driver.ts +5 -0
  60. package/src/drivers/in-memory-driver.ts +2 -0
  61. package/src/drivers/kysely-driver.ts +1 -0
  62. package/src/drizzle-fastify/index.ts +55 -14
  63. package/src/drizzle-fastify/mount-m2m.ts +126 -0
  64. package/src/drizzle-fastify/util.ts +6 -0
  65. package/src/extract-object.ts +16 -6
  66. package/src/identity-strategy.ts +2 -1
  67. package/src/index.ts +7 -0
  68. package/src/llm-recorder.ts +166 -0
  69. package/src/n2m-resolver.ts +143 -57
  70. package/src/object-manager.ts +67 -18
  71. package/src/persistence-driver.ts +2 -1
  72. package/src/query-builder.ts +33 -8
  73. package/src/tph.ts +46 -0
  74. package/src/type-coercer.ts +94 -8
  75. package/src/validator-runner.ts +23 -3
@@ -1,22 +1,107 @@
1
- // v0.1 only handles SQLite's int↔boolean. Date/timestamp coercion (ISO string Date) is deferred.
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 { TYPE_FIELD, FIELD_SUBTYPE_BOOLEAN } from "@metaobjectsdev/metadata";
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
- if (dialect !== "sqlite") return row;
9
- return mapBooleansFromInt(entity, row);
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
- if (dialect !== "sqlite") return row;
14
- return mapBooleansToInt(entity, row);
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
- for (const child of entity.ownChildren()) {
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
- for (const child of entity.ownChildren()) {
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];
@@ -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
- FIELD_SUBTYPE_SHORT, FIELD_SUBTYPE_BYTE, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
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, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_SHORT, FIELD_SUBTYPE_BYTE,
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
- for (const field of entity.ownChildren()) {
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
  }