@nextlyhq/adapter-drizzle 0.0.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 +22 -0
- package/README.md +9 -0
- package/dist/adapter-BxJVtttb.d.ts +592 -0
- package/dist/adapter-nvlxFkF-.d.cts +592 -0
- package/dist/core-CVO7WYDj.d.cts +74 -0
- package/dist/core-CVO7WYDj.d.ts +74 -0
- package/dist/error-um1d_3Uo.d.cts +105 -0
- package/dist/error-um1d_3Uo.d.ts +105 -0
- package/dist/index.cjs +1137 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.mjs +1134 -0
- package/dist/index.mjs.map +1 -0
- package/dist/migration-BbO5meEV.d.cts +622 -0
- package/dist/migration-Qe70wDOC.d.ts +622 -0
- package/dist/migrations.cjs +195 -0
- package/dist/migrations.cjs.map +1 -0
- package/dist/migrations.d.cts +351 -0
- package/dist/migrations.d.ts +351 -0
- package/dist/migrations.mjs +185 -0
- package/dist/migrations.mjs.map +1 -0
- package/dist/schema/index.cjs +10 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.cts +133 -0
- package/dist/schema/index.d.ts +133 -0
- package/dist/schema/index.mjs +7 -0
- package/dist/schema/index.mjs.map +1 -0
- package/dist/schema-BDn8WfSL.d.cts +200 -0
- package/dist/schema-BIQ0YQZ_.d.ts +200 -0
- package/dist/types/index.cjs +24 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +210 -0
- package/dist/types/index.d.ts +210 -0
- package/dist/types/index.mjs +21 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/version-check.cjs +154 -0
- package/dist/version-check.cjs.map +1 -0
- package/dist/version-check.d.cts +43 -0
- package/dist/version-check.d.ts +43 -0
- package/dist/version-check.mjs +150 -0
- package/dist/version-check.mjs.map +1 -0
- package/package.json +94 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var drizzleOrm = require('drizzle-orm');
|
|
4
|
+
|
|
5
|
+
// src/adapter.ts
|
|
6
|
+
function buildDrizzleWhere(table, where) {
|
|
7
|
+
const columns = drizzleOrm.getTableColumns(table);
|
|
8
|
+
return processWhereClause(columns, where);
|
|
9
|
+
}
|
|
10
|
+
function processWhereClause(columns, where) {
|
|
11
|
+
const parts = [];
|
|
12
|
+
if (where.and?.length) {
|
|
13
|
+
const andParts = where.and.map((item) => {
|
|
14
|
+
if (isWhereCondition(item)) {
|
|
15
|
+
return buildCondition(columns, item);
|
|
16
|
+
}
|
|
17
|
+
return processWhereClause(columns, item);
|
|
18
|
+
}).filter((p) => p !== void 0);
|
|
19
|
+
if (andParts.length) {
|
|
20
|
+
parts.push(drizzleOrm.and(...andParts));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (where.or?.length) {
|
|
24
|
+
const orParts = where.or.map((item) => {
|
|
25
|
+
if (isWhereCondition(item)) {
|
|
26
|
+
return buildCondition(columns, item);
|
|
27
|
+
}
|
|
28
|
+
return processWhereClause(columns, item);
|
|
29
|
+
}).filter((p) => p !== void 0);
|
|
30
|
+
if (orParts.length) {
|
|
31
|
+
parts.push(drizzleOrm.or(...orParts));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (where.not) {
|
|
35
|
+
const notItem = where.not;
|
|
36
|
+
const notCondition = isWhereCondition(notItem) ? buildCondition(columns, notItem) : processWhereClause(columns, notItem);
|
|
37
|
+
if (notCondition) {
|
|
38
|
+
parts.push(drizzleOrm.not(notCondition));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (parts.length === 0) return void 0;
|
|
42
|
+
if (parts.length === 1) return parts[0];
|
|
43
|
+
return drizzleOrm.and(...parts);
|
|
44
|
+
}
|
|
45
|
+
function isWhereCondition(item) {
|
|
46
|
+
return "column" in item && "op" in item;
|
|
47
|
+
}
|
|
48
|
+
function buildCondition(columns, cond) {
|
|
49
|
+
const column = columns[cond.column];
|
|
50
|
+
if (!column) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Column "${cond.column}" not found in table. Available: ${Object.keys(columns).join(", ")}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const col = column;
|
|
56
|
+
switch (cond.op) {
|
|
57
|
+
case "=":
|
|
58
|
+
return drizzleOrm.eq(col, cond.value);
|
|
59
|
+
case "!=":
|
|
60
|
+
return drizzleOrm.ne(col, cond.value);
|
|
61
|
+
case ">":
|
|
62
|
+
return drizzleOrm.gt(col, cond.value);
|
|
63
|
+
case "<":
|
|
64
|
+
return drizzleOrm.lt(col, cond.value);
|
|
65
|
+
case ">=":
|
|
66
|
+
return drizzleOrm.gte(col, cond.value);
|
|
67
|
+
case "<=":
|
|
68
|
+
return drizzleOrm.lte(col, cond.value);
|
|
69
|
+
case "LIKE":
|
|
70
|
+
return drizzleOrm.like(col, cond.value);
|
|
71
|
+
case "ILIKE":
|
|
72
|
+
return drizzleOrm.ilike(col, cond.value);
|
|
73
|
+
case "IN":
|
|
74
|
+
return drizzleOrm.inArray(col, cond.value);
|
|
75
|
+
case "NOT IN":
|
|
76
|
+
return drizzleOrm.notInArray(col, cond.value);
|
|
77
|
+
case "IS NULL":
|
|
78
|
+
return drizzleOrm.isNull(col);
|
|
79
|
+
case "IS NOT NULL":
|
|
80
|
+
return drizzleOrm.isNotNull(col);
|
|
81
|
+
case "BETWEEN":
|
|
82
|
+
return drizzleOrm.between(col, cond.value, cond.valueTo);
|
|
83
|
+
case "NOT BETWEEN":
|
|
84
|
+
return drizzleOrm.notBetween(col, cond.value, cond.valueTo);
|
|
85
|
+
case "CONTAINS":
|
|
86
|
+
return drizzleOrm.like(col, `%${String(cond.value)}%`);
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unsupported operator: ${cond.op}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/types/error.ts
|
|
93
|
+
function isDatabaseError(error) {
|
|
94
|
+
return typeof error === "object" && error !== null && "kind" in error && typeof error.kind === "string";
|
|
95
|
+
}
|
|
96
|
+
function createDatabaseError(options) {
|
|
97
|
+
const error = new Error(options.message);
|
|
98
|
+
error.name = "DatabaseError";
|
|
99
|
+
error.kind = options.kind;
|
|
100
|
+
if (options.code !== void 0) error.code = options.code;
|
|
101
|
+
if (options.constraint !== void 0) error.constraint = options.constraint;
|
|
102
|
+
if (options.table !== void 0) error.table = options.table;
|
|
103
|
+
if (options.column !== void 0) error.column = options.column;
|
|
104
|
+
if (options.detail !== void 0) error.detail = options.detail;
|
|
105
|
+
if (options.hint !== void 0) error.hint = options.hint;
|
|
106
|
+
if (options.cause !== void 0) error.cause = options.cause;
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/adapter.ts
|
|
111
|
+
var DrizzleAdapter = class {
|
|
112
|
+
// ============================================================
|
|
113
|
+
// Drizzle Query API Support
|
|
114
|
+
// ============================================================
|
|
115
|
+
/**
|
|
116
|
+
* Table resolver for looking up Drizzle table objects by name.
|
|
117
|
+
* When set, CRUD methods use Drizzle's query API instead of raw SQL.
|
|
118
|
+
* Set via setTableResolver() after boot-time schema loading.
|
|
119
|
+
*/
|
|
120
|
+
tableResolver = null;
|
|
121
|
+
/**
|
|
122
|
+
* Set the table resolver for Drizzle query API support.
|
|
123
|
+
* When a resolver is set, CRUD methods (select, insert, update, delete, upsert)
|
|
124
|
+
* will use Drizzle's query API (db.select().from(), etc.) instead of raw SQL
|
|
125
|
+
* string building. Falls back to raw SQL if the resolver doesn't have the table.
|
|
126
|
+
*
|
|
127
|
+
* @param resolver - TableResolver implementation (e.g. SchemaRegistry)
|
|
128
|
+
*/
|
|
129
|
+
setTableResolver(resolver) {
|
|
130
|
+
this.tableResolver = resolver;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get a Drizzle table object by name from the resolver.
|
|
134
|
+
* Returns null if no resolver is set or table is not found.
|
|
135
|
+
*/
|
|
136
|
+
getTableObject(tableName) {
|
|
137
|
+
return this.tableResolver?.getTable(tableName) ?? null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Map data keys from SQL column names (snake_case) to Drizzle JS property names (camelCase).
|
|
141
|
+
* Drizzle schemas define columns as e.g. `createdAt: timestamp("created_at")` — the JS
|
|
142
|
+
* property is `createdAt` but the SQL column is `created_at`. Services pass snake_case keys
|
|
143
|
+
* because they match the DB column names. This method maps them to the JS names Drizzle expects.
|
|
144
|
+
*/
|
|
145
|
+
mapDataToColumnNames(tableObj, data) {
|
|
146
|
+
if (!tableObj || typeof tableObj !== "object") return data;
|
|
147
|
+
const sqlToJs = /* @__PURE__ */ new Map();
|
|
148
|
+
const jsonColumns = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const [jsName, colDef] of Object.entries(
|
|
150
|
+
tableObj
|
|
151
|
+
)) {
|
|
152
|
+
if (!colDef || typeof colDef !== "object" || !("name" in colDef) || typeof colDef.name !== "string")
|
|
153
|
+
continue;
|
|
154
|
+
const sqlName = colDef.name;
|
|
155
|
+
sqlToJs.set(sqlName, jsName);
|
|
156
|
+
const dataType = colDef.dataType;
|
|
157
|
+
const columnType = colDef.columnType;
|
|
158
|
+
if (dataType === "json" || columnType === "PgJsonb" || columnType === "PgJson" || columnType === "MySqlJson" || columnType === "SQLiteTextJson") {
|
|
159
|
+
jsonColumns.add(jsName);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (sqlToJs.size === 0) return data;
|
|
163
|
+
const mapped = {};
|
|
164
|
+
for (const [key, value] of Object.entries(data)) {
|
|
165
|
+
const jsKey = sqlToJs.get(key) ?? key;
|
|
166
|
+
if (jsonColumns.has(jsKey) && typeof value === "string") {
|
|
167
|
+
try {
|
|
168
|
+
mapped[jsKey] = JSON.parse(value);
|
|
169
|
+
} catch {
|
|
170
|
+
mapped[jsKey] = value;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
mapped[jsKey] = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return mapped;
|
|
177
|
+
}
|
|
178
|
+
// ============================================================
|
|
179
|
+
// Connection Status (Default implementations, can override)
|
|
180
|
+
// ============================================================
|
|
181
|
+
/**
|
|
182
|
+
* Check if the adapter is currently connected.
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* Default implementation returns false. Subclasses should override
|
|
186
|
+
* to provide accurate connection status.
|
|
187
|
+
*
|
|
188
|
+
* @returns True if connected, false otherwise
|
|
189
|
+
*/
|
|
190
|
+
isConnected() {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get connection pool statistics.
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* Returns null by default. Subclasses with connection pooling
|
|
198
|
+
* should override to provide pool statistics.
|
|
199
|
+
*
|
|
200
|
+
* @returns Pool statistics or null if not applicable
|
|
201
|
+
*/
|
|
202
|
+
getPoolStats() {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
// ============================================================
|
|
206
|
+
// Timeout Utilities
|
|
207
|
+
// ============================================================
|
|
208
|
+
/**
|
|
209
|
+
* Default query timeout in milliseconds.
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* This value is used by executeWithTimeout() when no explicit timeout
|
|
213
|
+
* is provided. Subclasses should set this from their config.
|
|
214
|
+
*
|
|
215
|
+
* @default 15000 (15 seconds)
|
|
216
|
+
*
|
|
217
|
+
* @protected
|
|
218
|
+
*/
|
|
219
|
+
defaultQueryTimeoutMs = 15e3;
|
|
220
|
+
/**
|
|
221
|
+
* Execute an async operation with a timeout.
|
|
222
|
+
*
|
|
223
|
+
* @remarks
|
|
224
|
+
* Wraps an async operation with a timeout that aborts if the operation
|
|
225
|
+
* exceeds the specified duration. Uses Promise.race for clean timeout
|
|
226
|
+
* handling without memory leaks.
|
|
227
|
+
*
|
|
228
|
+
* When the timeout is reached, a DatabaseError with kind 'timeout' is thrown.
|
|
229
|
+
* Note that this does NOT cancel the underlying database query - it only
|
|
230
|
+
* prevents the calling code from waiting indefinitely. For true query
|
|
231
|
+
* cancellation, use database-level statement timeouts (PostgreSQL) or
|
|
232
|
+
* similar mechanisms.
|
|
233
|
+
*
|
|
234
|
+
* @param operation - Async operation to execute
|
|
235
|
+
* @param timeoutMs - Timeout in milliseconds (defaults to defaultQueryTimeoutMs)
|
|
236
|
+
* @returns Result of the operation
|
|
237
|
+
*
|
|
238
|
+
* @throws {DatabaseError} With kind 'timeout' if operation exceeds timeout
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* // Use default timeout
|
|
243
|
+
* const result = await adapter.executeWithTimeout(
|
|
244
|
+
* () => adapter.select('users', { limit: 1000 })
|
|
245
|
+
* );
|
|
246
|
+
*
|
|
247
|
+
* // Use custom timeout for specific operation
|
|
248
|
+
* const result = await adapter.executeWithTimeout(
|
|
249
|
+
* () => adapter.select('large_table'),
|
|
250
|
+
* 60000 // 60 seconds for large queries
|
|
251
|
+
* );
|
|
252
|
+
* ```
|
|
253
|
+
*
|
|
254
|
+
* @public
|
|
255
|
+
*/
|
|
256
|
+
async executeWithTimeout(operation, timeoutMs) {
|
|
257
|
+
const timeout = timeoutMs ?? this.defaultQueryTimeoutMs;
|
|
258
|
+
if (timeout <= 0) {
|
|
259
|
+
return operation();
|
|
260
|
+
}
|
|
261
|
+
let timeoutId;
|
|
262
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
263
|
+
timeoutId = setTimeout(() => {
|
|
264
|
+
reject(
|
|
265
|
+
this.createDatabaseError(
|
|
266
|
+
"timeout",
|
|
267
|
+
`Query execution timed out after ${timeout}ms`
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
}, timeout);
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
const result = await Promise.race([operation(), timeoutPromise]);
|
|
274
|
+
return result;
|
|
275
|
+
} finally {
|
|
276
|
+
if (timeoutId !== void 0) {
|
|
277
|
+
clearTimeout(timeoutId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Set the default query timeout.
|
|
283
|
+
*
|
|
284
|
+
* @remarks
|
|
285
|
+
* This method allows runtime configuration of the default timeout.
|
|
286
|
+
* Subclasses should call this in their constructor or connect() method
|
|
287
|
+
* based on their configuration.
|
|
288
|
+
*
|
|
289
|
+
* @param timeoutMs - Timeout in milliseconds (0 to disable)
|
|
290
|
+
*
|
|
291
|
+
* @protected
|
|
292
|
+
*/
|
|
293
|
+
setDefaultQueryTimeout(timeoutMs) {
|
|
294
|
+
this.defaultQueryTimeoutMs = timeoutMs;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get the current default query timeout.
|
|
298
|
+
*
|
|
299
|
+
* @returns Current default timeout in milliseconds
|
|
300
|
+
*
|
|
301
|
+
* @public
|
|
302
|
+
*/
|
|
303
|
+
getDefaultQueryTimeout() {
|
|
304
|
+
return this.defaultQueryTimeoutMs;
|
|
305
|
+
}
|
|
306
|
+
// ============================================================
|
|
307
|
+
// CRUD Operations (Default implementations, can override)
|
|
308
|
+
// ============================================================
|
|
309
|
+
/**
|
|
310
|
+
* Select multiple records from a table.
|
|
311
|
+
*
|
|
312
|
+
* @remarks
|
|
313
|
+
* Default implementation builds a SELECT query and executes it.
|
|
314
|
+
* Subclasses can override for optimization or dialect-specific features.
|
|
315
|
+
*
|
|
316
|
+
* @param table - Table name
|
|
317
|
+
* @param options - Select options (filtering, sorting, pagination)
|
|
318
|
+
* @returns Array of matching records
|
|
319
|
+
*
|
|
320
|
+
* @throws {DatabaseError} If query fails
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const users = await adapter.select('users', {
|
|
325
|
+
* where: { and: [{ column: 'role', op: '=', value: 'admin' }] },
|
|
326
|
+
* orderBy: [{ column: 'created_at', direction: 'desc' }],
|
|
327
|
+
* limit: 10
|
|
328
|
+
* });
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
async select(table, options) {
|
|
332
|
+
const tableObj = this.getTableObject(table);
|
|
333
|
+
if (tableObj) {
|
|
334
|
+
try {
|
|
335
|
+
const db = this.getDrizzle();
|
|
336
|
+
let query = db.select().from(tableObj);
|
|
337
|
+
if (options?.where) {
|
|
338
|
+
const whereCondition = buildDrizzleWhere(
|
|
339
|
+
tableObj,
|
|
340
|
+
options.where
|
|
341
|
+
);
|
|
342
|
+
if (whereCondition) {
|
|
343
|
+
query = query.where(whereCondition);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (options?.orderBy?.length) {
|
|
347
|
+
const columns = drizzleOrm.getTableColumns(tableObj);
|
|
348
|
+
const orderClauses = options.orderBy.map((o) => {
|
|
349
|
+
const col = columns[o.column];
|
|
350
|
+
if (!col) return void 0;
|
|
351
|
+
return o.direction === "desc" ? drizzleOrm.desc(col) : drizzleOrm.asc(col);
|
|
352
|
+
}).filter(Boolean);
|
|
353
|
+
if (orderClauses.length) {
|
|
354
|
+
query = query.orderBy(...orderClauses);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (options?.limit !== void 0) {
|
|
358
|
+
query = query.limit(options.limit);
|
|
359
|
+
}
|
|
360
|
+
if (options?.offset !== void 0) {
|
|
361
|
+
query = query.offset(options.offset);
|
|
362
|
+
}
|
|
363
|
+
return await query;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw this.handleQueryError(error, "select", table);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw this.createDatabaseError(
|
|
369
|
+
"query",
|
|
370
|
+
`Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
|
|
371
|
+
void 0
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Select a single record from a table.
|
|
376
|
+
*
|
|
377
|
+
* @remarks
|
|
378
|
+
* Default implementation uses `select()` with limit 1 and returns first result.
|
|
379
|
+
* Returns null if no matching record is found.
|
|
380
|
+
*
|
|
381
|
+
* @param table - Table name
|
|
382
|
+
* @param options - Select options
|
|
383
|
+
* @returns First matching record or null
|
|
384
|
+
*
|
|
385
|
+
* @throws {DatabaseError} If query fails
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* const user = await adapter.selectOne('users', {
|
|
390
|
+
* where: { and: [{ column: 'email', op: '=', value: 'user@example.com' }] }
|
|
391
|
+
* });
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
async selectOne(table, options) {
|
|
395
|
+
const results = await this.select(table, { ...options, limit: 1 });
|
|
396
|
+
return results.length > 0 ? results[0] : null;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Insert a single record into a table.
|
|
400
|
+
*
|
|
401
|
+
* @remarks
|
|
402
|
+
* Default implementation handles databases with and without RETURNING support.
|
|
403
|
+
* For databases without RETURNING (MySQL), performs INSERT followed by SELECT.
|
|
404
|
+
*
|
|
405
|
+
* @param table - Table name
|
|
406
|
+
* @param data - Record data to insert
|
|
407
|
+
* @param options - Insert options
|
|
408
|
+
* @returns Inserted record (with RETURNING columns if specified)
|
|
409
|
+
*
|
|
410
|
+
* @throws {DatabaseError} If insert fails
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* const user = await adapter.insert('users', {
|
|
415
|
+
* email: 'user@example.com',
|
|
416
|
+
* name: 'John Doe'
|
|
417
|
+
* }, { returning: ['id', 'email', 'created_at'] });
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
async insert(table, data, options) {
|
|
421
|
+
const tableObj = this.getTableObject(table);
|
|
422
|
+
if (tableObj) {
|
|
423
|
+
try {
|
|
424
|
+
const mappedData = this.mapDataToColumnNames(tableObj, data);
|
|
425
|
+
const db = this.getDrizzle();
|
|
426
|
+
const caps = this.getCapabilities();
|
|
427
|
+
if (caps.supportsReturning && options?.returning) {
|
|
428
|
+
const result2 = await db.insert(tableObj).values(mappedData).returning();
|
|
429
|
+
return Array.isArray(result2) ? result2[0] : result2;
|
|
430
|
+
}
|
|
431
|
+
const result = await db.insert(tableObj).values(mappedData);
|
|
432
|
+
if (!caps.supportsReturning && options?.returning) {
|
|
433
|
+
if (data.id !== void 0) {
|
|
434
|
+
return await this.selectOne(table, {
|
|
435
|
+
where: {
|
|
436
|
+
and: [{ column: "id", op: "=", value: data.id }]
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return Array.isArray(result) ? result[0] : result;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
throw this.handleQueryError(error, "insert", table);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
throw this.createDatabaseError(
|
|
447
|
+
"query",
|
|
448
|
+
`Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
|
|
449
|
+
void 0
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Insert multiple records into a table.
|
|
454
|
+
*
|
|
455
|
+
* @remarks
|
|
456
|
+
* Default implementation performs individual inserts in sequence.
|
|
457
|
+
* Subclasses can override for bulk insert optimization (e.g., COPY in PostgreSQL).
|
|
458
|
+
*
|
|
459
|
+
* @param table - Table name
|
|
460
|
+
* @param data - Array of records to insert
|
|
461
|
+
* @param options - Insert options
|
|
462
|
+
* @returns Inserted records (with RETURNING columns if specified)
|
|
463
|
+
*
|
|
464
|
+
* @throws {DatabaseError} If insert fails
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const users = await adapter.insertMany('users', [
|
|
469
|
+
* { email: 'user1@example.com', name: 'User 1' },
|
|
470
|
+
* { email: 'user2@example.com', name: 'User 2' }
|
|
471
|
+
* ], { returning: ['id'] });
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
async insertMany(table, data, options) {
|
|
475
|
+
if (data.length === 0) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
const results = [];
|
|
479
|
+
for (const record of data) {
|
|
480
|
+
const result = await this.insert(table, record, options);
|
|
481
|
+
results.push(result);
|
|
482
|
+
}
|
|
483
|
+
return results;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Update records in a table.
|
|
487
|
+
*
|
|
488
|
+
* @remarks
|
|
489
|
+
* Default implementation builds an UPDATE query with WHERE clause.
|
|
490
|
+
* Returns updated records if RETURNING is supported and requested.
|
|
491
|
+
*
|
|
492
|
+
* @param table - Table name
|
|
493
|
+
* @param data - Data to update
|
|
494
|
+
* @param where - Conditions for records to update
|
|
495
|
+
* @param options - Update options
|
|
496
|
+
* @returns Updated records (with RETURNING columns if specified)
|
|
497
|
+
*
|
|
498
|
+
* @throws {DatabaseError} If update fails
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```typescript
|
|
502
|
+
* const updated = await adapter.update('users',
|
|
503
|
+
* { status: 'active' },
|
|
504
|
+
* { and: [{ column: 'id', op: '=', value: userId }] },
|
|
505
|
+
* { returning: ['id', 'status', 'updated_at'] }
|
|
506
|
+
* );
|
|
507
|
+
* ```
|
|
508
|
+
*/
|
|
509
|
+
async update(table, data, where, options) {
|
|
510
|
+
const tableObj = this.getTableObject(table);
|
|
511
|
+
if (tableObj) {
|
|
512
|
+
try {
|
|
513
|
+
const db = this.getDrizzle();
|
|
514
|
+
const caps = this.getCapabilities();
|
|
515
|
+
const mappedData = this.mapDataToColumnNames(tableObj, data);
|
|
516
|
+
let query = db.update(tableObj).set(mappedData);
|
|
517
|
+
const whereCondition = buildDrizzleWhere(tableObj, where);
|
|
518
|
+
if (whereCondition) {
|
|
519
|
+
query = query.where(whereCondition);
|
|
520
|
+
}
|
|
521
|
+
if (caps.supportsReturning && options?.returning) {
|
|
522
|
+
return await query.returning();
|
|
523
|
+
}
|
|
524
|
+
await query;
|
|
525
|
+
if (!caps.supportsReturning && options?.returning) {
|
|
526
|
+
return await this.select(table, { where });
|
|
527
|
+
}
|
|
528
|
+
return [];
|
|
529
|
+
} catch (error) {
|
|
530
|
+
throw this.handleQueryError(error, "update", table);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
throw this.createDatabaseError(
|
|
534
|
+
"query",
|
|
535
|
+
`Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
|
|
536
|
+
void 0
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Delete records from a table.
|
|
541
|
+
*
|
|
542
|
+
* @remarks
|
|
543
|
+
* Default implementation builds a DELETE query with WHERE clause.
|
|
544
|
+
* Returns the number of deleted records.
|
|
545
|
+
*
|
|
546
|
+
* @param table - Table name
|
|
547
|
+
* @param where - Conditions for records to delete
|
|
548
|
+
* @param options - Delete options
|
|
549
|
+
* @returns Number of deleted records
|
|
550
|
+
*
|
|
551
|
+
* @throws {DatabaseError} If delete fails
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```typescript
|
|
555
|
+
* const count = await adapter.delete('users', {
|
|
556
|
+
* and: [{ column: 'status', op: '=', value: 'inactive' }]
|
|
557
|
+
* });
|
|
558
|
+
* console.log(`Deleted ${count} users`);
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
async delete(table, where, _options) {
|
|
562
|
+
const tableObj = this.getTableObject(table);
|
|
563
|
+
if (tableObj) {
|
|
564
|
+
try {
|
|
565
|
+
const db = this.getDrizzle();
|
|
566
|
+
let query = db.delete(tableObj);
|
|
567
|
+
const whereCondition = buildDrizzleWhere(tableObj, where);
|
|
568
|
+
if (whereCondition) {
|
|
569
|
+
query = query.where(whereCondition);
|
|
570
|
+
}
|
|
571
|
+
const result = await query;
|
|
572
|
+
return Array.isArray(result) ? result.length : result?.rowCount ?? result?.changes ?? 0;
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw this.handleQueryError(error, "delete", table);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
throw this.createDatabaseError(
|
|
578
|
+
"query",
|
|
579
|
+
`Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
|
|
580
|
+
void 0
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Upsert (INSERT or UPDATE) a record.
|
|
585
|
+
*
|
|
586
|
+
* @remarks
|
|
587
|
+
* Default implementation uses dialect-specific ON CONFLICT syntax.
|
|
588
|
+
* PostgreSQL/SQLite: ON CONFLICT ... DO UPDATE
|
|
589
|
+
* MySQL: ON DUPLICATE KEY UPDATE
|
|
590
|
+
*
|
|
591
|
+
* @param table - Table name
|
|
592
|
+
* @param data - Record data
|
|
593
|
+
* @param options - Upsert options (must specify conflict columns)
|
|
594
|
+
* @returns Upserted record (with RETURNING columns if specified)
|
|
595
|
+
*
|
|
596
|
+
* @throws {DatabaseError} If upsert fails
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```typescript
|
|
600
|
+
* const user = await adapter.upsert('users', {
|
|
601
|
+
* email: 'user@example.com',
|
|
602
|
+
* name: 'Updated Name'
|
|
603
|
+
* }, {
|
|
604
|
+
* conflictColumns: ['email'],
|
|
605
|
+
* updateColumns: ['name'],
|
|
606
|
+
* returning: ['id', 'email', 'name']
|
|
607
|
+
* });
|
|
608
|
+
* ```
|
|
609
|
+
*/
|
|
610
|
+
async upsert(table, data, options) {
|
|
611
|
+
const tableObj = this.getTableObject(table);
|
|
612
|
+
if (tableObj) {
|
|
613
|
+
try {
|
|
614
|
+
const db = this.getDrizzle();
|
|
615
|
+
const caps = this.getCapabilities();
|
|
616
|
+
const columns = drizzleOrm.getTableColumns(tableObj);
|
|
617
|
+
const conflictTarget = options.conflictColumns.map((col) => columns[col]).filter(Boolean);
|
|
618
|
+
const conflictSet = new Set(options.conflictColumns);
|
|
619
|
+
const updateData = {};
|
|
620
|
+
const updateColumns = options.updateColumns ?? Object.keys(data);
|
|
621
|
+
for (const key of updateColumns) {
|
|
622
|
+
if (!conflictSet.has(key) && key in data) {
|
|
623
|
+
updateData[key] = data[key];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
let query;
|
|
627
|
+
if (caps.supportsOnConflict) {
|
|
628
|
+
query = db.insert(tableObj).values(data).onConflictDoUpdate({
|
|
629
|
+
target: conflictTarget,
|
|
630
|
+
set: updateData
|
|
631
|
+
});
|
|
632
|
+
} else {
|
|
633
|
+
query = db.insert(tableObj).values(data);
|
|
634
|
+
}
|
|
635
|
+
if (caps.supportsReturning) {
|
|
636
|
+
const result = await query.returning();
|
|
637
|
+
return Array.isArray(result) ? result[0] : result;
|
|
638
|
+
}
|
|
639
|
+
await query;
|
|
640
|
+
if (options.conflictColumns.length && data[options.conflictColumns[0]] !== void 0) {
|
|
641
|
+
return await this.selectOne(table, {
|
|
642
|
+
where: {
|
|
643
|
+
and: [
|
|
644
|
+
{
|
|
645
|
+
column: options.conflictColumns[0],
|
|
646
|
+
op: "=",
|
|
647
|
+
value: data[options.conflictColumns[0]]
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return data;
|
|
654
|
+
} catch (error) {
|
|
655
|
+
throw this.handleQueryError(error, "upsert", table);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
throw this.createDatabaseError(
|
|
659
|
+
"query",
|
|
660
|
+
`Table "${table}" not found in schema registry. Ensure setTableResolver() has been called during boot.`,
|
|
661
|
+
void 0
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
// ============================================================
|
|
665
|
+
// Migration Support (Default implementations, can override)
|
|
666
|
+
// ============================================================
|
|
667
|
+
/**
|
|
668
|
+
* Run pending migrations.
|
|
669
|
+
*
|
|
670
|
+
* @remarks
|
|
671
|
+
* Default implementation is a placeholder. Subclasses should implement
|
|
672
|
+
* migration tracking and execution logic.
|
|
673
|
+
*
|
|
674
|
+
* @param migrations - Array of migrations to run
|
|
675
|
+
* @returns Migration result with applied and pending migrations
|
|
676
|
+
*
|
|
677
|
+
* @throws {DatabaseError} If migration fails
|
|
678
|
+
*/
|
|
679
|
+
// Base implementation throws synchronously; dialect adapters override with async logic.
|
|
680
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
681
|
+
async migrate(_migrations) {
|
|
682
|
+
throw this.createDatabaseError(
|
|
683
|
+
"query",
|
|
684
|
+
"migrate() must be implemented by dialect-specific adapter. Use PostgresAdapter, MySqlAdapter, or SqliteAdapter.",
|
|
685
|
+
void 0
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Rollback the last migration.
|
|
690
|
+
*
|
|
691
|
+
* @remarks
|
|
692
|
+
* Default implementation is a placeholder. Subclasses should implement
|
|
693
|
+
* migration rollback logic.
|
|
694
|
+
*
|
|
695
|
+
* @returns Migration result after rollback
|
|
696
|
+
*
|
|
697
|
+
* @throws {DatabaseError} If rollback fails
|
|
698
|
+
*/
|
|
699
|
+
// Base implementation throws synchronously; dialect adapters override with async logic.
|
|
700
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
701
|
+
async rollback() {
|
|
702
|
+
throw this.createDatabaseError(
|
|
703
|
+
"query",
|
|
704
|
+
"rollback() must be implemented by dialect-specific adapter. Use PostgresAdapter, MySqlAdapter, or SqliteAdapter.",
|
|
705
|
+
void 0
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get migration status.
|
|
710
|
+
*
|
|
711
|
+
* @remarks
|
|
712
|
+
* Default implementation is a placeholder. Subclasses should implement
|
|
713
|
+
* migration status checking.
|
|
714
|
+
*
|
|
715
|
+
* @returns Current migration status
|
|
716
|
+
*
|
|
717
|
+
* @throws {DatabaseError} If status check fails
|
|
718
|
+
*/
|
|
719
|
+
async getMigrationStatus() {
|
|
720
|
+
try {
|
|
721
|
+
const rows = await this.executeQuery(`SELECT * FROM "__drizzle_migrations" ORDER BY created_at ASC`);
|
|
722
|
+
const applied = rows.map((r) => ({
|
|
723
|
+
id: String(r.id),
|
|
724
|
+
name: r.hash,
|
|
725
|
+
appliedAt: new Date(r.created_at),
|
|
726
|
+
checksum: r.hash
|
|
727
|
+
}));
|
|
728
|
+
return {
|
|
729
|
+
applied,
|
|
730
|
+
pending: [],
|
|
731
|
+
current: applied.length > 0 ? applied[applied.length - 1].id : null
|
|
732
|
+
};
|
|
733
|
+
} catch {
|
|
734
|
+
return { applied: [], pending: [], current: null };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// ============================================================
|
|
738
|
+
// Schema Operations (Default implementations, can override)
|
|
739
|
+
// ============================================================
|
|
740
|
+
/**
|
|
741
|
+
* Create a new table.
|
|
742
|
+
*
|
|
743
|
+
* @remarks
|
|
744
|
+
* Default implementation is a placeholder. Subclasses should implement
|
|
745
|
+
* table creation logic.
|
|
746
|
+
*
|
|
747
|
+
* @param definition - Table definition
|
|
748
|
+
* @param options - Creation options
|
|
749
|
+
*
|
|
750
|
+
* @throws {DatabaseError} If table creation fails
|
|
751
|
+
*/
|
|
752
|
+
async createTable(definition, options) {
|
|
753
|
+
const columnDefs = definition.columns.map((col) => {
|
|
754
|
+
let colSql = `${this.escapeIdentifier(col.name)} ${col.type.toUpperCase()}`;
|
|
755
|
+
if (col.primaryKey) colSql += " PRIMARY KEY";
|
|
756
|
+
if (col.nullable === false) colSql += " NOT NULL";
|
|
757
|
+
if (col.unique) colSql += " UNIQUE";
|
|
758
|
+
if (col.default !== void 0) {
|
|
759
|
+
colSql += ` DEFAULT ${renderDefaultValue(col.default, col.name)}`;
|
|
760
|
+
}
|
|
761
|
+
return colSql;
|
|
762
|
+
});
|
|
763
|
+
const ifNotExists = options?.ifNotExists !== false ? "IF NOT EXISTS " : "";
|
|
764
|
+
const query = `CREATE TABLE ${ifNotExists}${this.escapeIdentifier(definition.name)} (
|
|
765
|
+
${columnDefs.join(",\n ")}
|
|
766
|
+
)`;
|
|
767
|
+
try {
|
|
768
|
+
await this.executeQuery(query);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
throw this.handleQueryError(error, "createTable", definition.name);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Drop a table.
|
|
775
|
+
*
|
|
776
|
+
* @remarks
|
|
777
|
+
* Default implementation is a placeholder. Subclasses should implement
|
|
778
|
+
* table dropping logic.
|
|
779
|
+
*
|
|
780
|
+
* @param tableName - Name of table to drop
|
|
781
|
+
* @param options - Drop options
|
|
782
|
+
*
|
|
783
|
+
* @throws {DatabaseError} If table drop fails
|
|
784
|
+
*/
|
|
785
|
+
async dropTable(tableName, options) {
|
|
786
|
+
const ifExists = options?.ifExists !== false ? "IF EXISTS " : "";
|
|
787
|
+
const cascade = options?.cascade ? " CASCADE" : "";
|
|
788
|
+
const query = `DROP TABLE ${ifExists}${this.escapeIdentifier(tableName)}${cascade}`;
|
|
789
|
+
try {
|
|
790
|
+
await this.executeQuery(query);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
throw this.handleQueryError(error, "dropTable", tableName);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Alter an existing table.
|
|
797
|
+
*
|
|
798
|
+
* @remarks
|
|
799
|
+
* Default implementation is a placeholder. Subclasses should implement
|
|
800
|
+
* table alteration logic.
|
|
801
|
+
*
|
|
802
|
+
* @param tableName - Name of table to alter
|
|
803
|
+
* @param operations - Alteration operations
|
|
804
|
+
* @param options - Alter options
|
|
805
|
+
*
|
|
806
|
+
* @throws {DatabaseError} If table alteration fails
|
|
807
|
+
*/
|
|
808
|
+
async alterTable(tableName, operations, _options) {
|
|
809
|
+
const quotedTable = this.escapeIdentifier(tableName);
|
|
810
|
+
for (const op of operations) {
|
|
811
|
+
let query;
|
|
812
|
+
switch (op.kind) {
|
|
813
|
+
case "add_column": {
|
|
814
|
+
let colDef = `${this.escapeIdentifier(op.column.name)} ${op.column.type.toUpperCase()}`;
|
|
815
|
+
if (op.column.nullable === false) colDef += " NOT NULL";
|
|
816
|
+
if (op.column.unique) colDef += " UNIQUE";
|
|
817
|
+
if (op.column.default !== void 0) {
|
|
818
|
+
const defaultVal = typeof op.column.default === "object" && op.column.default !== null && "sql" in op.column.default ? op.column.default.sql : typeof op.column.default === "string" ? `'${op.column.default}'` : String(op.column.default);
|
|
819
|
+
colDef += ` DEFAULT ${defaultVal}`;
|
|
820
|
+
}
|
|
821
|
+
query = `ALTER TABLE ${quotedTable} ADD COLUMN ${colDef}`;
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
case "drop_column": {
|
|
825
|
+
const cascade = op.cascade ? " CASCADE" : "";
|
|
826
|
+
query = `ALTER TABLE ${quotedTable} DROP COLUMN ${this.escapeIdentifier(op.columnName)}${cascade}`;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
case "rename_column":
|
|
830
|
+
query = `ALTER TABLE ${quotedTable} RENAME COLUMN ${this.escapeIdentifier(op.from)} TO ${this.escapeIdentifier(op.to)}`;
|
|
831
|
+
break;
|
|
832
|
+
case "modify_column": {
|
|
833
|
+
query = `ALTER TABLE ${quotedTable} ALTER COLUMN ${this.escapeIdentifier(op.column.name)} TYPE ${op.column.type.toUpperCase()}`;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "add_constraint":
|
|
837
|
+
{
|
|
838
|
+
const constraintCols = op.constraint.columns ?? [];
|
|
839
|
+
const colList = constraintCols.map((c) => this.escapeIdentifier(c)).join(", ");
|
|
840
|
+
if (op.constraint.type === "check" && op.constraint.expression) {
|
|
841
|
+
query = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${this.escapeIdentifier(op.constraint.name)} CHECK (${op.constraint.expression})`;
|
|
842
|
+
} else {
|
|
843
|
+
query = `ALTER TABLE ${quotedTable} ADD CONSTRAINT ${this.escapeIdentifier(op.constraint.name)} ${op.constraint.type.toUpperCase()} (${colList})`;
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
case "drop_constraint": {
|
|
848
|
+
const cascadeConstraint = op.cascade ? " CASCADE" : "";
|
|
849
|
+
query = `ALTER TABLE ${quotedTable} DROP CONSTRAINT ${this.escapeIdentifier(op.constraintName)}${cascadeConstraint}`;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
default:
|
|
853
|
+
throw this.createDatabaseError(
|
|
854
|
+
"query",
|
|
855
|
+
`Unsupported alter table operation: ${op.kind}`,
|
|
856
|
+
void 0
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
await this.executeQuery(query);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
throw this.handleQueryError(error, "alterTable", tableName);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Check if a table exists in the database.
|
|
868
|
+
*
|
|
869
|
+
* @remarks
|
|
870
|
+
* Uses dialect-specific information schema queries to check table existence.
|
|
871
|
+
* This is useful for development mode auto-sync to determine whether to
|
|
872
|
+
* CREATE or DROP/CREATE tables.
|
|
873
|
+
*
|
|
874
|
+
* @param tableName - Name of table to check
|
|
875
|
+
* @param schema - Optional schema name (defaults to 'public' for PostgreSQL)
|
|
876
|
+
* @returns True if table exists, false otherwise
|
|
877
|
+
*
|
|
878
|
+
* @throws {DatabaseError} If query fails
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* ```typescript
|
|
882
|
+
* const exists = await adapter.tableExists('users');
|
|
883
|
+
* if (exists) {
|
|
884
|
+
* await adapter.dropTable('users');
|
|
885
|
+
* }
|
|
886
|
+
* await adapter.createTable(userTableDef);
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
async tableExists(tableName, schema) {
|
|
890
|
+
try {
|
|
891
|
+
let sql;
|
|
892
|
+
const params = [];
|
|
893
|
+
switch (this.dialect) {
|
|
894
|
+
case "postgresql":
|
|
895
|
+
sql = `
|
|
896
|
+
SELECT EXISTS (
|
|
897
|
+
SELECT FROM information_schema.tables
|
|
898
|
+
WHERE table_schema = $1
|
|
899
|
+
AND table_name = $2
|
|
900
|
+
) as exists
|
|
901
|
+
`;
|
|
902
|
+
params.push(schema ?? "public", tableName);
|
|
903
|
+
break;
|
|
904
|
+
case "mysql":
|
|
905
|
+
sql = `
|
|
906
|
+
SELECT COUNT(*) as count
|
|
907
|
+
FROM information_schema.tables
|
|
908
|
+
WHERE table_schema = DATABASE()
|
|
909
|
+
AND table_name = ?
|
|
910
|
+
`;
|
|
911
|
+
params.push(tableName);
|
|
912
|
+
break;
|
|
913
|
+
case "sqlite":
|
|
914
|
+
sql = `
|
|
915
|
+
SELECT COUNT(*) as count
|
|
916
|
+
FROM sqlite_master
|
|
917
|
+
WHERE type = 'table'
|
|
918
|
+
AND name = ?
|
|
919
|
+
`;
|
|
920
|
+
params.push(tableName);
|
|
921
|
+
break;
|
|
922
|
+
default:
|
|
923
|
+
throw this.createDatabaseError(
|
|
924
|
+
"query",
|
|
925
|
+
`tableExists not implemented for dialect: ${String(this.dialect)}`,
|
|
926
|
+
void 0
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const results = await this.executeQuery(
|
|
930
|
+
sql,
|
|
931
|
+
params
|
|
932
|
+
);
|
|
933
|
+
if (results.length === 0) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const row = results[0];
|
|
937
|
+
if ("exists" in row) {
|
|
938
|
+
return row.exists === true || row.exists === "t" || row.exists === 1;
|
|
939
|
+
}
|
|
940
|
+
if ("count" in row) {
|
|
941
|
+
return Number(row.count) > 0;
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
throw this.handleQueryError(error, "tableExists", tableName);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Get list of all tables in the database.
|
|
950
|
+
*
|
|
951
|
+
* @remarks
|
|
952
|
+
* Uses dialect-specific information schema queries to list tables.
|
|
953
|
+
* Useful for detecting orphaned tables or validating schema state.
|
|
954
|
+
*
|
|
955
|
+
* @param schema - Optional schema name (defaults to 'public' for PostgreSQL)
|
|
956
|
+
* @returns Array of table names
|
|
957
|
+
*
|
|
958
|
+
* @throws {DatabaseError} If query fails
|
|
959
|
+
*/
|
|
960
|
+
async listTables(schema) {
|
|
961
|
+
try {
|
|
962
|
+
let sql;
|
|
963
|
+
const params = [];
|
|
964
|
+
switch (this.dialect) {
|
|
965
|
+
case "postgresql":
|
|
966
|
+
sql = `
|
|
967
|
+
SELECT table_name
|
|
968
|
+
FROM information_schema.tables
|
|
969
|
+
WHERE table_schema = $1
|
|
970
|
+
AND table_type = 'BASE TABLE'
|
|
971
|
+
ORDER BY table_name
|
|
972
|
+
`;
|
|
973
|
+
params.push(schema ?? "public");
|
|
974
|
+
break;
|
|
975
|
+
case "mysql":
|
|
976
|
+
sql = `
|
|
977
|
+
SELECT table_name
|
|
978
|
+
FROM information_schema.tables
|
|
979
|
+
WHERE table_schema = DATABASE()
|
|
980
|
+
AND table_type = 'BASE TABLE'
|
|
981
|
+
ORDER BY table_name
|
|
982
|
+
`;
|
|
983
|
+
break;
|
|
984
|
+
case "sqlite":
|
|
985
|
+
sql = `
|
|
986
|
+
SELECT name as table_name
|
|
987
|
+
FROM sqlite_master
|
|
988
|
+
WHERE type = 'table'
|
|
989
|
+
AND name NOT LIKE 'sqlite_%'
|
|
990
|
+
ORDER BY name
|
|
991
|
+
`;
|
|
992
|
+
break;
|
|
993
|
+
default:
|
|
994
|
+
throw this.createDatabaseError(
|
|
995
|
+
"query",
|
|
996
|
+
`listTables not implemented for dialect: ${String(this.dialect)}`,
|
|
997
|
+
void 0
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const results = await this.executeQuery(
|
|
1001
|
+
sql,
|
|
1002
|
+
params
|
|
1003
|
+
);
|
|
1004
|
+
return results.map((row) => row.table_name);
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
throw this.handleQueryError(error, "listTables", "");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
// ============================================================
|
|
1010
|
+
// Protected Utilities
|
|
1011
|
+
// ============================================================
|
|
1012
|
+
// Old raw SQL query builders (buildSelectQuery, buildInsertQuery, buildUpdateQuery,
|
|
1013
|
+
// buildDeleteQuery, buildUpsertQuery, buildWhereClause, buildPlaceholder,
|
|
1014
|
+
// buildPlaceholders) have been removed. CRUD methods now use Drizzle query API
|
|
1015
|
+
// via the TableResolver set during boot. See drizzle-where.ts for where clause
|
|
1016
|
+
// translation.
|
|
1017
|
+
// NOTE: The following content up to escapeIdentifier has been removed.
|
|
1018
|
+
// If you're looking for the old query builder methods, see git history
|
|
1019
|
+
// (commit before "refactor: remove dead SQL builder code").
|
|
1020
|
+
/**
|
|
1021
|
+
* Escape a table or column identifier.
|
|
1022
|
+
*
|
|
1023
|
+
* @remarks
|
|
1024
|
+
* Default uses double quotes (SQL standard).
|
|
1025
|
+
* MySQL adapter should override to use backticks.
|
|
1026
|
+
*
|
|
1027
|
+
* @param identifier - Identifier to escape
|
|
1028
|
+
* @returns Escaped identifier
|
|
1029
|
+
*
|
|
1030
|
+
* @protected
|
|
1031
|
+
*/
|
|
1032
|
+
escapeIdentifier(identifier) {
|
|
1033
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Create a DatabaseError with proper error kind classification.
|
|
1037
|
+
*
|
|
1038
|
+
* @remarks
|
|
1039
|
+
* Protected helper for subclasses to create consistent errors.
|
|
1040
|
+
* Subclasses can override to add dialect-specific error classification.
|
|
1041
|
+
*
|
|
1042
|
+
* @param kind - Error kind
|
|
1043
|
+
* @param message - Error message
|
|
1044
|
+
* @param cause - Original error
|
|
1045
|
+
* @returns DatabaseError instance
|
|
1046
|
+
*
|
|
1047
|
+
* @protected
|
|
1048
|
+
*/
|
|
1049
|
+
createDatabaseError(kind, message, cause) {
|
|
1050
|
+
return createDatabaseError({
|
|
1051
|
+
kind,
|
|
1052
|
+
message,
|
|
1053
|
+
cause
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Handle query errors and convert to DatabaseError.
|
|
1058
|
+
*
|
|
1059
|
+
* @remarks
|
|
1060
|
+
* Protected helper for consistent error handling across CRUD operations.
|
|
1061
|
+
* Subclasses can override to add dialect-specific error classification.
|
|
1062
|
+
*
|
|
1063
|
+
* @param error - Original error
|
|
1064
|
+
* @param operation - Operation that failed
|
|
1065
|
+
* @param table - Table name
|
|
1066
|
+
* @returns DatabaseError instance
|
|
1067
|
+
*
|
|
1068
|
+
* @protected
|
|
1069
|
+
*/
|
|
1070
|
+
handleQueryError(error, operation, table) {
|
|
1071
|
+
if (isDatabaseError(error)) {
|
|
1072
|
+
return error;
|
|
1073
|
+
}
|
|
1074
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1075
|
+
return this.createDatabaseError(
|
|
1076
|
+
"query",
|
|
1077
|
+
`${operation} operation failed on table '${table}': ${errorMessage}`,
|
|
1078
|
+
error instanceof Error ? error : void 0
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
var ALLOWED_DEFAULT_SQL_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
1083
|
+
"current_timestamp",
|
|
1084
|
+
"now()",
|
|
1085
|
+
"gen_random_uuid()",
|
|
1086
|
+
"uuid_generate_v4()",
|
|
1087
|
+
"current_date",
|
|
1088
|
+
"current_time"
|
|
1089
|
+
]);
|
|
1090
|
+
function renderDefaultValue(value, columnName) {
|
|
1091
|
+
if (value === null) return "NULL";
|
|
1092
|
+
if (typeof value === "number") {
|
|
1093
|
+
if (!Number.isFinite(value)) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`DEFAULT for column "${columnName}" must be a finite number (got ${value}).`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return String(value);
|
|
1099
|
+
}
|
|
1100
|
+
if (typeof value === "boolean") {
|
|
1101
|
+
return value ? "TRUE" : "FALSE";
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof value === "string") {
|
|
1104
|
+
if (/[;\\\n\r\0]/.test(value)) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`DEFAULT string for column "${columnName}" contains characters that are not allowed in DDL (no semicolons, backslashes, control chars, or null bytes).`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1110
|
+
}
|
|
1111
|
+
if (typeof value === "object" && value !== null && "sql" in value) {
|
|
1112
|
+
const raw = value.sql;
|
|
1113
|
+
if (typeof raw !== "string") {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
`DEFAULT for column "${columnName}" has a { sql } expression that is not a string.`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
const normalized = raw.trim().toLowerCase();
|
|
1119
|
+
if (!ALLOWED_DEFAULT_SQL_EXPRESSIONS.has(normalized)) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`DEFAULT for column "${columnName}" uses a SQL expression "${raw}" that is not on the allowlist. Allowed: ${[...ALLOWED_DEFAULT_SQL_EXPRESSIONS].join(", ")}.`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
return raw.trim();
|
|
1125
|
+
}
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`DEFAULT for column "${columnName}" has an unsupported type (${typeof value}).`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/index.ts
|
|
1132
|
+
var version = "0.1.0";
|
|
1133
|
+
|
|
1134
|
+
exports.DrizzleAdapter = DrizzleAdapter;
|
|
1135
|
+
exports.version = version;
|
|
1136
|
+
//# sourceMappingURL=index.cjs.map
|
|
1137
|
+
//# sourceMappingURL=index.cjs.map
|