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