@metaobjectsdev/runtime-ts 0.5.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/LICENSE +189 -0
- package/README.md +102 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/drivers/drizzle-driver.d.ts +25 -0
- package/dist/drivers/drizzle-driver.d.ts.map +1 -0
- package/dist/drivers/drizzle-driver.js +405 -0
- package/dist/drivers/drizzle-driver.js.map +1 -0
- package/dist/drivers/in-memory-driver.d.ts +11 -0
- package/dist/drivers/in-memory-driver.d.ts.map +1 -0
- package/dist/drivers/in-memory-driver.js +232 -0
- package/dist/drivers/in-memory-driver.js.map +1 -0
- package/dist/drivers/index.d.ts +4 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.js +4 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/kysely-driver.d.ts +12 -0
- package/dist/drivers/kysely-driver.d.ts.map +1 -0
- package/dist/drivers/kysely-driver.js +203 -0
- package/dist/drivers/kysely-driver.js.map +1 -0
- package/dist/drizzle-fastify/filter-allowlist.d.ts +19 -0
- package/dist/drizzle-fastify/filter-allowlist.d.ts.map +1 -0
- package/dist/drizzle-fastify/filter-allowlist.js +10 -0
- package/dist/drizzle-fastify/filter-allowlist.js.map +1 -0
- package/dist/drizzle-fastify/filter-parser.d.ts +28 -0
- package/dist/drizzle-fastify/filter-parser.d.ts.map +1 -0
- package/dist/drizzle-fastify/filter-parser.js +185 -0
- package/dist/drizzle-fastify/filter-parser.js.map +1 -0
- package/dist/drizzle-fastify/index.d.ts +48 -0
- package/dist/drizzle-fastify/index.d.ts.map +1 -0
- package/dist/drizzle-fastify/index.js +181 -0
- package/dist/drizzle-fastify/index.js.map +1 -0
- package/dist/drizzle-fastify/mount-read-only.d.ts +17 -0
- package/dist/drizzle-fastify/mount-read-only.d.ts.map +1 -0
- package/dist/drizzle-fastify/mount-read-only.js +159 -0
- package/dist/drizzle-fastify/mount-read-only.js.map +1 -0
- package/dist/drizzle-fastify/util.d.ts +5 -0
- package/dist/drizzle-fastify/util.d.ts.map +1 -0
- package/dist/drizzle-fastify/util.js +12 -0
- package/dist/drizzle-fastify/util.js.map +1 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +86 -0
- package/dist/errors.js.map +1 -0
- package/dist/fastify/index.d.ts +65 -0
- package/dist/fastify/index.d.ts.map +1 -0
- package/dist/fastify/index.js +118 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/identity-strategy.d.ts +10 -0
- package/dist/identity-strategy.d.ts.map +1 -0
- package/dist/identity-strategy.js +67 -0
- package/dist/identity-strategy.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/n2m-resolver.d.ts +27 -0
- package/dist/n2m-resolver.d.ts.map +1 -0
- package/dist/n2m-resolver.js +103 -0
- package/dist/n2m-resolver.js.map +1 -0
- package/dist/object-manager.d.ts +51 -0
- package/dist/object-manager.d.ts.map +1 -0
- package/dist/object-manager.js +355 -0
- package/dist/object-manager.js.map +1 -0
- package/dist/persistence-driver.d.ts +88 -0
- package/dist/persistence-driver.d.ts.map +1 -0
- package/dist/persistence-driver.js +5 -0
- package/dist/persistence-driver.js.map +1 -0
- package/dist/query-builder.d.ts +35 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +178 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/ref-codec.d.ts +8 -0
- package/dist/ref-codec.d.ts.map +1 -0
- package/dist/ref-codec.js +42 -0
- package/dist/ref-codec.js.map +1 -0
- package/dist/relation-resolver.d.ts +27 -0
- package/dist/relation-resolver.d.ts.map +1 -0
- package/dist/relation-resolver.js +136 -0
- package/dist/relation-resolver.js.map +1 -0
- package/dist/type-coercer.d.ts +5 -0
- package/dist/type-coercer.d.ts.map +1 -0
- package/dist/type-coercer.js +43 -0
- package/dist/type-coercer.js.map +1 -0
- package/dist/validator-runner.d.ts +14 -0
- package/dist/validator-runner.d.ts.map +1 -0
- package/dist/validator-runner.js +155 -0
- package/dist/validator-runner.js.map +1 -0
- package/dist/view.d.ts +19 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +60 -0
- package/dist/view.js.map +1 -0
- package/package.json +87 -0
- package/src/constants.ts +7 -0
- package/src/drivers/drizzle-driver.ts +474 -0
- package/src/drivers/in-memory-driver.ts +256 -0
- package/src/drivers/index.ts +3 -0
- package/src/drivers/kysely-driver.ts +240 -0
- package/src/drizzle-fastify/filter-allowlist.ts +31 -0
- package/src/drizzle-fastify/filter-parser.ts +229 -0
- package/src/drizzle-fastify/index.ts +225 -0
- package/src/drizzle-fastify/mount-read-only.ts +187 -0
- package/src/drizzle-fastify/util.ts +10 -0
- package/src/errors.ts +114 -0
- package/src/fastify/index.ts +181 -0
- package/src/identity-strategy.ts +92 -0
- package/src/index.ts +24 -0
- package/src/n2m-resolver.ts +152 -0
- package/src/object-manager.ts +444 -0
- package/src/persistence-driver.ts +92 -0
- package/src/query-builder.ts +224 -0
- package/src/ref-codec.ts +64 -0
- package/src/relation-resolver.ts +171 -0
- package/src/type-coercer.ts +39 -0
- package/src/validator-runner.ts +168 -0
- package/src/view.ts +82 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PersistenceDriver, SelectSpec, CountSpec, InsertSpec, InsertManySpec,
|
|
3
|
+
UpdateSpec, UpdateManySpec, DeleteSpec, DeleteManySpec, WhereClause, Row,
|
|
4
|
+
} from "../persistence-driver.js";
|
|
5
|
+
import { ConstraintViolationError } from "../errors.js";
|
|
6
|
+
|
|
7
|
+
export interface InMemoryDriverOptions {
|
|
8
|
+
/** Initial table data: { tableName: Row[] }. */
|
|
9
|
+
seed?: Record<string, Row[]>;
|
|
10
|
+
/** Per-table PK field names (used to detect collisions + auto-increment). Default: ["id"]. */
|
|
11
|
+
pkFields?: Record<string, string[]>;
|
|
12
|
+
/** Auto-increment counter starting value per table. Default: 1 + max existing PK. */
|
|
13
|
+
startCounters?: Record<string, number>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface State {
|
|
17
|
+
// Outer key: table name. Inner key: pk values joined by REF_PK_SEPARATOR (",").
|
|
18
|
+
tables: Map<string, Map<string, Row>>;
|
|
19
|
+
pkFields: Map<string, string[]>;
|
|
20
|
+
counters: Map<string, number>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function inMemoryDriver(opts: InMemoryDriverOptions = {}): PersistenceDriver {
|
|
24
|
+
const state = createState(opts);
|
|
25
|
+
return makeDriver(state);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createState(opts: InMemoryDriverOptions): State {
|
|
29
|
+
const tables = new Map<string, Map<string, Row>>();
|
|
30
|
+
const pkFields = new Map<string, string[]>();
|
|
31
|
+
const counters = new Map<string, number>();
|
|
32
|
+
|
|
33
|
+
for (const [tableName, rows] of Object.entries(opts.seed ?? {})) {
|
|
34
|
+
const pk = opts.pkFields?.[tableName] ?? ["id"];
|
|
35
|
+
pkFields.set(tableName, pk);
|
|
36
|
+
const map = new Map<string, Row>();
|
|
37
|
+
let maxNumericPk = 0;
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const key = pkKey(row, pk);
|
|
40
|
+
map.set(key, structuredClone(row));
|
|
41
|
+
const v = row[pk[0]!];
|
|
42
|
+
if (typeof v === "number" && v > maxNumericPk) maxNumericPk = v;
|
|
43
|
+
}
|
|
44
|
+
tables.set(tableName, map);
|
|
45
|
+
counters.set(tableName, opts.startCounters?.[tableName] ?? maxNumericPk + 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { tables, pkFields, counters };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeDriver(state: State): PersistenceDriver {
|
|
52
|
+
const ensureTable = (table: string): Map<string, Row> => {
|
|
53
|
+
let t = state.tables.get(table);
|
|
54
|
+
if (!t) {
|
|
55
|
+
t = new Map();
|
|
56
|
+
state.tables.set(table, t);
|
|
57
|
+
state.pkFields.set(table, ["id"]);
|
|
58
|
+
state.counters.set(table, 1);
|
|
59
|
+
}
|
|
60
|
+
return t;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const ensurePk = (table: string): string[] => {
|
|
64
|
+
let pk = state.pkFields.get(table);
|
|
65
|
+
if (!pk) {
|
|
66
|
+
pk = ["id"];
|
|
67
|
+
state.pkFields.set(table, pk);
|
|
68
|
+
}
|
|
69
|
+
return pk;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
dialect: "memory",
|
|
74
|
+
|
|
75
|
+
async selectOne(spec) {
|
|
76
|
+
const rows = await this.selectMany({ ...spec, limit: 1 });
|
|
77
|
+
return rows[0] ?? null;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async selectMany(spec) {
|
|
81
|
+
const t = state.tables.get(spec.table);
|
|
82
|
+
if (!t) return [];
|
|
83
|
+
let rows = [...t.values()].filter((r) => matchesWhere(r, spec.where));
|
|
84
|
+
if (spec.orderBy) {
|
|
85
|
+
for (const ob of [...spec.orderBy].reverse()) {
|
|
86
|
+
rows.sort((a, b) => compareValues(a[ob.column], b[ob.column]) * (ob.direction === "asc" ? 1 : -1));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (spec.offset) rows = rows.slice(spec.offset);
|
|
90
|
+
if (spec.limit !== undefined) rows = rows.slice(0, spec.limit);
|
|
91
|
+
return rows.map((r) => projectColumns(r, spec.columns));
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async count(spec) {
|
|
95
|
+
const t = state.tables.get(spec.table);
|
|
96
|
+
if (!t) return 0;
|
|
97
|
+
return [...t.values()].filter((r) => matchesWhere(r, spec.where)).length;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async insert(spec) {
|
|
101
|
+
const t = ensureTable(spec.table);
|
|
102
|
+
const pk = ensurePk(spec.table);
|
|
103
|
+
const row = { ...spec.values };
|
|
104
|
+
for (const f of pk) {
|
|
105
|
+
if (row[f] === undefined || row[f] === null) {
|
|
106
|
+
if (pk.length !== 1) {
|
|
107
|
+
throw new ConstraintViolationError(
|
|
108
|
+
`Composite PK requires all values; missing ${f}`, { kind: "not_null", table: spec.table, field: f },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const next = state.counters.get(spec.table) ?? 1;
|
|
112
|
+
row[f] = next;
|
|
113
|
+
state.counters.set(spec.table, next + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const key = pkKey(row, pk);
|
|
117
|
+
if (t.has(key)) {
|
|
118
|
+
throw new ConstraintViolationError(
|
|
119
|
+
`Unique violation on ${spec.table} PK ${key}`, { kind: "unique", table: spec.table, field: pk[0]! },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
t.set(key, structuredClone(row));
|
|
123
|
+
return projectColumns(row, spec.returning);
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async insertMany(spec) {
|
|
127
|
+
const out: Row[] = [];
|
|
128
|
+
for (const values of spec.rows) {
|
|
129
|
+
out.push(await this.insert({ table: spec.table, values, returning: spec.returning }));
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async update(spec) {
|
|
135
|
+
const t = state.tables.get(spec.table);
|
|
136
|
+
if (!t) return null;
|
|
137
|
+
const matches = [...t.entries()].filter(([, r]) => matchesWhere(r, spec.where));
|
|
138
|
+
if (matches.length === 0) return null;
|
|
139
|
+
const [key, row] = matches[0]!;
|
|
140
|
+
const updated = { ...row, ...spec.values };
|
|
141
|
+
t.set(key, structuredClone(updated));
|
|
142
|
+
return projectColumns(updated, spec.returning);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
async updateMany(spec) {
|
|
146
|
+
const t = state.tables.get(spec.table);
|
|
147
|
+
if (!t) return 0;
|
|
148
|
+
let n = 0;
|
|
149
|
+
for (const [key, row] of t.entries()) {
|
|
150
|
+
if (!matchesWhere(row, spec.where)) continue;
|
|
151
|
+
t.set(key, structuredClone({ ...row, ...spec.values }));
|
|
152
|
+
n++;
|
|
153
|
+
}
|
|
154
|
+
return n;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async delete(spec) {
|
|
158
|
+
return this.deleteMany(spec);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async deleteMany(spec) {
|
|
162
|
+
const t = state.tables.get(spec.table);
|
|
163
|
+
if (!t) return 0;
|
|
164
|
+
const keysToDelete = [...t.entries()].filter(([, r]) => matchesWhere(r, spec.where)).map(([k]) => k);
|
|
165
|
+
for (const k of keysToDelete) t.delete(k);
|
|
166
|
+
return keysToDelete.length;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
async transaction(fn) {
|
|
170
|
+
const snapshot = snapshotState(state);
|
|
171
|
+
try {
|
|
172
|
+
return await fn(this);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
restoreState(state, snapshot);
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function projectColumns(row: Row, columns: string[]): Row {
|
|
182
|
+
if (columns.length === 0) return { ...row };
|
|
183
|
+
const out: Row = {};
|
|
184
|
+
for (const c of columns) {
|
|
185
|
+
if (c in row) out[c] = row[c];
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function pkKey(row: Row, pkFields: string[]): string {
|
|
191
|
+
return pkFields.map((f) => String(row[f])).join(",");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function matchesWhere(row: Row, where?: WhereClause): boolean {
|
|
195
|
+
if (!where) return true;
|
|
196
|
+
switch (where.kind) {
|
|
197
|
+
case "eq":
|
|
198
|
+
return row[where.column] === where.value;
|
|
199
|
+
case "ne":
|
|
200
|
+
return row[where.column] !== where.value;
|
|
201
|
+
case "gt":
|
|
202
|
+
return compareValues(row[where.column], where.value) > 0;
|
|
203
|
+
case "gte":
|
|
204
|
+
return compareValues(row[where.column], where.value) >= 0;
|
|
205
|
+
case "lt":
|
|
206
|
+
return compareValues(row[where.column], where.value) < 0;
|
|
207
|
+
case "lte":
|
|
208
|
+
return compareValues(row[where.column], where.value) <= 0;
|
|
209
|
+
case "in":
|
|
210
|
+
return where.values.some((v) => row[where.column] === v);
|
|
211
|
+
case "like":
|
|
212
|
+
return likeMatch(row[where.column], where.pattern);
|
|
213
|
+
case "isNull": {
|
|
214
|
+
const isNull = row[where.column] === null || row[where.column] === undefined;
|
|
215
|
+
return where.not ? !isNull : isNull;
|
|
216
|
+
}
|
|
217
|
+
case "and":
|
|
218
|
+
return where.clauses.every((c) => matchesWhere(row, c));
|
|
219
|
+
default: {
|
|
220
|
+
const exhaustive: never = where;
|
|
221
|
+
throw new Error(`Unhandled WhereClause kind: ${JSON.stringify(exhaustive)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function compareValues(a: unknown, b: unknown): number {
|
|
227
|
+
if (a === b) return 0;
|
|
228
|
+
if (a === null || a === undefined) return -1;
|
|
229
|
+
if (b === null || b === undefined) return 1;
|
|
230
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
231
|
+
return String(a).localeCompare(String(b));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// SQL LIKE: % → .*, _ → ., case-sensitive. Other regex metacharacters in `pattern` are escaped first.
|
|
235
|
+
function likeMatch(value: unknown, pattern: string): boolean {
|
|
236
|
+
if (typeof value !== "string") return false;
|
|
237
|
+
const re = new RegExp("^" + pattern.replace(/[.+*?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".") + "$");
|
|
238
|
+
return re.test(value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function snapshotState(state: State): State {
|
|
242
|
+
return {
|
|
243
|
+
tables: new Map([...state.tables.entries()].map(([t, m]) => [t, new Map([...m.entries()].map(([k, r]) => [k, structuredClone(r)]))])),
|
|
244
|
+
pkFields: new Map(state.pkFields),
|
|
245
|
+
counters: new Map(state.counters),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function restoreState(state: State, snapshot: State): void {
|
|
250
|
+
state.tables.clear();
|
|
251
|
+
for (const [t, m] of snapshot.tables.entries()) state.tables.set(t, m);
|
|
252
|
+
state.pkFields.clear();
|
|
253
|
+
for (const [k, v] of snapshot.pkFields.entries()) state.pkFields.set(k, v);
|
|
254
|
+
state.counters.clear();
|
|
255
|
+
for (const [k, v] of snapshot.counters.entries()) state.counters.set(k, v);
|
|
256
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { inMemoryDriver, type InMemoryDriverOptions } from "./in-memory-driver.js";
|
|
2
|
+
export { kyselyDriver, type KyselyDriverOptions, type KyselyDriverPublic } from "./kysely-driver.js";
|
|
3
|
+
export { drizzleDriver, type DrizzleDriverOptions, type DrizzleDriverPublic } from "./drizzle-driver.js";
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { Kysely, ExpressionBuilder, SelectQueryBuilder, Expression, SqlBool } from "kysely";
|
|
2
|
+
import type {
|
|
3
|
+
PersistenceDriver, SelectSpec, CountSpec, InsertSpec, InsertManySpec,
|
|
4
|
+
UpdateSpec, UpdateManySpec, DeleteSpec, DeleteManySpec, WhereClause, Row,
|
|
5
|
+
} from "../persistence-driver.js";
|
|
6
|
+
import { ConstraintViolationError } from "../errors.js";
|
|
7
|
+
|
|
8
|
+
// Kysely's fluent builder is heavily generic on the database schema, but this driver
|
|
9
|
+
// is metadata-driven and accepts any table name at runtime. Confine the schema-agnostic
|
|
10
|
+
// escape to these aliases rather than per-call `as any`.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
type RawKysely = Kysely<any>;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
type AnyExprBuilder = ExpressionBuilder<any, any>;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
type AnyQuery = SelectQueryBuilder<any, any, any>;
|
|
17
|
+
|
|
18
|
+
export interface KyselyDriverOptions {
|
|
19
|
+
db: Kysely<Record<string, Row>>;
|
|
20
|
+
dialect: "sqlite" | "postgres";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KyselyDriverPublic extends PersistenceDriver {
|
|
24
|
+
/** The underlying Kysely instance — power-user escape hatch. */
|
|
25
|
+
readonly db: Kysely<Record<string, Row>>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function kyselyDriver(opts: KyselyDriverOptions): KyselyDriverPublic {
|
|
29
|
+
return makeKyselyDriver(opts.db, opts.dialect);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeKyselyDriver(
|
|
33
|
+
db: Kysely<Record<string, Row>>,
|
|
34
|
+
dialect: "sqlite" | "postgres",
|
|
35
|
+
): KyselyDriverPublic {
|
|
36
|
+
return {
|
|
37
|
+
db,
|
|
38
|
+
dialect,
|
|
39
|
+
|
|
40
|
+
async selectOne(spec: SelectSpec): Promise<Row | null> {
|
|
41
|
+
let q = (db as RawKysely).selectFrom(spec.table).select(spec.columns);
|
|
42
|
+
q = applyWhere(q, spec.where);
|
|
43
|
+
q = applyOrderLimit(q, spec.orderBy, 1, undefined);
|
|
44
|
+
const rows = await q.execute();
|
|
45
|
+
return (rows[0] as Row | undefined) ?? null;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async selectMany(spec: SelectSpec): Promise<Row[]> {
|
|
49
|
+
let q = (db as RawKysely).selectFrom(spec.table).select(spec.columns);
|
|
50
|
+
q = applyWhere(q, spec.where);
|
|
51
|
+
q = applyOrderLimit(q, spec.orderBy, spec.limit, spec.offset);
|
|
52
|
+
return (await q.execute()) as Row[];
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async count(spec: CountSpec): Promise<number> {
|
|
56
|
+
let q = (db as RawKysely).selectFrom(spec.table).select((eb: AnyExprBuilder) => eb.fn.countAll().as("c"));
|
|
57
|
+
q = applyWhere(q, spec.where);
|
|
58
|
+
const rows = (await q.execute()) as Array<{ c: number | string | bigint }>;
|
|
59
|
+
const v = rows[0]?.c;
|
|
60
|
+
return typeof v === "number" ? v : Number(v ?? 0);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async insert(spec: InsertSpec): Promise<Row> {
|
|
64
|
+
try {
|
|
65
|
+
const result = await (db as RawKysely)
|
|
66
|
+
.insertInto(spec.table)
|
|
67
|
+
.values(spec.values)
|
|
68
|
+
.returning(spec.returning)
|
|
69
|
+
.executeTakeFirstOrThrow();
|
|
70
|
+
return result as Row;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async insertMany(spec: InsertManySpec): Promise<Row[]> {
|
|
77
|
+
try {
|
|
78
|
+
const rows = await (db as RawKysely)
|
|
79
|
+
.insertInto(spec.table)
|
|
80
|
+
.values(spec.rows)
|
|
81
|
+
.returning(spec.returning)
|
|
82
|
+
.execute();
|
|
83
|
+
return rows as Row[];
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async update(spec: UpdateSpec): Promise<Row | null> {
|
|
90
|
+
try {
|
|
91
|
+
let q = (db as RawKysely).updateTable(spec.table).set(spec.values);
|
|
92
|
+
q = applyWhere(q, spec.where);
|
|
93
|
+
const rows = await q.returning(spec.returning).execute();
|
|
94
|
+
return ((rows as Row[])[0]) ?? null;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async updateMany(spec: UpdateManySpec): Promise<number> {
|
|
101
|
+
try {
|
|
102
|
+
let q = (db as RawKysely).updateTable(spec.table).set(spec.values);
|
|
103
|
+
q = applyWhere(q, spec.where);
|
|
104
|
+
const result = await q.executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined;
|
|
105
|
+
return Number(result?.numUpdatedRows ?? 0);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async delete(spec: DeleteSpec): Promise<number> {
|
|
112
|
+
try {
|
|
113
|
+
let q = (db as RawKysely).deleteFrom(spec.table);
|
|
114
|
+
q = applyWhere(q, spec.where);
|
|
115
|
+
const result = await q.executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined;
|
|
116
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async deleteMany(spec: DeleteManySpec): Promise<number> {
|
|
123
|
+
try {
|
|
124
|
+
let q = (db as RawKysely).deleteFrom(spec.table);
|
|
125
|
+
q = applyWhere(q, spec.where);
|
|
126
|
+
const result = await q.executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined;
|
|
127
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async transaction<T>(fn: (txDriver: PersistenceDriver) => Promise<T>): Promise<T> {
|
|
134
|
+
return await db.transaction().execute(async (trx) => {
|
|
135
|
+
const txDriver = makeKyselyDriver(trx as Kysely<Record<string, Row>>, dialect);
|
|
136
|
+
return await fn(txDriver);
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface HasWhere<Q> {
|
|
143
|
+
where(cb: (eb: AnyExprBuilder) => Expression<SqlBool>): Q;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyWhere<Q extends HasWhere<Q>>(q: Q, where: WhereClause | undefined): Q {
|
|
147
|
+
if (!where) return q;
|
|
148
|
+
return q.where((eb: AnyExprBuilder) => buildExpression(eb, where));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildExpression(eb: AnyExprBuilder, w: WhereClause): Expression<SqlBool> {
|
|
152
|
+
switch (w.kind) {
|
|
153
|
+
case "eq": return w.value === null ? eb(w.column, "is", null) : eb(w.column, "=", w.value);
|
|
154
|
+
case "ne": return w.value === null ? eb(w.column, "is not", null) : eb(w.column, "<>", w.value);
|
|
155
|
+
case "gt": return eb(w.column, ">", w.value);
|
|
156
|
+
case "gte": return eb(w.column, ">=", w.value);
|
|
157
|
+
case "lt": return eb(w.column, "<", w.value);
|
|
158
|
+
case "lte": return eb(w.column, "<=", w.value);
|
|
159
|
+
case "like": return eb(w.column, "like", w.pattern);
|
|
160
|
+
case "in": return eb(w.column, "in", w.values);
|
|
161
|
+
case "isNull": return w.not ? eb(w.column, "is not", null) : eb(w.column, "is", null);
|
|
162
|
+
case "and": return eb.and(w.clauses.map((c: WhereClause) => buildExpression(eb, c)));
|
|
163
|
+
default: {
|
|
164
|
+
const exhaustive: never = w;
|
|
165
|
+
throw new Error(`Unhandled WhereClause kind: ${JSON.stringify(exhaustive)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function applyOrderLimit(
|
|
171
|
+
q: AnyQuery,
|
|
172
|
+
orderBy: { column: string; direction: "asc" | "desc" }[] | undefined,
|
|
173
|
+
limit: number | undefined,
|
|
174
|
+
offset: number | undefined,
|
|
175
|
+
): AnyQuery {
|
|
176
|
+
let out = q;
|
|
177
|
+
if (orderBy) {
|
|
178
|
+
for (const ob of orderBy) {
|
|
179
|
+
out = out.orderBy(ob.column, ob.direction);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (limit !== undefined) out = out.limit(limit);
|
|
183
|
+
if (offset !== undefined) out = out.offset(offset);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mapDriverError(err: unknown, table: string, dialect: "sqlite" | "postgres"): unknown {
|
|
188
|
+
if (!(err instanceof Error)) return err;
|
|
189
|
+
const msg = err.message;
|
|
190
|
+
const code = (err as { code?: string }).code;
|
|
191
|
+
|
|
192
|
+
if (dialect === "sqlite") {
|
|
193
|
+
const kind = sqliteConstraintKind(code, msg);
|
|
194
|
+
if (kind !== null) {
|
|
195
|
+
const field = extractSqliteField(kind, msg);
|
|
196
|
+
return new ConstraintViolationError(msg, {
|
|
197
|
+
kind, table, ...(field !== undefined ? { field } : {}), cause: err,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return err;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Postgres pg driver attaches SQLSTATE on `.code`.
|
|
204
|
+
if (code === "23505") return new ConstraintViolationError(msg, { kind: "unique", table, cause: err });
|
|
205
|
+
if (code === "23503") return new ConstraintViolationError(msg, { kind: "foreign_key", table, cause: err });
|
|
206
|
+
if (code === "23502") return new ConstraintViolationError(msg, { kind: "not_null", table, cause: err });
|
|
207
|
+
if (code === "23514") return new ConstraintViolationError(msg, { kind: "check", table, cause: err });
|
|
208
|
+
|
|
209
|
+
return err;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// libsql wraps better-sqlite3, which maps SQLite extended error codes to
|
|
213
|
+
// strings like SQLITE_CONSTRAINT_UNIQUE. Older/server-mode libsql may only
|
|
214
|
+
// surface SQLITE_CONSTRAINT and signal the kind in the message — fall through
|
|
215
|
+
// to message-parse in that case.
|
|
216
|
+
function sqliteConstraintKind(
|
|
217
|
+
code: string | undefined,
|
|
218
|
+
msg: string,
|
|
219
|
+
): "unique" | "foreign_key" | "not_null" | "check" | null {
|
|
220
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") return "unique";
|
|
221
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") return "foreign_key";
|
|
222
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") return "not_null";
|
|
223
|
+
if (code === "SQLITE_CONSTRAINT_CHECK") return "check";
|
|
224
|
+
if (code === "SQLITE_CONSTRAINT" || code === undefined) {
|
|
225
|
+
if (msg.includes("UNIQUE constraint failed")) return "unique";
|
|
226
|
+
if (msg.includes("FOREIGN KEY constraint failed")) return "foreign_key";
|
|
227
|
+
if (msg.includes("NOT NULL constraint failed")) return "not_null";
|
|
228
|
+
if (msg.includes("CHECK constraint failed")) return "check";
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function extractSqliteField(kind: "unique" | "foreign_key" | "not_null" | "check", msg: string): string | undefined {
|
|
234
|
+
const pattern = kind === "unique" ? /UNIQUE constraint failed: ([^\s,]+)/
|
|
235
|
+
: kind === "not_null" ? /NOT NULL constraint failed: ([^\s,]+)/
|
|
236
|
+
: null;
|
|
237
|
+
if (!pattern) return undefined;
|
|
238
|
+
const m = msg.match(pattern);
|
|
239
|
+
return m ? m[1]?.split(".")[1] : undefined;
|
|
240
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-entity filter+sort allowlist generated by codegen-ts from @filterable/@sortable
|
|
3
|
+
* metadata. Validates incoming HTTP filter params at the route boundary so the
|
|
4
|
+
* Drizzle query layer never sees an unwhitelisted (field, op) combination.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// FILTER_OPS, FilterOp, OPS_BY_SUBTYPE, and opsForSubType are canonical in
|
|
8
|
+
// @metaobjectsdev/metadata — import from there and re-export for backwards compat.
|
|
9
|
+
import {
|
|
10
|
+
FILTER_OPS,
|
|
11
|
+
OPS_BY_SUBTYPE,
|
|
12
|
+
opsForSubType,
|
|
13
|
+
type FilterOp,
|
|
14
|
+
} from "@metaobjectsdev/metadata";
|
|
15
|
+
export { FILTER_OPS, OPS_BY_SUBTYPE, opsForSubType, type FilterOp };
|
|
16
|
+
|
|
17
|
+
export type FilterSubType = "string" | "number" | "boolean" | "datetime";
|
|
18
|
+
|
|
19
|
+
export interface FilterFieldRule {
|
|
20
|
+
readonly ops: readonly FilterOp[];
|
|
21
|
+
readonly subType: FilterSubType;
|
|
22
|
+
readonly leadingWildcard: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FilterAllowlist = Readonly<Record<string, FilterFieldRule>>;
|
|
26
|
+
|
|
27
|
+
export interface SortFieldRule {
|
|
28
|
+
readonly defaultOrder?: "asc" | "desc";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SortAllowlist = Readonly<Record<string, SortFieldRule>>;
|