@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,474 @@
|
|
|
1
|
+
// Drizzle driver for ObjectManager.
|
|
2
|
+
//
|
|
3
|
+
// Lets a consumer that already has Drizzle wired up (schema, connection, types)
|
|
4
|
+
// also expose the same database via ObjectManager — no second connection, no
|
|
5
|
+
// separate ORM. The driver is dynamic: it resolves table names from the user's
|
|
6
|
+
// schema object at construction time and dispatches per-request.
|
|
7
|
+
//
|
|
8
|
+
// Why exists: runtime-ts ships with Kysely + in-memory drivers, but most TS
|
|
9
|
+
// apps use Drizzle. Shipping a Drizzle driver lets ObjectManager layer on top
|
|
10
|
+
// of an existing Drizzle setup instead of forcing a parallel ORM stack.
|
|
11
|
+
//
|
|
12
|
+
// Java analog: ObjectConnection backed by a JDBC DataSource.
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
and as drzAnd,
|
|
16
|
+
eq as drzEq,
|
|
17
|
+
ne as drzNe,
|
|
18
|
+
gt as drzGt,
|
|
19
|
+
gte as drzGte,
|
|
20
|
+
lt as drzLt,
|
|
21
|
+
lte as drzLte,
|
|
22
|
+
like as drzLike,
|
|
23
|
+
inArray as drzInArray,
|
|
24
|
+
isNull as drzIsNull,
|
|
25
|
+
isNotNull as drzIsNotNull,
|
|
26
|
+
asc as drzAsc,
|
|
27
|
+
desc as drzDesc,
|
|
28
|
+
count as drzCount,
|
|
29
|
+
getTableName,
|
|
30
|
+
getTableColumns,
|
|
31
|
+
type SQL,
|
|
32
|
+
} from "drizzle-orm";
|
|
33
|
+
import type {
|
|
34
|
+
PersistenceDriver,
|
|
35
|
+
SelectSpec,
|
|
36
|
+
CountSpec,
|
|
37
|
+
InsertSpec,
|
|
38
|
+
InsertManySpec,
|
|
39
|
+
UpdateSpec,
|
|
40
|
+
UpdateManySpec,
|
|
41
|
+
DeleteSpec,
|
|
42
|
+
DeleteManySpec,
|
|
43
|
+
WhereClause,
|
|
44
|
+
Row,
|
|
45
|
+
} from "../persistence-driver.js";
|
|
46
|
+
import { ConstraintViolationError } from "../errors.js";
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Public surface
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Minimal Drizzle DB shape we need. We don't import a concrete backend type
|
|
54
|
+
* (PgDatabase / BaseSQLiteDatabase / ...) so the driver works for any of them.
|
|
55
|
+
* The runtime calls match Drizzle's universal builder API.
|
|
56
|
+
*/
|
|
57
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic table dispatch
|
|
58
|
+
type AnyDrizzleDB = any;
|
|
59
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic table dispatch
|
|
60
|
+
type AnyTable = any;
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic table dispatch
|
|
62
|
+
type AnyColumn = any;
|
|
63
|
+
|
|
64
|
+
export interface DrizzleDriverOptions {
|
|
65
|
+
/** A Drizzle database instance — `drizzle(...)` from any backend module. */
|
|
66
|
+
db: AnyDrizzleDB;
|
|
67
|
+
/**
|
|
68
|
+
* The user's schema namespace, typically `import * as schema from "./schema"`.
|
|
69
|
+
* The driver resolves table-name strings via `getTableName(table)` so the
|
|
70
|
+
* JS variable name doesn't have to match the SQL table name.
|
|
71
|
+
*/
|
|
72
|
+
schema: Record<string, unknown>;
|
|
73
|
+
dialect: "sqlite" | "postgres";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DrizzleDriverPublic extends PersistenceDriver {
|
|
77
|
+
/** The underlying Drizzle instance — escape hatch for hand-written queries. */
|
|
78
|
+
readonly db: AnyDrizzleDB;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function drizzleDriver(opts: DrizzleDriverOptions): DrizzleDriverPublic {
|
|
82
|
+
const tables = indexTables(opts.schema);
|
|
83
|
+
return makeDrizzleDriver(opts.db, tables, opts.dialect);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Internals
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/** SQL table name → Drizzle table const. */
|
|
91
|
+
type TableIndex = Map<string, AnyTable>;
|
|
92
|
+
|
|
93
|
+
function indexTables(schema: Record<string, unknown>): TableIndex {
|
|
94
|
+
const index: TableIndex = new Map();
|
|
95
|
+
for (const value of Object.values(schema)) {
|
|
96
|
+
if (!isDrizzleTable(value)) continue;
|
|
97
|
+
const tableName = getTableName(value as AnyTable);
|
|
98
|
+
if (typeof tableName === "string") index.set(tableName, value as AnyTable);
|
|
99
|
+
}
|
|
100
|
+
return index;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isDrizzleTable(v: unknown): boolean {
|
|
104
|
+
// Drizzle tables expose a non-enumerable Symbol-keyed metadata block.
|
|
105
|
+
// We don't depend on the exact symbol; `getTableName` will throw for non-tables,
|
|
106
|
+
// so we let `try { getTableName(v) }` discriminate.
|
|
107
|
+
if (v === null || typeof v !== "object") return false;
|
|
108
|
+
try {
|
|
109
|
+
return typeof getTableName(v as AnyTable) === "string";
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function makeDrizzleDriver(
|
|
116
|
+
db: AnyDrizzleDB,
|
|
117
|
+
tables: TableIndex,
|
|
118
|
+
dialect: "sqlite" | "postgres",
|
|
119
|
+
): DrizzleDriverPublic {
|
|
120
|
+
function requireTable(name: string): AnyTable {
|
|
121
|
+
const t = tables.get(name);
|
|
122
|
+
if (!t) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`drizzleDriver: no table named '${name}' in the provided schema (known: ${[...tables.keys()].sort().join(", ") || "<none>"})`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return t;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Build a `dbColumnName → DrizzleColumn` map for a table. */
|
|
131
|
+
function columnMap(table: AnyTable): Map<string, AnyColumn> {
|
|
132
|
+
const m = new Map<string, AnyColumn>();
|
|
133
|
+
const cols = getTableColumns(table) as Record<string, AnyColumn>;
|
|
134
|
+
for (const col of Object.values(cols)) {
|
|
135
|
+
// `col.name` is the SQL column name; the JS key is the JS field name.
|
|
136
|
+
const dbName = (col as { name: string }).name;
|
|
137
|
+
if (typeof dbName === "string") m.set(dbName, col);
|
|
138
|
+
}
|
|
139
|
+
return m;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Build a `jsFieldName → DrizzleColumn` map (used when constructing input rows). */
|
|
143
|
+
function jsColumnMap(table: AnyTable): Map<string, AnyColumn> {
|
|
144
|
+
const m = new Map<string, AnyColumn>();
|
|
145
|
+
const cols = getTableColumns(table) as Record<string, AnyColumn>;
|
|
146
|
+
for (const [jsKey, col] of Object.entries(cols)) {
|
|
147
|
+
m.set(jsKey, col);
|
|
148
|
+
}
|
|
149
|
+
return m;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert spec.values (keyed by DB column names) into the JS-keyed object
|
|
154
|
+
* Drizzle's `.values()`/`.set()` expects. Skips columns not present on the
|
|
155
|
+
* table.
|
|
156
|
+
*/
|
|
157
|
+
function toJsValues(table: AnyTable, dbKeyed: Row): Row {
|
|
158
|
+
const dbToJs = new Map<string, string>();
|
|
159
|
+
const cols = getTableColumns(table) as Record<string, AnyColumn>;
|
|
160
|
+
for (const [jsKey, col] of Object.entries(cols)) {
|
|
161
|
+
dbToJs.set((col as { name: string }).name, jsKey);
|
|
162
|
+
}
|
|
163
|
+
const out: Row = {};
|
|
164
|
+
for (const [dbKey, val] of Object.entries(dbKeyed)) {
|
|
165
|
+
const jsKey = dbToJs.get(dbKey);
|
|
166
|
+
if (jsKey) out[jsKey] = val;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Result rows from `db.select({alias: column})` are already keyed by the
|
|
173
|
+
* alias we passed. Since `buildSelectMap` uses DB names as aliases, the
|
|
174
|
+
* row is already in the DB-keyed shape that ObjectManager expects — no
|
|
175
|
+
* translation needed.
|
|
176
|
+
*/
|
|
177
|
+
function toDbKeyedRow(_table: AnyTable, row: Row | undefined): Row {
|
|
178
|
+
return row ?? {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyWhere(qb: AnyTable, where: WhereClause | undefined, table: AnyTable): unknown {
|
|
182
|
+
if (!where) return undefined;
|
|
183
|
+
return buildExpression(where, columnMap(table));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applyOrder(qb: AnyDrizzleDB, orderBy: SelectSpec["orderBy"], table: AnyTable): AnyDrizzleDB {
|
|
187
|
+
if (!orderBy) return qb;
|
|
188
|
+
const cols = columnMap(table);
|
|
189
|
+
const exprs = orderBy
|
|
190
|
+
.map((ob) => {
|
|
191
|
+
const c = cols.get(ob.column);
|
|
192
|
+
if (!c) return undefined;
|
|
193
|
+
return ob.direction === "desc" ? drzDesc(c) : drzAsc(c);
|
|
194
|
+
})
|
|
195
|
+
.filter((e): e is SQL => e !== undefined);
|
|
196
|
+
return exprs.length > 0 ? qb.orderBy(...exprs) : qb;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function pickJsCols(table: AnyTable, dbColumnNames: string[]): AnyColumn[] {
|
|
200
|
+
const map = columnMap(table);
|
|
201
|
+
const out: AnyColumn[] = [];
|
|
202
|
+
for (const dbName of dbColumnNames) {
|
|
203
|
+
const c = map.get(dbName);
|
|
204
|
+
if (c) out.push(c);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
db,
|
|
211
|
+
dialect,
|
|
212
|
+
|
|
213
|
+
async selectOne(spec: SelectSpec): Promise<Row | null> {
|
|
214
|
+
const table = requireTable(spec.table);
|
|
215
|
+
const selectArg = buildSelectMap(table, spec.columns);
|
|
216
|
+
let q = db.select(selectArg).from(table);
|
|
217
|
+
const w = applyWhere(table, spec.where, table);
|
|
218
|
+
if (w !== undefined) q = q.where(w);
|
|
219
|
+
q = applyOrder(q, spec.orderBy, table);
|
|
220
|
+
q = q.limit(1);
|
|
221
|
+
const rows = await q;
|
|
222
|
+
const first = (rows as Row[])[0];
|
|
223
|
+
return first ? toDbKeyedRow(table, jsKeyedFromSelect(first, selectArg)) : null;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async selectMany(spec: SelectSpec): Promise<Row[]> {
|
|
227
|
+
const table = requireTable(spec.table);
|
|
228
|
+
const selectArg = buildSelectMap(table, spec.columns);
|
|
229
|
+
let q = db.select(selectArg).from(table);
|
|
230
|
+
const w = applyWhere(table, spec.where, table);
|
|
231
|
+
if (w !== undefined) q = q.where(w);
|
|
232
|
+
q = applyOrder(q, spec.orderBy, table);
|
|
233
|
+
if (spec.limit !== undefined) q = q.limit(spec.limit);
|
|
234
|
+
if (spec.offset !== undefined) q = q.offset(spec.offset);
|
|
235
|
+
const rows = (await q) as Row[];
|
|
236
|
+
return rows.map((r) => toDbKeyedRow(table, jsKeyedFromSelect(r, selectArg)));
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async count(spec: CountSpec): Promise<number> {
|
|
240
|
+
const table = requireTable(spec.table);
|
|
241
|
+
let q = db.select({ c: drzCount() }).from(table);
|
|
242
|
+
const w = applyWhere(table, spec.where, table);
|
|
243
|
+
if (w !== undefined) q = q.where(w);
|
|
244
|
+
const rows = (await q) as Array<{ c: number | string | bigint }>;
|
|
245
|
+
const v = rows[0]?.c;
|
|
246
|
+
return typeof v === "number" ? v : Number(v ?? 0);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
async insert(spec: InsertSpec): Promise<Row> {
|
|
250
|
+
const table = requireTable(spec.table);
|
|
251
|
+
try {
|
|
252
|
+
const result = await db
|
|
253
|
+
.insert(table)
|
|
254
|
+
.values(toJsValues(table, spec.values))
|
|
255
|
+
.returning(buildSelectMap(table, spec.returning));
|
|
256
|
+
const first = (result as Row[])[0];
|
|
257
|
+
if (!first) {
|
|
258
|
+
throw new Error(`drizzleDriver.insert: no row returned for '${spec.table}'`);
|
|
259
|
+
}
|
|
260
|
+
return toDbKeyedRow(table, jsKeyedFromSelect(first, buildSelectMap(table, spec.returning)));
|
|
261
|
+
} catch (err) {
|
|
262
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
async insertMany(spec: InsertManySpec): Promise<Row[]> {
|
|
267
|
+
const table = requireTable(spec.table);
|
|
268
|
+
try {
|
|
269
|
+
const values = spec.rows.map((r) => toJsValues(table, r));
|
|
270
|
+
const rows = (await db
|
|
271
|
+
.insert(table)
|
|
272
|
+
.values(values)
|
|
273
|
+
.returning(buildSelectMap(table, spec.returning))) as Row[];
|
|
274
|
+
return rows.map((r) =>
|
|
275
|
+
toDbKeyedRow(table, jsKeyedFromSelect(r, buildSelectMap(table, spec.returning))),
|
|
276
|
+
);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async update(spec: UpdateSpec): Promise<Row | null> {
|
|
283
|
+
const table = requireTable(spec.table);
|
|
284
|
+
try {
|
|
285
|
+
let q = db.update(table).set(toJsValues(table, spec.values));
|
|
286
|
+
const w = applyWhere(table, spec.where, table);
|
|
287
|
+
if (w !== undefined) q = q.where(w);
|
|
288
|
+
const rows = (await q.returning(buildSelectMap(table, spec.returning))) as Row[];
|
|
289
|
+
const first = rows[0];
|
|
290
|
+
return first
|
|
291
|
+
? toDbKeyedRow(table, jsKeyedFromSelect(first, buildSelectMap(table, spec.returning)))
|
|
292
|
+
: null;
|
|
293
|
+
} catch (err) {
|
|
294
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
async updateMany(spec: UpdateManySpec): Promise<number> {
|
|
299
|
+
const table = requireTable(spec.table);
|
|
300
|
+
try {
|
|
301
|
+
let q = db.update(table).set(toJsValues(table, spec.values));
|
|
302
|
+
const w = applyWhere(table, spec.where, table);
|
|
303
|
+
if (w !== undefined) q = q.where(w);
|
|
304
|
+
const result = await q;
|
|
305
|
+
return extractRowCount(result);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
async delete(spec: DeleteSpec): Promise<number> {
|
|
312
|
+
const table = requireTable(spec.table);
|
|
313
|
+
try {
|
|
314
|
+
let q = db.delete(table);
|
|
315
|
+
const w = applyWhere(table, spec.where, table);
|
|
316
|
+
if (w !== undefined) q = q.where(w);
|
|
317
|
+
const result = await q;
|
|
318
|
+
return extractRowCount(result);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
async deleteMany(spec: DeleteManySpec): Promise<number> {
|
|
325
|
+
const table = requireTable(spec.table);
|
|
326
|
+
try {
|
|
327
|
+
let q = db.delete(table);
|
|
328
|
+
const w = applyWhere(table, spec.where, table);
|
|
329
|
+
if (w !== undefined) q = q.where(w);
|
|
330
|
+
const result = await q;
|
|
331
|
+
return extractRowCount(result);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
throw mapDriverError(err, spec.table, dialect);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
async transaction<T>(fn: (txDriver: PersistenceDriver) => Promise<T>): Promise<T> {
|
|
338
|
+
return await db.transaction(async (tx: AnyDrizzleDB) => {
|
|
339
|
+
const txDriver = makeDrizzleDriver(tx, tables, dialect);
|
|
340
|
+
return await fn(txDriver);
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Build the `select` arg for Drizzle by mapping DB column names back to
|
|
348
|
+
* the table's JS columns. Drizzle expects a `Record<aliasKey, column>`;
|
|
349
|
+
* we use the DB column name as the alias key so result rows are easy to
|
|
350
|
+
* normalize back to a DB-keyed shape.
|
|
351
|
+
*/
|
|
352
|
+
function buildSelectMap(table: AnyTable, dbColumnNames: string[]): Record<string, AnyColumn> {
|
|
353
|
+
const allCols = getTableColumns(table) as Record<string, AnyColumn>;
|
|
354
|
+
const dbToCol = new Map<string, AnyColumn>();
|
|
355
|
+
for (const col of Object.values(allCols)) {
|
|
356
|
+
const name = (col as { name: string }).name;
|
|
357
|
+
if (typeof name === "string") dbToCol.set(name, col);
|
|
358
|
+
}
|
|
359
|
+
const out: Record<string, AnyColumn> = {};
|
|
360
|
+
for (const dbName of dbColumnNames) {
|
|
361
|
+
const c = dbToCol.get(dbName);
|
|
362
|
+
if (c) out[dbName] = c;
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Result rows from Drizzle are already keyed by whatever alias we passed in.
|
|
369
|
+
* Since we used DB names as aliases in buildSelectMap, the result row is
|
|
370
|
+
* already DB-keyed — we just pass it through to keep the function symmetric.
|
|
371
|
+
*/
|
|
372
|
+
function jsKeyedFromSelect(row: Row, _selectMap: Record<string, AnyColumn>): Row {
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function extractRowCount(result: unknown): number {
|
|
377
|
+
// Drizzle's update/delete return shape varies by backend. We support the
|
|
378
|
+
// common shapes; unknown shapes fall through to 0.
|
|
379
|
+
if (typeof result === "number") return result;
|
|
380
|
+
if (Array.isArray(result)) return result.length;
|
|
381
|
+
if (result && typeof result === "object") {
|
|
382
|
+
const obj = result as { rowsAffected?: number | bigint; rowCount?: number };
|
|
383
|
+
if (typeof obj.rowsAffected === "number") return obj.rowsAffected;
|
|
384
|
+
if (typeof obj.rowsAffected === "bigint") return Number(obj.rowsAffected);
|
|
385
|
+
if (typeof obj.rowCount === "number") return obj.rowCount;
|
|
386
|
+
}
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildExpression(w: WhereClause, cols: Map<string, AnyColumn>): unknown {
|
|
391
|
+
switch (w.kind) {
|
|
392
|
+
case "eq": {
|
|
393
|
+
const c = cols.get(w.column);
|
|
394
|
+
return w.value === null ? drzIsNull(c) : drzEq(c, w.value);
|
|
395
|
+
}
|
|
396
|
+
case "ne": {
|
|
397
|
+
const c = cols.get(w.column);
|
|
398
|
+
return w.value === null ? drzIsNotNull(c) : drzNe(c, w.value);
|
|
399
|
+
}
|
|
400
|
+
case "gt": return drzGt(cols.get(w.column), w.value);
|
|
401
|
+
case "gte": return drzGte(cols.get(w.column), w.value);
|
|
402
|
+
case "lt": return drzLt(cols.get(w.column), w.value);
|
|
403
|
+
case "lte": return drzLte(cols.get(w.column), w.value);
|
|
404
|
+
case "like": return drzLike(cols.get(w.column), w.pattern);
|
|
405
|
+
case "in": return drzInArray(cols.get(w.column), w.values);
|
|
406
|
+
case "isNull": return w.not ? drzIsNotNull(cols.get(w.column)) : drzIsNull(cols.get(w.column));
|
|
407
|
+
case "and": {
|
|
408
|
+
// drzAnd is typed as accepting SQLWrapper; our recursive return is
|
|
409
|
+
// structurally compatible but typed as unknown. Cast at the boundary.
|
|
410
|
+
const parts = w.clauses.map((c) => buildExpression(c, cols)) as SQL[];
|
|
411
|
+
return drzAnd(...parts);
|
|
412
|
+
}
|
|
413
|
+
default: {
|
|
414
|
+
const exhaustive: never = w;
|
|
415
|
+
throw new Error(`Unhandled WhereClause kind: ${JSON.stringify(exhaustive)}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Error normalization — same shape as kysely-driver so callers see one error
|
|
422
|
+
// type regardless of which driver they wired up.
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
function mapDriverError(err: unknown, table: string, dialect: "sqlite" | "postgres"): unknown {
|
|
426
|
+
if (!(err instanceof Error)) return err;
|
|
427
|
+
const msg = err.message;
|
|
428
|
+
const code = (err as { code?: string }).code;
|
|
429
|
+
|
|
430
|
+
if (dialect === "sqlite") {
|
|
431
|
+
const kind = sqliteConstraintKind(code, msg);
|
|
432
|
+
if (kind !== null) {
|
|
433
|
+
const field = extractSqliteField(kind, msg);
|
|
434
|
+
return new ConstraintViolationError(msg, {
|
|
435
|
+
kind, table, ...(field !== undefined ? { field } : {}), cause: err,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return err;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// pg-style SQLSTATE codes.
|
|
442
|
+
if (code === "23505") return new ConstraintViolationError(msg, { kind: "unique", table, cause: err });
|
|
443
|
+
if (code === "23503") return new ConstraintViolationError(msg, { kind: "foreign_key", table, cause: err });
|
|
444
|
+
if (code === "23502") return new ConstraintViolationError(msg, { kind: "not_null", table, cause: err });
|
|
445
|
+
if (code === "23514") return new ConstraintViolationError(msg, { kind: "check", table, cause: err });
|
|
446
|
+
|
|
447
|
+
return err;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sqliteConstraintKind(
|
|
451
|
+
code: string | undefined,
|
|
452
|
+
msg: string,
|
|
453
|
+
): "unique" | "foreign_key" | "not_null" | "check" | null {
|
|
454
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") return "unique";
|
|
455
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") return "foreign_key";
|
|
456
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") return "not_null";
|
|
457
|
+
if (code === "SQLITE_CONSTRAINT_CHECK") return "check";
|
|
458
|
+
if (code === "SQLITE_CONSTRAINT" || code === undefined) {
|
|
459
|
+
if (msg.includes("UNIQUE constraint failed")) return "unique";
|
|
460
|
+
if (msg.includes("FOREIGN KEY constraint failed")) return "foreign_key";
|
|
461
|
+
if (msg.includes("NOT NULL constraint failed")) return "not_null";
|
|
462
|
+
if (msg.includes("CHECK constraint failed")) return "check";
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function extractSqliteField(kind: "unique" | "foreign_key" | "not_null" | "check", msg: string): string | undefined {
|
|
468
|
+
const pattern = kind === "unique" ? /UNIQUE constraint failed: ([^\s,]+)/
|
|
469
|
+
: kind === "not_null" ? /NOT NULL constraint failed: ([^\s,]+)/
|
|
470
|
+
: null;
|
|
471
|
+
if (!pattern) return undefined;
|
|
472
|
+
const m = msg.match(pattern);
|
|
473
|
+
return m ? m[1]?.split(".")[1] : undefined;
|
|
474
|
+
}
|