@nextlyhq/adapter-mysql 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/dist/index.cjs ADDED
@@ -0,0 +1,612 @@
1
+ 'use strict';
2
+
3
+ var adapterDrizzle = require('@nextlyhq/adapter-drizzle');
4
+ var types = require('@nextlyhq/adapter-drizzle/types');
5
+ var versionCheck = require('@nextlyhq/adapter-drizzle/version-check');
6
+ var mysql2 = require('drizzle-orm/mysql2');
7
+ var mysql = require('mysql2/promise');
8
+
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var mysql__default = /*#__PURE__*/_interopDefault(mysql);
12
+
13
+ // src/index.ts
14
+ var VERSION = "0.1.0";
15
+ var DEFAULT_POOL_CONFIG = {
16
+ max: 10,
17
+ idleTimeoutMs: 3e4,
18
+ connectionTimeoutMs: 1e4
19
+ };
20
+ var MYSQL_ERROR_CODES = {
21
+ // Unique/Duplicate key violations
22
+ 1022: "unique_violation",
23
+ // ER_DUP_KEY
24
+ 1062: "unique_violation",
25
+ // ER_DUP_ENTRY
26
+ 1169: "unique_violation",
27
+ // ER_DUP_UNIQUE
28
+ 1586: "unique_violation",
29
+ // ER_DUP_ENTRY_WITH_KEY_NAME
30
+ // Foreign key violations
31
+ 1216: "foreign_key_violation",
32
+ // ER_NO_REFERENCED_ROW
33
+ 1217: "foreign_key_violation",
34
+ // ER_ROW_IS_REFERENCED
35
+ 1451: "foreign_key_violation",
36
+ // ER_ROW_IS_REFERENCED_2
37
+ 1452: "foreign_key_violation",
38
+ // ER_NO_REFERENCED_ROW_2
39
+ // Not null violations
40
+ 1048: "not_null_violation",
41
+ // ER_BAD_NULL_ERROR
42
+ 1364: "not_null_violation",
43
+ // ER_NO_DEFAULT_FOR_FIELD
44
+ // Check constraint violations (MySQL 8.0.16+)
45
+ 3819: "check_violation",
46
+ // ER_CHECK_CONSTRAINT_VIOLATED
47
+ // Deadlock
48
+ 1213: "deadlock",
49
+ // ER_LOCK_DEADLOCK
50
+ // Timeout
51
+ 1205: "timeout",
52
+ // ER_LOCK_WAIT_TIMEOUT
53
+ // Connection errors
54
+ 1040: "connection",
55
+ // ER_CON_COUNT_ERROR - Too many connections
56
+ 1042: "connection",
57
+ // ER_BAD_HOST_ERROR
58
+ 1043: "connection",
59
+ // ER_HANDSHAKE_ERROR
60
+ 1044: "connection",
61
+ // ER_DBACCESS_DENIED_ERROR
62
+ 1045: "connection",
63
+ // ER_ACCESS_DENIED_ERROR
64
+ 1129: "connection",
65
+ // ER_HOST_IS_BLOCKED
66
+ 1130: "connection",
67
+ // ER_HOST_NOT_PRIVILEGED
68
+ 2002: "connection",
69
+ // CR_CONNECTION_ERROR
70
+ 2003: "connection",
71
+ // CR_CONN_HOST_ERROR
72
+ 2006: "connection",
73
+ // CR_SERVER_GONE_ERROR
74
+ 2013: "connection",
75
+ // CR_SERVER_LOST
76
+ // Query errors
77
+ 1064: "query",
78
+ // ER_PARSE_ERROR
79
+ 1146: "query",
80
+ // ER_NO_SUCH_TABLE
81
+ 1054: "query"
82
+ // ER_BAD_FIELD_ERROR
83
+ };
84
+ function delay(ms) {
85
+ return new Promise((resolve) => setTimeout(resolve, ms));
86
+ }
87
+ var MySqlAdapter = class extends adapterDrizzle.DrizzleAdapter {
88
+ /**
89
+ * The database dialect - always 'mysql' for this adapter.
90
+ */
91
+ dialect = "mysql";
92
+ /**
93
+ * Adapter configuration.
94
+ */
95
+ config;
96
+ /**
97
+ * Connection pool instance.
98
+ */
99
+ pool = null;
100
+ /**
101
+ * Connection state flag.
102
+ */
103
+ connected = false;
104
+ /**
105
+ * Creates a new MySQL adapter instance.
106
+ *
107
+ * @param config - Adapter configuration
108
+ */
109
+ constructor(config) {
110
+ super();
111
+ this.config = config;
112
+ }
113
+ /**
114
+ * Connect to the MySQL database.
115
+ * Creates a connection pool using mysql2.
116
+ *
117
+ * @remarks
118
+ * This method initializes the connection pool and verifies connectivity
119
+ * by executing a simple query. It is idempotent - calling it multiple
120
+ * times will not create multiple pools.
121
+ *
122
+ * @throws {DatabaseError} If connection fails
123
+ */
124
+ async connect() {
125
+ if (this.connected && this.pool) {
126
+ return;
127
+ }
128
+ try {
129
+ const poolConfig = this.buildPoolConfig();
130
+ this.pool = mysql__default.default.createPool(poolConfig);
131
+ const connection = await this.pool.getConnection();
132
+ try {
133
+ await connection.query("SELECT 1");
134
+ await versionCheck.checkDialectVersion(connection, "mysql", {
135
+ // Why: route variant warnings through the adapter's logger so
136
+ // users see a single, consistent log surface.
137
+ onWarning: (msg) => this.config.logger?.warn?.(msg)
138
+ });
139
+ this.connected = true;
140
+ if (this.config.logger?.info) {
141
+ this.config.logger.info("MySQL connection established", {
142
+ host: this.config.host ?? "from URL",
143
+ database: this.config.database ?? "from URL"
144
+ });
145
+ }
146
+ } finally {
147
+ connection.release();
148
+ }
149
+ } catch (error) {
150
+ if (this.pool) {
151
+ await this.pool.end().catch(() => {
152
+ });
153
+ this.pool = null;
154
+ }
155
+ throw this.classifyError(error);
156
+ }
157
+ }
158
+ /**
159
+ * Disconnect from the MySQL database.
160
+ * Gracefully closes the connection pool.
161
+ *
162
+ * @remarks
163
+ * This method is idempotent - calling it multiple times is safe.
164
+ * It waits for all connections to be released before shutting down.
165
+ */
166
+ async disconnect() {
167
+ if (!this.pool) {
168
+ return;
169
+ }
170
+ try {
171
+ await this.pool.end();
172
+ if (this.config.logger?.info) {
173
+ this.config.logger.info("MySQL connection closed");
174
+ }
175
+ } finally {
176
+ this.pool = null;
177
+ this.connected = false;
178
+ }
179
+ }
180
+ /**
181
+ * Check if connected to the database.
182
+ */
183
+ isConnected() {
184
+ return this.connected && this.pool !== null;
185
+ }
186
+ /**
187
+ * Get connection pool statistics.
188
+ * Returns null if not connected.
189
+ *
190
+ * @remarks
191
+ * MySQL2 pool exposes different stats than pg:
192
+ * - _allConnections: all connections
193
+ * - _freeConnections: idle connections
194
+ * - _connectionQueue: waiting requests
195
+ */
196
+ getPoolStats() {
197
+ if (!this.pool) {
198
+ return null;
199
+ }
200
+ const poolInternal = this.pool;
201
+ const internal = poolInternal.pool;
202
+ if (!internal) {
203
+ return {
204
+ total: 0,
205
+ idle: 0,
206
+ waiting: 0,
207
+ active: 0
208
+ };
209
+ }
210
+ const total = internal._allConnections?.length ?? 0;
211
+ const idle = internal._freeConnections?.length ?? 0;
212
+ const waiting = internal._connectionQueue?.length ?? 0;
213
+ return {
214
+ total,
215
+ idle,
216
+ waiting,
217
+ active: total - idle
218
+ };
219
+ }
220
+ /**
221
+ * Execute a raw SQL query.
222
+ *
223
+ * @param sql - SQL query string with ? placeholders
224
+ * @param params - Query parameters
225
+ * @returns Query results
226
+ *
227
+ * @throws {DatabaseError} If query execution fails
228
+ */
229
+ async executeQuery(sql, params = []) {
230
+ const pool = this.ensurePool();
231
+ const startTime = Date.now();
232
+ try {
233
+ const [rows] = await pool.query(
234
+ sql,
235
+ params
236
+ );
237
+ if (this.config.logger?.query) {
238
+ const durationMs = Date.now() - startTime;
239
+ this.config.logger.query(sql, params, durationMs);
240
+ }
241
+ return rows;
242
+ } catch (error) {
243
+ throw this.classifyError(error, sql);
244
+ }
245
+ }
246
+ /**
247
+ * Execute work within a transaction.
248
+ *
249
+ * @param work - Function containing transactional operations
250
+ * @param options - Transaction options (isolation level, timeout, retry)
251
+ * @returns Result of the work function
252
+ *
253
+ * @remarks
254
+ * MySQL transactions support isolation levels. Automatic retry is
255
+ * implemented for deadlocks (error 1213) when `retryCount` is specified.
256
+ *
257
+ * Note: Savepoints are disabled in this adapter for safety due to
258
+ * MySQL's quirks with nested transactions.
259
+ */
260
+ async transaction(work, options) {
261
+ const pool = this.ensurePool();
262
+ const maxAttempts = (options?.retryCount ?? 0) + 1;
263
+ const retryDelayMs = options?.retryDelayMs ?? 100;
264
+ let lastError;
265
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
266
+ const connection = await pool.getConnection();
267
+ const startTime = Date.now();
268
+ try {
269
+ await this.beginTransaction(connection, options);
270
+ const ctx = this.createTransactionContext(connection);
271
+ const result = await work(ctx);
272
+ await connection.query("COMMIT");
273
+ if (this.config.logger?.debug) {
274
+ const durationMs = Date.now() - startTime;
275
+ this.config.logger.debug("Transaction committed", {
276
+ attempt,
277
+ durationMs
278
+ });
279
+ }
280
+ return result;
281
+ } catch (error) {
282
+ await connection.query("ROLLBACK").catch(() => {
283
+ });
284
+ lastError = error;
285
+ const mysqlError = error;
286
+ const isRetryable = mysqlError.errno === 1213;
287
+ if (isRetryable && attempt < maxAttempts) {
288
+ if (this.config.logger?.warn) {
289
+ this.config.logger.warn(
290
+ `Transaction failed with deadlock, retrying (${attempt}/${maxAttempts})`,
291
+ { errno: mysqlError.errno, attempt }
292
+ );
293
+ }
294
+ await delay(retryDelayMs * attempt);
295
+ continue;
296
+ }
297
+ throw this.classifyError(error);
298
+ } finally {
299
+ connection.release();
300
+ }
301
+ }
302
+ throw this.classifyError(lastError);
303
+ }
304
+ /**
305
+ * Get MySQL database capabilities.
306
+ *
307
+ * @remarks
308
+ * MySQL has some limitations:
309
+ * - No JSONB (uses JSON)
310
+ * - No arrays
311
+ * - No native ILIKE
312
+ * - No RETURNING clause
313
+ * - Savepoints disabled for safety
314
+ */
315
+ getCapabilities() {
316
+ return {
317
+ dialect: "mysql",
318
+ supportsJsonb: false,
319
+ // MySQL uses JSON, not JSONB
320
+ supportsJson: true,
321
+ supportsArrays: false,
322
+ // MySQL doesn't support array types
323
+ supportsGeneratedColumns: true,
324
+ // MySQL 5.7.6+
325
+ supportsFts: true,
326
+ // MySQL has FULLTEXT indexes
327
+ supportsIlike: false,
328
+ // No native ILIKE, use LOWER() LIKE
329
+ supportsReturning: false,
330
+ // No RETURNING clause in MySQL
331
+ supportsSavepoints: false,
332
+ // Disabled for safety per approved approach
333
+ supportsOnConflict: true,
334
+ // ON DUPLICATE KEY UPDATE
335
+ maxParamsPerQuery: 65535,
336
+ // MySQL limit
337
+ maxIdentifierLength: 64
338
+ // MySQL limit
339
+ };
340
+ }
341
+ /**
342
+ * Build a placeholder for MySQL (uses ? instead of $1, $2, etc.)
343
+ *
344
+ * @param _index - Parameter index (ignored for MySQL)
345
+ * @returns The ? placeholder
346
+ */
347
+ buildPlaceholder(_index) {
348
+ return "?";
349
+ }
350
+ /**
351
+ * Build multiple placeholders for MySQL.
352
+ *
353
+ * @param count - Number of placeholders needed
354
+ * @param _startIndex - Starting index (ignored for MySQL)
355
+ * @returns Comma-separated ? placeholders
356
+ */
357
+ buildPlaceholders(count, _startIndex = 0) {
358
+ return Array(count).fill("?").join(", ");
359
+ }
360
+ /**
361
+ * Escape an identifier for MySQL (uses backticks instead of double quotes).
362
+ *
363
+ * @param identifier - The identifier to escape
364
+ * @returns Escaped identifier with backticks
365
+ */
366
+ escapeIdentifier(identifier) {
367
+ return `\`${identifier.replace(/`/g, "``")}\``;
368
+ }
369
+ // ============================================================
370
+ // Protected Helper Methods
371
+ // ============================================================
372
+ /**
373
+ * Ensures pool is connected and returns it.
374
+ *
375
+ * @throws {DatabaseError} If not connected
376
+ */
377
+ ensurePool() {
378
+ if (!this.pool) {
379
+ throw types.createDatabaseError({
380
+ kind: "connection",
381
+ message: "MySqlAdapter is not connected. Call connect() first."
382
+ });
383
+ }
384
+ return this.pool;
385
+ }
386
+ /**
387
+ * Return the typed Drizzle instance for MySQL.
388
+ * Guarded for server-only usage and requires an active connection.
389
+ *
390
+ * @param schema - Optional schema for relational queries (db.query.*)
391
+ * @returns Drizzle ORM instance wrapping the mysql2 pool connection
392
+ * @throws {Error} If called in browser or not connected
393
+ */
394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
395
+ getDrizzle(schema) {
396
+ if (typeof window !== "undefined") {
397
+ throw new Error("getDrizzle() is server-only");
398
+ }
399
+ const pool = this.ensurePool();
400
+ return schema ? mysql2.drizzle({ client: pool, schema, mode: "default" }) : mysql2.drizzle(pool);
401
+ }
402
+ /**
403
+ * Builds mysql2 Pool configuration from adapter config.
404
+ */
405
+ buildPoolConfig() {
406
+ const config = {};
407
+ if (this.config.url) {
408
+ config.uri = this.config.url;
409
+ } else {
410
+ if (this.config.host) config.host = this.config.host;
411
+ if (this.config.port) config.port = this.config.port;
412
+ if (this.config.database) config.database = this.config.database;
413
+ if (this.config.user) config.user = this.config.user;
414
+ if (this.config.password) config.password = this.config.password;
415
+ }
416
+ config.connectionLimit = this.config.pool?.max ?? DEFAULT_POOL_CONFIG.max;
417
+ config.idleTimeout = this.config.pool?.idleTimeoutMs ?? DEFAULT_POOL_CONFIG.idleTimeoutMs;
418
+ config.connectTimeout = this.config.pool?.connectionTimeoutMs ?? DEFAULT_POOL_CONFIG.connectionTimeoutMs;
419
+ config.waitForConnections = true;
420
+ config.queueLimit = 0;
421
+ if (this.config.ssl) {
422
+ if (typeof this.config.ssl === "boolean") {
423
+ config.ssl = this.config.ssl ? {} : void 0;
424
+ } else {
425
+ config.ssl = {
426
+ rejectUnauthorized: this.config.ssl.rejectUnauthorized,
427
+ ca: this.config.ssl.ca,
428
+ cert: this.config.ssl.cert,
429
+ key: this.config.ssl.key
430
+ };
431
+ }
432
+ }
433
+ if (this.config.timezone) {
434
+ config.timezone = this.config.timezone;
435
+ }
436
+ if (this.config.charset) {
437
+ config.charset = this.config.charset;
438
+ }
439
+ config.multipleStatements = false;
440
+ config.dateStrings = false;
441
+ return config;
442
+ }
443
+ /**
444
+ * Begins a transaction with the specified options.
445
+ */
446
+ async beginTransaction(connection, options) {
447
+ if (options?.isolationLevel) {
448
+ const isolationMap = {
449
+ "read uncommitted": "READ UNCOMMITTED",
450
+ "read committed": "READ COMMITTED",
451
+ "repeatable read": "REPEATABLE READ",
452
+ serializable: "SERIALIZABLE"
453
+ };
454
+ const level = isolationMap[options.isolationLevel];
455
+ if (level) {
456
+ await connection.query(`SET TRANSACTION ISOLATION LEVEL ${level}`);
457
+ }
458
+ }
459
+ if (options?.readOnly) {
460
+ await connection.query("SET TRANSACTION READ ONLY");
461
+ }
462
+ await connection.query("START TRANSACTION");
463
+ if (options?.timeoutMs) {
464
+ const timeoutSeconds = Math.ceil(options.timeoutMs / 1e3);
465
+ await connection.query(
466
+ `SET SESSION innodb_lock_wait_timeout = ${timeoutSeconds}`
467
+ );
468
+ }
469
+ }
470
+ /**
471
+ * Creates a TransactionContext for the given connection.
472
+ *
473
+ * @remarks
474
+ * Note: Savepoint methods are not implemented (set to undefined)
475
+ * as savepoints are disabled in this adapter per approved approach.
476
+ */
477
+ createTransactionContext(connection) {
478
+ return {
479
+ execute: async (sql, params = []) => {
480
+ const [rows] = await connection.query(
481
+ sql,
482
+ params
483
+ );
484
+ return rows;
485
+ },
486
+ insert: async (table, data, _options) => {
487
+ const columns = Object.keys(data);
488
+ const values = Object.values(data);
489
+ const placeholders = this.buildPlaceholders(values.length, 0);
490
+ const sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
491
+ const [result] = await connection.query(sql, values);
492
+ if (result.insertId) {
493
+ const [rows2] = await connection.query(
494
+ `SELECT * FROM ${this.escapeIdentifier(table)} WHERE id = ?`,
495
+ [result.insertId]
496
+ );
497
+ return rows2[0];
498
+ }
499
+ const whereClauses = columns.map(
500
+ (c) => `${this.escapeIdentifier(c)} = ?`
501
+ );
502
+ const [rows] = await connection.query(
503
+ `SELECT * FROM ${this.escapeIdentifier(table)} WHERE ${whereClauses.join(" AND ")} LIMIT 1`,
504
+ values
505
+ );
506
+ return rows[0];
507
+ },
508
+ insertMany: async (table, data, _options) => {
509
+ if (data.length === 0) return [];
510
+ const columns = Object.keys(data[0]);
511
+ const allValues = [];
512
+ const valuesClauses = [];
513
+ for (const record of data) {
514
+ const placeholders = [];
515
+ for (const col of columns) {
516
+ allValues.push(record[col]);
517
+ placeholders.push("?");
518
+ }
519
+ valuesClauses.push(`(${placeholders.join(", ")})`);
520
+ }
521
+ const sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
522
+ const [result] = await connection.query(
523
+ sql,
524
+ allValues
525
+ );
526
+ if (result.insertId && result.affectedRows > 0) {
527
+ const ids = [];
528
+ for (let i = 0; i < result.affectedRows; i++) {
529
+ ids.push(result.insertId + i);
530
+ }
531
+ const placeholders = ids.map(() => "?").join(", ");
532
+ const [rows] = await connection.query(
533
+ `SELECT * FROM ${this.escapeIdentifier(table)} WHERE id IN (${placeholders})`,
534
+ ids
535
+ );
536
+ return rows;
537
+ }
538
+ return [];
539
+ },
540
+ // TransactionContext CRUD methods delegate to the adapter's CRUD
541
+ // which uses Drizzle query API via the TableResolver.
542
+ select: async (table, options) => {
543
+ return this.select(table, options);
544
+ },
545
+ selectOne: async (table, options) => {
546
+ return this.selectOne(table, options);
547
+ },
548
+ update: async (table, data, where, options) => {
549
+ return this.update(table, data, where, options);
550
+ },
551
+ delete: async (table, where, _options) => {
552
+ return this.delete(table, where);
553
+ },
554
+ upsert: async (table, data, options) => {
555
+ return this.upsert(table, data, options);
556
+ },
557
+ // Savepoints disabled per approved approach
558
+ savepoint: void 0,
559
+ rollbackToSavepoint: void 0,
560
+ releaseSavepoint: void 0
561
+ };
562
+ }
563
+ /**
564
+ * Classifies a MySQL error into a DatabaseError.
565
+ *
566
+ * @param error - Original error from mysql2
567
+ * @param sql - SQL statement that caused the error (optional)
568
+ * @returns DatabaseError with proper classification
569
+ */
570
+ classifyError(error, sql) {
571
+ if (types.isDatabaseError(error)) return error;
572
+ const mysqlError = error;
573
+ const kind = mysqlError.errno && MYSQL_ERROR_CODES[mysqlError.errno] || "unknown";
574
+ let message = mysqlError.message ?? String(error);
575
+ if (sql && kind === "query") {
576
+ message = `Query failed: ${message}`;
577
+ }
578
+ return types.createDatabaseError({
579
+ kind,
580
+ message,
581
+ code: mysqlError.code ?? mysqlError.errno?.toString(),
582
+ detail: mysqlError.sql,
583
+ cause: error instanceof Error ? error : void 0
584
+ });
585
+ }
586
+ /**
587
+ * Override handleQueryError to use MySQL-specific classification.
588
+ */
589
+ handleQueryError(error, operation, table) {
590
+ const dbError = this.classifyError(error);
591
+ if (!dbError.message.includes(operation)) {
592
+ dbError.message = `${operation} operation failed on table '${table}': ${dbError.message}`;
593
+ }
594
+ if (!dbError.table) {
595
+ dbError.table = table;
596
+ }
597
+ return dbError;
598
+ }
599
+ };
600
+ function createMySqlAdapter(config) {
601
+ return new MySqlAdapter(config);
602
+ }
603
+ function isMySqlAdapter(value) {
604
+ return value instanceof MySqlAdapter;
605
+ }
606
+
607
+ exports.MySqlAdapter = MySqlAdapter;
608
+ exports.VERSION = VERSION;
609
+ exports.createMySqlAdapter = createMySqlAdapter;
610
+ exports.isMySqlAdapter = isMySqlAdapter;
611
+ //# sourceMappingURL=index.cjs.map
612
+ //# sourceMappingURL=index.cjs.map