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