@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.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