@metaobjectsdev/runtime-ts 0.9.0 → 0.11.0-rc.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/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/dist/type-coercer.js
CHANGED
|
@@ -1,18 +1,100 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
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.
|
|
15
|
+
import { TYPE_FIELD, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_OBJECT, FIELD_ATTR_STORAGE, FIELD_ATTR_DB_COLUMN_TYPE, STORAGE_JSONB, STORAGE_FLATTENED, DB_COLUMN_TYPE_JSONB, } from "@metaobjectsdev/metadata";
|
|
3
16
|
export function coerceRowOnRead(entity, row, dialect) {
|
|
17
|
+
const hydrated = deserializeJsonbObjectFields(entity, row);
|
|
4
18
|
if (dialect !== "sqlite")
|
|
5
|
-
return
|
|
6
|
-
return mapBooleansFromInt(entity,
|
|
19
|
+
return hydrated;
|
|
20
|
+
return mapBooleansFromInt(entity, hydrated);
|
|
7
21
|
}
|
|
8
22
|
export function coerceRowOnWrite(entity, row, dialect) {
|
|
23
|
+
const jsonbColumned = serializeJsonbColumns(entity, row);
|
|
9
24
|
if (dialect !== "sqlite")
|
|
10
|
-
return
|
|
11
|
-
return mapBooleansToInt(entity,
|
|
25
|
+
return jsonbColumned;
|
|
26
|
+
return mapBooleansToInt(entity, jsonbColumned);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A `field.object` lands in a single JSONB column unless its `@storage` is
|
|
30
|
+
* `flattened` (then it expands to prefixed columns and has no column of its own).
|
|
31
|
+
* Default (no `@storage`) and `@storage: jsonb` both store one jsonb column.
|
|
32
|
+
*/
|
|
33
|
+
function isJsonbObjectField(child) {
|
|
34
|
+
if (child.subType !== FIELD_SUBTYPE_OBJECT)
|
|
35
|
+
return false;
|
|
36
|
+
return child.ownAttr(FIELD_ATTR_STORAGE) !== STORAGE_FLATTENED;
|
|
37
|
+
}
|
|
38
|
+
/** A field explicitly pinned to a JSONB physical column via `@dbColumnType`. */
|
|
39
|
+
function isJsonbColumnTypeField(child) {
|
|
40
|
+
return child.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB;
|
|
41
|
+
}
|
|
42
|
+
function serializeJsonbColumns(entity, row) {
|
|
43
|
+
let out = null;
|
|
44
|
+
for (const child of entity.ownChildren()) {
|
|
45
|
+
if (child.type !== TYPE_FIELD)
|
|
46
|
+
continue;
|
|
47
|
+
if (!isJsonbObjectField(child) && !isJsonbColumnTypeField(child))
|
|
48
|
+
continue;
|
|
49
|
+
if (!(child.name in row))
|
|
50
|
+
continue;
|
|
51
|
+
const v = row[child.name];
|
|
52
|
+
// Only objects/arrays need stringifying; an already-serialized string passes
|
|
53
|
+
// through unchanged (the existing Asset.payload contract), as does null.
|
|
54
|
+
if (v === null || v === undefined || typeof v !== "object")
|
|
55
|
+
continue;
|
|
56
|
+
out ??= { ...row };
|
|
57
|
+
out[child.name] = JSON.stringify(v);
|
|
58
|
+
}
|
|
59
|
+
return out ?? row;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read-side complement of `serializeJsonbColumns`: a `field.object` jsonb column
|
|
63
|
+
* is the structured-VO storage and must round-trip as a native object (ADR-0019),
|
|
64
|
+
* not the serialized string. node-pg's jsonb parser already returns a parsed
|
|
65
|
+
* object for the postgres driver, so this only acts on a still-stringified value
|
|
66
|
+
* (the in-memory test driver, or any driver that does not parse jsonb) — an
|
|
67
|
+
* already-parsed object/array passes through untouched. Only `field.object`
|
|
68
|
+
* columns are hydrated; a `field.string @dbColumnType: jsonb` stays the raw
|
|
69
|
+
* string it is declared to be. A non-JSON string is left as-is.
|
|
70
|
+
*/
|
|
71
|
+
function deserializeJsonbObjectFields(entity, row) {
|
|
72
|
+
let out = null;
|
|
73
|
+
for (const child of entity.ownChildren()) {
|
|
74
|
+
if (child.type !== TYPE_FIELD)
|
|
75
|
+
continue;
|
|
76
|
+
if (!isJsonbObjectField(child))
|
|
77
|
+
continue;
|
|
78
|
+
if (!(child.name in row))
|
|
79
|
+
continue;
|
|
80
|
+
const v = row[child.name];
|
|
81
|
+
if (typeof v !== "string")
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(v);
|
|
85
|
+
out ??= { ...row };
|
|
86
|
+
out[child.name] = parsed;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not JSON — leave the raw string in place.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out ?? row;
|
|
12
93
|
}
|
|
13
94
|
function mapBooleansFromInt(entity, row) {
|
|
14
95
|
const out = { ...row };
|
|
15
|
-
|
|
96
|
+
// Effective children so a TPH subtype coerces inherited boolean fields too.
|
|
97
|
+
for (const child of entity.children()) {
|
|
16
98
|
if (child.type !== TYPE_FIELD)
|
|
17
99
|
continue;
|
|
18
100
|
if (child.subType !== FIELD_SUBTYPE_BOOLEAN)
|
|
@@ -27,7 +109,8 @@ function mapBooleansFromInt(entity, row) {
|
|
|
27
109
|
}
|
|
28
110
|
function mapBooleansToInt(entity, row) {
|
|
29
111
|
const out = { ...row };
|
|
30
|
-
|
|
112
|
+
// Effective children so a TPH subtype coerces inherited boolean fields too.
|
|
113
|
+
for (const child of entity.children()) {
|
|
31
114
|
if (child.type !== TYPE_FIELD)
|
|
32
115
|
continue;
|
|
33
116
|
if (child.subType !== FIELD_SUBTYPE_BOOLEAN)
|
package/dist/type-coercer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type-coercer.js","sourceRoot":"","sources":["../src/type-coercer.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"type-coercer.js","sourceRoot":"","sources":["../src/type-coercer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator-runner.d.ts","sourceRoot":"","sources":["../src/validator-runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAUzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"validator-runner.d.ts","sourceRoot":"","sources":["../src/validator-runner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAUzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAAE,CAAC;AAqB/C,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,QAAQ,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,GAAE,iBAAsB,GAC3B,gBAAgB,CA8FlB"}
|
package/dist/validator-runner.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
// Pure function: NEVER throws. ObjectManager wraps a non-ok result in a ValidationError on writes;
|
|
2
2
|
// om.validate() returns the result directly.
|
|
3
|
-
import { TYPE_FIELD, TYPE_VALIDATOR, VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX, FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG,
|
|
3
|
+
import { TYPE_FIELD, TYPE_VALIDATOR, VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX, FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_UUID, FIELD_ATTR_REQUIRED, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_PATTERN, } from "@metaobjectsdev/metadata";
|
|
4
|
+
// JS-safe numeric fields: must arrive as a JS `number` (they fit in 2^53).
|
|
4
5
|
const NUMERIC_FIELD_SUBTYPES = new Set([
|
|
5
|
-
FIELD_SUBTYPE_INT,
|
|
6
|
+
FIELD_SUBTYPE_INT,
|
|
6
7
|
FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
|
|
7
8
|
]);
|
|
9
|
+
// 64-bit integer fields (BIGINT on the wire). A full int64 (> 2^53) cannot
|
|
10
|
+
// survive a JS `number`, so the write contract additionally accepts a numeric
|
|
11
|
+
// `string` or a `bigint` for these — the runtime passes them through unchanged
|
|
12
|
+
// so the BIGINT round-trips exactly (read-back is BIGINT→string per
|
|
13
|
+
// normalization.md / ADR-0019). `field.currency` is integer minor units → BIGINT.
|
|
14
|
+
const INT64_FIELD_SUBTYPES = new Set([
|
|
15
|
+
FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
|
|
16
|
+
]);
|
|
17
|
+
// A base-10 signed integer literal with no fractional/exponent part — the only
|
|
18
|
+
// string shape accepted for an int64 field (a "1.5" or "1e3" is rejected).
|
|
19
|
+
const INT64_STRING_RE = /^-?\d+$/;
|
|
8
20
|
export function runValidators(entity, data, opts = {}) {
|
|
9
21
|
const errors = [];
|
|
10
|
-
|
|
22
|
+
// Effective children so a TPH subtype validates inherited base fields too.
|
|
23
|
+
for (const field of entity.children()) {
|
|
11
24
|
if (field.type !== TYPE_FIELD)
|
|
12
25
|
continue;
|
|
13
26
|
const present = Object.prototype.hasOwnProperty.call(data, field.name);
|
|
@@ -146,6 +159,14 @@ function checkType(subType, value) {
|
|
|
146
159
|
if (typeof value !== "number")
|
|
147
160
|
return `expected number`;
|
|
148
161
|
}
|
|
162
|
+
else if (INT64_FIELD_SUBTYPES.has(subType)) {
|
|
163
|
+
// number (in-band) | bigint | base-10 integer string (full int64 fidelity).
|
|
164
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
165
|
+
return null;
|
|
166
|
+
if (typeof value === "string" && INT64_STRING_RE.test(value))
|
|
167
|
+
return null;
|
|
168
|
+
return `expected a 64-bit integer (number, bigint, or numeric string)`;
|
|
169
|
+
}
|
|
149
170
|
else if (subType === FIELD_SUBTYPE_BOOLEAN) {
|
|
150
171
|
if (typeof value !== "boolean")
|
|
151
172
|
return `expected boolean`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator-runner.js","sourceRoot":"","sources":["../src/validator-runner.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,6CAA6C;AAG7C,OAAO,EACL,UAAU,EAAE,cAAc,EAC1B,0BAA0B,EAAE,wBAAwB,EAAE,uBAAuB,EAC7E,oBAAoB,EAAE,iBAAiB,EAAE,kBAAkB,EAC3D,
|
|
1
|
+
{"version":3,"file":"validator-runner.js","sourceRoot":"","sources":["../src/validator-runner.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/runtime-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-rc.1",
|
|
4
4
|
"description": "Node-side runtime helpers for MetaObjects: Fastify route builders, Drizzle filter/sort integration, Kysely drivers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
"default": "./dist/hono/index.js"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
-
"files": [
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
36
41
|
"scripts": {
|
|
37
42
|
"build": "tsc -p .",
|
|
38
43
|
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
@@ -41,58 +46,64 @@
|
|
|
41
46
|
"author": "Doug Mealing <doug@dougmealing.com>",
|
|
42
47
|
"homepage": "https://metaobjects.dev",
|
|
43
48
|
"bugs": {
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
"repository": {
|
|
47
|
-
"type": "git",
|
|
48
|
-
"url": "https://github.com/metaobjectsdev/metaobjects.git",
|
|
49
|
-
"directory": "server/typescript/packages/runtime-ts"
|
|
50
|
-
},
|
|
51
|
-
"keywords": ["metaobjects", "runtime", "fastify", "drizzle", "kysely"],
|
|
52
|
-
"publishConfig": {
|
|
53
|
-
"access": "public"
|
|
54
|
-
},
|
|
55
|
-
"dependencies": {
|
|
56
|
-
"@metaobjectsdev/metadata": "0.9.0",
|
|
57
|
-
"@metaobjectsdev/render": "0.9.0",
|
|
58
|
-
"qs": "^6.13.0"
|
|
59
|
-
},
|
|
60
|
-
"peerDependencies": {
|
|
61
|
-
"drizzle-orm": ">=0.36.0",
|
|
62
|
-
"fastify": ">=5.0.0",
|
|
63
|
-
"hono": ">=4.0.0",
|
|
64
|
-
"kysely": ">=0.27.0",
|
|
65
|
-
"zod": ">=3.23.0"
|
|
66
|
-
},
|
|
67
|
-
"peerDependenciesMeta": {
|
|
68
|
-
"drizzle-orm": {
|
|
69
|
-
"optional": true
|
|
49
|
+
"url": "https://github.com/metaobjectsdev/metaobjects/issues"
|
|
70
50
|
},
|
|
71
|
-
"
|
|
72
|
-
"
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/metaobjectsdev/metaobjects.git",
|
|
54
|
+
"directory": "server/typescript/packages/runtime-ts"
|
|
73
55
|
},
|
|
74
|
-
"
|
|
75
|
-
"
|
|
56
|
+
"keywords": [
|
|
57
|
+
"metaobjects",
|
|
58
|
+
"runtime",
|
|
59
|
+
"fastify",
|
|
60
|
+
"drizzle",
|
|
61
|
+
"kysely"
|
|
62
|
+
],
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
76
65
|
},
|
|
77
|
-
"
|
|
78
|
-
"
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@metaobjectsdev/metadata": "0.11.0-rc.1",
|
|
68
|
+
"@metaobjectsdev/render": "0.11.0-rc.1",
|
|
69
|
+
"qs": "^6.13.0"
|
|
79
70
|
},
|
|
80
|
-
"
|
|
81
|
-
"
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"drizzle-orm": ">=0.36.0",
|
|
73
|
+
"fastify": ">=5.0.0",
|
|
74
|
+
"hono": ">=4.0.0",
|
|
75
|
+
"kysely": ">=0.27.0",
|
|
76
|
+
"zod": ">=3.23.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"drizzle-orm": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"fastify": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"hono": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"kysely": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"zod": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@libsql/client": "^0.15.0",
|
|
97
|
+
"@libsql/kysely-libsql": "^0.4.1",
|
|
98
|
+
"@types/qs": "^6.9.0",
|
|
99
|
+
"bun-types": "latest",
|
|
100
|
+
"drizzle-orm": "^0.45.1",
|
|
101
|
+
"fastify": "^5.6.2",
|
|
102
|
+
"hono": "^4.6.0",
|
|
103
|
+
"jsdom": "^29.1.1",
|
|
104
|
+
"kysely": "^0.27.0",
|
|
105
|
+
"pg-mem": "^3.0.4",
|
|
106
|
+
"typescript": "^5.6.0",
|
|
107
|
+
"zod": "^4.4.3"
|
|
82
108
|
}
|
|
83
|
-
},
|
|
84
|
-
"devDependencies": {
|
|
85
|
-
"@libsql/client": "^0.15.0",
|
|
86
|
-
"@libsql/kysely-libsql": "^0.4.1",
|
|
87
|
-
"@types/qs": "^6.9.0",
|
|
88
|
-
"bun-types": "latest",
|
|
89
|
-
"drizzle-orm": "^0.45.1",
|
|
90
|
-
"fastify": "^5.6.2",
|
|
91
|
-
"hono": "^4.6.0",
|
|
92
|
-
"jsdom": "^29.1.1",
|
|
93
|
-
"kysely": "^0.27.0",
|
|
94
|
-
"pg-mem": "^3.0.4",
|
|
95
|
-
"typescript": "^5.6.0",
|
|
96
|
-
"zod": "^4.4.3"
|
|
97
|
-
}
|
|
98
109
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
and as drzAnd,
|
|
16
|
+
or as drzOr,
|
|
16
17
|
eq as drzEq,
|
|
17
18
|
ne as drzNe,
|
|
18
19
|
gt as drzGt,
|
|
@@ -410,6 +411,10 @@ function buildExpression(w: WhereClause, cols: Map<string, AnyColumn>): unknown
|
|
|
410
411
|
const parts = w.clauses.map((c) => buildExpression(c, cols)) as SQL[];
|
|
411
412
|
return drzAnd(...parts);
|
|
412
413
|
}
|
|
414
|
+
case "or": {
|
|
415
|
+
const parts = w.clauses.map((c) => buildExpression(c, cols)) as SQL[];
|
|
416
|
+
return drzOr(...parts);
|
|
417
|
+
}
|
|
413
418
|
default: {
|
|
414
419
|
const exhaustive: never = w;
|
|
415
420
|
throw new Error(`Unhandled WhereClause kind: ${JSON.stringify(exhaustive)}`);
|
|
@@ -216,6 +216,8 @@ function matchesWhere(row: Row, where?: WhereClause): boolean {
|
|
|
216
216
|
}
|
|
217
217
|
case "and":
|
|
218
218
|
return where.clauses.every((c) => matchesWhere(row, c));
|
|
219
|
+
case "or":
|
|
220
|
+
return where.clauses.some((c) => matchesWhere(row, c));
|
|
219
221
|
default: {
|
|
220
222
|
const exhaustive: never = where;
|
|
221
223
|
throw new Error(`Unhandled WhereClause kind: ${JSON.stringify(exhaustive)}`);
|
|
@@ -160,6 +160,7 @@ function buildExpression(eb: AnyExprBuilder, w: WhereClause): Expression<SqlBool
|
|
|
160
160
|
case "in": return eb(w.column, "in", w.values);
|
|
161
161
|
case "isNull": return w.not ? eb(w.column, "is not", null) : eb(w.column, "is", null);
|
|
162
162
|
case "and": return eb.and(w.clauses.map((c: WhereClause) => buildExpression(eb, c)));
|
|
163
|
+
case "or": return eb.or(w.clauses.map((c: WhereClause) => buildExpression(eb, c)));
|
|
163
164
|
default: {
|
|
164
165
|
const exhaustive: never = w;
|
|
165
166
|
throw new Error(`Unhandled WhereClause kind: ${JSON.stringify(exhaustive)}`);
|
|
@@ -21,8 +21,8 @@ import qs from "qs";
|
|
|
21
21
|
import type { FilterAllowlist, SortAllowlist } from "./filter-allowlist.js";
|
|
22
22
|
export type { FilterAllowlist, SortAllowlist } from "./filter-allowlist.js";
|
|
23
23
|
import { parseFilterParams, FilterParseError } from "./filter-parser.js";
|
|
24
|
-
import { isTruthyFlag, contractErrorCode } from "./util.js";
|
|
25
|
-
export { isTruthyFlag, contractErrorCode } from "./util.js";
|
|
24
|
+
import { isTruthyFlag, contractErrorCode, parseId } from "./util.js";
|
|
25
|
+
export { isTruthyFlag, contractErrorCode, parseId } from "./util.js";
|
|
26
26
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
// Loose types — we don't bind to a specific Drizzle backend so the helper
|
|
@@ -65,10 +65,28 @@ export interface CrudRoutesOptions {
|
|
|
65
65
|
sortAllowlist?: SortAllowlist;
|
|
66
66
|
/** Dialect — required if filterAllowlist or sortAllowlist is set (for like/ilike dispatch). */
|
|
67
67
|
dialect?: "sqlite" | "postgres";
|
|
68
|
+
/**
|
|
69
|
+
* FR-017 TPH — scope this route set to a single subtype of a single-table-
|
|
70
|
+
* inheritance base. When set:
|
|
71
|
+
* - list/get filter to `eq(table[column], value)`;
|
|
72
|
+
* - a get/update/delete targeting a row of another subtype 404s;
|
|
73
|
+
* - create injects `{ [column]: value }` AFTER body validation (the body
|
|
74
|
+
* omits the discriminator — the URL already names the subtype);
|
|
75
|
+
* - update strips `column` from the patch (a row's subtype is immutable).
|
|
76
|
+
* Absent → ordinary single-table CRUD, behaviour unchanged.
|
|
77
|
+
*/
|
|
78
|
+
discriminator?: { column: string; value: string };
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
const ALL_VERBS: readonly CrudVerb[] = ["list", "get", "create", "update", "delete"];
|
|
71
82
|
|
|
83
|
+
/** The TPH discriminator predicate for this route set, or undefined when the
|
|
84
|
+
* routes are not subtype-scoped. */
|
|
85
|
+
function discriminatorCond(opts: VerbOptions) {
|
|
86
|
+
const d = opts.discriminator;
|
|
87
|
+
return d ? eq(opts.table[d.column], d.value) : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
72
90
|
export function mountCrudRoutes(opts: CrudRoutesOptions): void {
|
|
73
91
|
const verbs = new Set<CrudVerb>(opts.expose ?? ALL_VERBS);
|
|
74
92
|
if (verbs.has("list")) mountListRoute(opts);
|
|
@@ -96,6 +114,10 @@ export function mountListRoute(opts: VerbOptions): void {
|
|
|
96
114
|
const qsParsed = qs.parse(rawSearch) as Record<string, unknown>;
|
|
97
115
|
const withCount = isTruthyFlag(qsParsed.withCount);
|
|
98
116
|
|
|
117
|
+
// FR-017 TPH: when subtype-scoped, AND the discriminator predicate into
|
|
118
|
+
// every list query (combined with any allowlist filters).
|
|
119
|
+
const discCond = discriminatorCond(opts);
|
|
120
|
+
|
|
99
121
|
let where: ReturnType<typeof parseFilterParams>["where"];
|
|
100
122
|
if (opts.filterAllowlist && opts.sortAllowlist) {
|
|
101
123
|
const parsed = parseFilterParams({
|
|
@@ -105,9 +127,12 @@ export function mountListRoute(opts: VerbOptions): void {
|
|
|
105
127
|
sortAllowlist: opts.sortAllowlist,
|
|
106
128
|
dialect: opts.dialect ?? "sqlite",
|
|
107
129
|
});
|
|
108
|
-
const
|
|
130
|
+
const filterWhere = parsed.where && parsed.searchWhere
|
|
109
131
|
? and(parsed.where, parsed.searchWhere)
|
|
110
132
|
: (parsed.where ?? parsed.searchWhere);
|
|
133
|
+
const combinedWhere = filterWhere && discCond
|
|
134
|
+
? and(filterWhere, discCond)
|
|
135
|
+
: (filterWhere ?? discCond);
|
|
111
136
|
if (combinedWhere) { q = q.where(combinedWhere); where = combinedWhere; }
|
|
112
137
|
// Default to stable id-ascending order when the caller specifies no
|
|
113
138
|
// sort — the cross-port contract asserts deterministic ordering for
|
|
@@ -118,8 +143,10 @@ export function mountListRoute(opts: VerbOptions): void {
|
|
|
118
143
|
if (parsed.limit !== undefined) q = q.limit(parsed.limit);
|
|
119
144
|
if (parsed.offset !== undefined) q = q.offset(parsed.offset);
|
|
120
145
|
} else {
|
|
121
|
-
// Legacy path — no allowlists configured. Only limit/offset
|
|
146
|
+
// Legacy path — no allowlists configured. Only limit/offset (+ the TPH
|
|
147
|
+
// discriminator predicate when subtype-scoped).
|
|
122
148
|
const { limit, offset } = req.query as { limit?: string; offset?: string };
|
|
149
|
+
if (discCond) { q = q.where(discCond); where = discCond; }
|
|
123
150
|
if (opts.table.id !== undefined) q = q.orderBy(asc(opts.table.id));
|
|
124
151
|
if (limit !== undefined) q = q.limit(Number(limit));
|
|
125
152
|
if (offset !== undefined) q = q.offset(Number(offset));
|
|
@@ -151,13 +178,15 @@ export function mountListRoute(opts: VerbOptions): void {
|
|
|
151
178
|
export function mountGetRoute(opts: VerbOptions): void {
|
|
152
179
|
opts.fastify.get(`${opts.path}/:id`, routeOpts(opts), async (req, reply) => {
|
|
153
180
|
const { id } = req.params as { id: string };
|
|
181
|
+
const discCond = discriminatorCond(opts);
|
|
182
|
+
const idCond = eq(opts.table.id, parseId(id));
|
|
154
183
|
// Await + take the first row rather than `.get()` — `.get()` is a
|
|
155
184
|
// libsql/better-sqlite3-only method; the node-postgres builder is thenable
|
|
156
185
|
// but has no `.get()`. Awaiting works on both dialects.
|
|
157
186
|
const rows = await opts.db
|
|
158
187
|
.select()
|
|
159
188
|
.from(opts.table)
|
|
160
|
-
.where(
|
|
189
|
+
.where(discCond ? and(idCond, discCond) : idCond)
|
|
161
190
|
.limit(1);
|
|
162
191
|
const row = (rows as unknown[])[0];
|
|
163
192
|
return row ?? reply.code(404).send({ error: "not_found" });
|
|
@@ -170,7 +199,12 @@ export function mountCreateRoute(opts: VerbOptions): void {
|
|
|
170
199
|
if (!parsed.success) {
|
|
171
200
|
return reply.code(400).send({ error: "validation", issues: parsed.error.issues });
|
|
172
201
|
}
|
|
173
|
-
|
|
202
|
+
// FR-017 TPH: the body omits the discriminator (the URL names the subtype);
|
|
203
|
+
// inject it server-side so the row lands tagged with the right subtype.
|
|
204
|
+
const values = opts.discriminator
|
|
205
|
+
? { ...(parsed.data as Record<string, unknown>), [opts.discriminator.column]: opts.discriminator.value }
|
|
206
|
+
: parsed.data;
|
|
207
|
+
const result = await opts.db.insert(opts.table).values(values).returning();
|
|
174
208
|
const row = (result as unknown[])[0];
|
|
175
209
|
return reply.code(201).send(row);
|
|
176
210
|
});
|
|
@@ -186,10 +220,19 @@ export function mountUpdateRoute(opts: VerbOptions): void {
|
|
|
186
220
|
if (!parsed.success) {
|
|
187
221
|
return reply.code(400).send({ error: "validation", issues: parsed.error.issues });
|
|
188
222
|
}
|
|
223
|
+
const discCond = discriminatorCond(opts);
|
|
224
|
+
// FR-017 TPH: a row's subtype is immutable — strip the discriminator from
|
|
225
|
+
// the patch so a client can't flip a Bridge into a Copay.
|
|
226
|
+
let data = parsed.data as Record<string, unknown>;
|
|
227
|
+
if (opts.discriminator) {
|
|
228
|
+
const { [opts.discriminator.column]: _omit, ...rest } = data;
|
|
229
|
+
data = rest;
|
|
230
|
+
}
|
|
231
|
+
const idCond = eq(opts.table.id, parseId(id));
|
|
189
232
|
const result = await opts.db
|
|
190
233
|
.update(opts.table)
|
|
191
|
-
.set(
|
|
192
|
-
.where(
|
|
234
|
+
.set(data)
|
|
235
|
+
.where(discCond ? and(idCond, discCond) : idCond)
|
|
193
236
|
.returning();
|
|
194
237
|
const row = (result as unknown[])[0];
|
|
195
238
|
return row ?? reply.code(404).send({ error: "not_found" });
|
|
@@ -216,9 +259,11 @@ export function mountUpdateRoute(opts: VerbOptions): void {
|
|
|
216
259
|
export function mountDeleteRoute(opts: VerbOptions): void {
|
|
217
260
|
opts.fastify.delete(`${opts.path}/:id`, routeOpts(opts), async (req, reply) => {
|
|
218
261
|
const { id } = req.params as { id: string };
|
|
262
|
+
const discCond = discriminatorCond(opts);
|
|
263
|
+
const idCond = eq(opts.table.id, parseId(id));
|
|
219
264
|
const result = await opts.db
|
|
220
265
|
.delete(opts.table)
|
|
221
|
-
.where(
|
|
266
|
+
.where(discCond ? and(idCond, discCond) : idCond);
|
|
222
267
|
// Both libsql and pg drivers expose a rows-affected counter, in different
|
|
223
268
|
// shapes. Treat anything > 0 as "found and deleted."
|
|
224
269
|
const affected = extractRowCount(result);
|
|
@@ -240,9 +285,5 @@ function extractRowCount(result: unknown): number {
|
|
|
240
285
|
return 0;
|
|
241
286
|
}
|
|
242
287
|
|
|
243
|
-
export function parseId(raw: string): number | string {
|
|
244
|
-
const n = Number(raw);
|
|
245
|
-
return Number.isFinite(n) && raw.trim() !== "" ? n : raw;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
288
|
export { mountReadOnlyCrudRoutes, type MountReadOnlyOptions } from "./mount-read-only.js";
|
|
289
|
+
export { mountM2mRoute, type M2mRouteOptions } from "./mount-m2m.js";
|