@nextlyhq/adapter-mysql 0.0.2-alpha.73 → 0.0.2-alpha.75
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 +167 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -3
- package/dist/index.d.ts +54 -3
- package/dist/index.mjs +168 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -168,6 +168,16 @@ async function returnTransactionConnection(connection, rollbackFailed, restore)
|
|
|
168
168
|
else connection.destroy();
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
+
* mysql2 accepts only `local`, `Z` and `±HH:MM` spellings for its driver
|
|
172
|
+
* zone, and the ones that resolve to UTC are exactly these. `local` resolves
|
|
173
|
+
* to the Node process's own zone, so it agrees with a UTC session only on a
|
|
174
|
+
* UTC host.
|
|
175
|
+
*/
|
|
176
|
+
function isUtcDriverZone(zone) {
|
|
177
|
+
if (zone === "local") return (/* @__PURE__ */ new Date()).getTimezoneOffset() === 0;
|
|
178
|
+
return zone === "Z" || zone === "+00:00" || zone === "-00:00";
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
171
181
|
* Package version
|
|
172
182
|
*/
|
|
173
183
|
const VERSION = "0.1.0";
|
|
@@ -214,11 +224,19 @@ const MYSQL_ERROR_CODES = {
|
|
|
214
224
|
1146: "query",
|
|
215
225
|
1054: "query"
|
|
216
226
|
};
|
|
227
|
+
/** ER_LOCK_DEADLOCK: the transaction lost a race, and running it again may succeed. */
|
|
228
|
+
const DEADLOCK = 1213;
|
|
217
229
|
/**
|
|
218
|
-
*
|
|
230
|
+
* The MySQL error number an error carries: its own, or that of the driver
|
|
231
|
+
* error it wraps, as a classified or a Drizzle query error does.
|
|
219
232
|
*/
|
|
220
|
-
function
|
|
221
|
-
|
|
233
|
+
function errnoOf(error) {
|
|
234
|
+
for (let at = error, hops = 0; hops < 4; hops++) {
|
|
235
|
+
if (typeof at !== "object" || at === null) return void 0;
|
|
236
|
+
const errno = Reflect.get(at, "errno");
|
|
237
|
+
if (typeof errno === "number") return errno;
|
|
238
|
+
at = Reflect.get(at, "cause");
|
|
239
|
+
}
|
|
222
240
|
}
|
|
223
241
|
/**
|
|
224
242
|
* MySQL database adapter for Nextly.
|
|
@@ -294,8 +312,14 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
294
312
|
async connect() {
|
|
295
313
|
if (this.connected && this.pool) return;
|
|
296
314
|
try {
|
|
315
|
+
this.warnOnDriverZoneDisagreement();
|
|
297
316
|
const poolConfig = this.buildPoolConfig();
|
|
298
317
|
const pool = mysql2_promise.default.createPool(poolConfig);
|
|
318
|
+
pool.pool.on("connection", (connection) => {
|
|
319
|
+
connection.query("SET SESSION time_zone = '+00:00'", (error) => {
|
|
320
|
+
if (error) connection.destroy();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
299
323
|
boundPoolWait(pool.pool, poolConfig.connectTimeout ?? 0);
|
|
300
324
|
this.pool = pool;
|
|
301
325
|
const connection = await this.pool.getConnection();
|
|
@@ -378,6 +402,59 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
378
402
|
};
|
|
379
403
|
}
|
|
380
404
|
/**
|
|
405
|
+
* The connection as a health check reads it, on one pooled connection: the
|
|
406
|
+
* server's version, timed for the round trip, then that session's
|
|
407
|
+
* `Ssl_cipher` status, which names the cipher when the session's connection
|
|
408
|
+
* is encrypted and is empty when it is not.
|
|
409
|
+
*
|
|
410
|
+
* @remarks
|
|
411
|
+
* The status and the connection's own socket are weighed by
|
|
412
|
+
* `encryptionOf`: behind a proxy the status can describe the proxy's own
|
|
413
|
+
* connection to the server, while this connection's socket is TLS. The
|
|
414
|
+
* socket is read as it is, never inferred from the `ssl` setting.
|
|
415
|
+
*
|
|
416
|
+
* Whether the database is on this machine is judged from the configured
|
|
417
|
+
* address, which never leaves this method. Any failure answers `"unknown"`.
|
|
418
|
+
*/
|
|
419
|
+
async inspectConnection() {
|
|
420
|
+
const local = this.isLocalHost(this.connectionHost());
|
|
421
|
+
let connection;
|
|
422
|
+
try {
|
|
423
|
+
connection = await this.ensurePool().getConnection();
|
|
424
|
+
const started = performance.now();
|
|
425
|
+
const [versionRows] = await connection.query("SELECT VERSION() AS version");
|
|
426
|
+
const roundTripMs = Math.max(0, Math.round(performance.now() - started));
|
|
427
|
+
const [statusRows] = await connection.query("SHOW SESSION STATUS LIKE 'Ssl_cipher'");
|
|
428
|
+
const cipher = statusRows[0]?.Value;
|
|
429
|
+
const version = versionRows[0]?.version;
|
|
430
|
+
return {
|
|
431
|
+
encryption: this.encryptionOf({
|
|
432
|
+
session: typeof cipher === "string" ? cipher !== "" : void 0,
|
|
433
|
+
ownSocketTls: this.ownSocketIsTls(connection)
|
|
434
|
+
}),
|
|
435
|
+
local,
|
|
436
|
+
roundTripMs,
|
|
437
|
+
...typeof version === "string" && version !== "" ? { serverVersion: version.split("-")[0] } : {}
|
|
438
|
+
};
|
|
439
|
+
} catch {
|
|
440
|
+
return {
|
|
441
|
+
encryption: "unknown",
|
|
442
|
+
local
|
|
443
|
+
};
|
|
444
|
+
} finally {
|
|
445
|
+
connection?.release();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The host this adapter connects to, as mysql2 resolves it: the connection
|
|
450
|
+
* string's when one is set, else the `host` option, else `localhost`.
|
|
451
|
+
* `undefined` when the connection string cannot be read.
|
|
452
|
+
*/
|
|
453
|
+
connectionHost() {
|
|
454
|
+
const configured = this.config.url ? this.hostOfUrl(this.config.url) : this.config.host ?? "";
|
|
455
|
+
return configured === "" ? "localhost" : configured;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
381
458
|
* Execute a raw SQL query.
|
|
382
459
|
*
|
|
383
460
|
* @param sql - SQL query string with ? placeholders
|
|
@@ -414,9 +491,29 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
414
491
|
*
|
|
415
492
|
* Note: Savepoints are disabled in this adapter for safety due to
|
|
416
493
|
* MySQL's quirks with nested transactions.
|
|
494
|
+
*
|
|
495
|
+
* A call made inside another transaction's work therefore joins it flat,
|
|
496
|
+
* on its connection: its statements are the outer's. Its failure cannot be
|
|
497
|
+
* undone alone, so it fails the whole transaction, even when the outer
|
|
498
|
+
* catches it: the outer's COMMIT is refused, it rolls back, and it rejects
|
|
499
|
+
* with the error that failed the joined call; any later joined call fails
|
|
500
|
+
* with that error at once. A joined call keeps the outer's options and is
|
|
501
|
+
* never retried itself. `retryWhen` is asked first, of every error that
|
|
502
|
+
* ends an attempt, application ones included.
|
|
417
503
|
*/
|
|
418
|
-
|
|
419
|
-
return this.
|
|
504
|
+
transaction(work, options) {
|
|
505
|
+
return this.openOrJoin(work, options, {
|
|
506
|
+
run: (_attempt, openedAt) => this.runAttempt(work, options, openedAt),
|
|
507
|
+
retryable: (error) => errnoOf(error) === DEADLOCK,
|
|
508
|
+
describe: (error) => {
|
|
509
|
+
const errno = errnoOf(error);
|
|
510
|
+
return {
|
|
511
|
+
label: errno === DEADLOCK ? "deadlock" : "a retryable error",
|
|
512
|
+
detail: { errno }
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
logger: this.config.logger
|
|
516
|
+
});
|
|
420
517
|
}
|
|
421
518
|
/**
|
|
422
519
|
* Runs `work` on a connection of its own, opened for it with the pool's
|
|
@@ -458,6 +555,7 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
458
555
|
try {
|
|
459
556
|
try {
|
|
460
557
|
await connection.query("SET SESSION autocommit = 1");
|
|
558
|
+
await connection.query("SET SESSION time_zone = '+00:00'");
|
|
461
559
|
if (options?.strictSqlMode) await this.enterStrictMode(connection, {});
|
|
462
560
|
} catch (error) {
|
|
463
561
|
throw this.classifyError(error);
|
|
@@ -476,50 +574,48 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
476
574
|
connection.destroy();
|
|
477
575
|
}
|
|
478
576
|
}
|
|
479
|
-
/**
|
|
480
|
-
async
|
|
577
|
+
/** One attempt at an outermost transaction, on a connection of its own. */
|
|
578
|
+
async runAttempt(work, options, openedAt) {
|
|
481
579
|
const pool = this.ensurePool();
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
let
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const result = await work(this.createTransactionContext(connection));
|
|
580
|
+
this.notePoolAccess();
|
|
581
|
+
const connection = await pool.getConnection();
|
|
582
|
+
let rollbackFailed = false;
|
|
583
|
+
const restore = {};
|
|
584
|
+
const settled = await this.settleOutermost({
|
|
585
|
+
context: this.createTransactionContext(connection),
|
|
586
|
+
join: this.joinSteps(connection),
|
|
587
|
+
openedAt,
|
|
588
|
+
begin: () => this.beginTransaction(connection, options, restore),
|
|
589
|
+
commit: async () => {
|
|
493
590
|
await connection.query("COMMIT");
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
attempt,
|
|
498
|
-
durationMs
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
return result;
|
|
502
|
-
} catch (error) {
|
|
503
|
-
await connection.query("ROLLBACK").catch(() => {
|
|
591
|
+
},
|
|
592
|
+
rollback: async () => {
|
|
593
|
+
await connection.query("ROLLBACK").catch((rollbackError) => {
|
|
504
594
|
rollbackFailed = true;
|
|
595
|
+
throw rollbackError;
|
|
505
596
|
});
|
|
506
|
-
lastError = error;
|
|
507
|
-
if ((0, _nextlyhq_adapter_drizzle_types.isApplicationError)(error)) throw error;
|
|
508
|
-
const mysqlError = error;
|
|
509
|
-
if (mysqlError.errno === 1213 && attempt < maxAttempts) {
|
|
510
|
-
if (this.config.logger?.warn) this.config.logger.warn(`Transaction failed with deadlock, retrying (${attempt}/${maxAttempts})`, {
|
|
511
|
-
errno: mysqlError.errno,
|
|
512
|
-
attempt
|
|
513
|
-
});
|
|
514
|
-
await delay(retryDelayMs * attempt);
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
throw this.classifyError(error);
|
|
518
|
-
} finally {
|
|
519
|
-
await returnTransactionConnection(connection, rollbackFailed, restore);
|
|
520
597
|
}
|
|
521
|
-
}
|
|
522
|
-
|
|
598
|
+
}, work, options);
|
|
599
|
+
await returnTransactionConnection(connection, rollbackFailed, restore);
|
|
600
|
+
return settled;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* How a call made inside a transaction on `connection` joins it: flat, on
|
|
604
|
+
* the same connection, with nothing to send. Any failure inside it fails
|
|
605
|
+
* the whole transaction, since no savepoint can undo the part alone.
|
|
606
|
+
*/
|
|
607
|
+
joinSteps(connection) {
|
|
608
|
+
const callbackConnection = connection.connection;
|
|
609
|
+
return this.joinStepsOn(() => this.createTransactionContext(connection), (relations) => relations ? (0, drizzle_orm_mysql2.drizzle)({
|
|
610
|
+
client: callbackConnection,
|
|
611
|
+
relations
|
|
612
|
+
}) : (0, drizzle_orm_mysql2.drizzle)({ client: callbackConnection }), (context) => ({
|
|
613
|
+
context,
|
|
614
|
+
begin() {},
|
|
615
|
+
commit() {},
|
|
616
|
+
rollback() {},
|
|
617
|
+
dooms: () => true
|
|
618
|
+
}));
|
|
523
619
|
}
|
|
524
620
|
/**
|
|
525
621
|
* Get MySQL database capabilities.
|
|
@@ -630,17 +726,36 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
630
726
|
"execute",
|
|
631
727
|
"getConnection"
|
|
632
728
|
]);
|
|
633
|
-
return this.
|
|
634
|
-
const connection = await
|
|
729
|
+
return this.joiningHandle(onClient(callbackPool), async () => {
|
|
730
|
+
const connection = await pool.getConnection();
|
|
635
731
|
return {
|
|
636
732
|
drizzle: onClient(connection.connection),
|
|
637
733
|
rollback: () => connection.query("ROLLBACK"),
|
|
638
734
|
release: (dirty) => {
|
|
639
735
|
if (dirty) connection.destroy();
|
|
640
736
|
else connection.release();
|
|
641
|
-
}
|
|
737
|
+
},
|
|
738
|
+
join: this.joinSteps(connection)
|
|
642
739
|
};
|
|
643
|
-
})
|
|
740
|
+
}, relations);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Warns when a caller's configured driver zone does not agree with the
|
|
744
|
+
* pinned UTC session.
|
|
745
|
+
*
|
|
746
|
+
* mysql2's driver-level `timezone` decides how it parses the date strings
|
|
747
|
+
* the pinned session answers with and how it binds raw Date parameters, so
|
|
748
|
+
* a non-UTC spelling there would shift values a round-trip fails to
|
|
749
|
+
* cancel. The configuration is aligned to the pin (`buildConnectionConfig`
|
|
750
|
+
* sets UTC unconditionally, for pooled and standalone connections alike),
|
|
751
|
+
* never rewritten silently: the caller configured a reading, and this is
|
|
752
|
+
* where that reading is named as lost. Runs from `connect`, once per pool
|
|
753
|
+
* build — the configuration cannot change between calls.
|
|
754
|
+
*/
|
|
755
|
+
warnOnDriverZoneDisagreement() {
|
|
756
|
+
const requestedZone = this.config.timezone ?? "local";
|
|
757
|
+
if (isUtcDriverZone(requestedZone)) return;
|
|
758
|
+
this.config.logger?.warn?.(`Driver timezone "${requestedZone}" is not UTC; it is aligned to the pinned UTC session and no longer shifts reads or writes.`);
|
|
644
759
|
}
|
|
645
760
|
/**
|
|
646
761
|
* Builds mysql2 Pool configuration from adapter config.
|
|
@@ -661,7 +776,7 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
661
776
|
const config = this.buildAddressConfig();
|
|
662
777
|
config.connectTimeout = this.config.pool?.connectionTimeoutMs ?? DEFAULT_POOL_CONFIG.connectionTimeoutMs;
|
|
663
778
|
if (this.config.ssl) config.ssl = this.buildSslConfig(this.config.ssl);
|
|
664
|
-
|
|
779
|
+
config.timezone = "Z";
|
|
665
780
|
if (this.config.charset) config.charset = this.config.charset;
|
|
666
781
|
config.multipleStatements = false;
|
|
667
782
|
config.dateStrings = false;
|
|
@@ -924,7 +1039,8 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
924
1039
|
await connection.query(`SELECT ${idColumn} FROM ${this.escapeIdentifier(table)} WHERE ${idColumn} = ? FOR UPDATE`, [id]);
|
|
925
1040
|
},
|
|
926
1041
|
insert: async (table, data, options) => {
|
|
927
|
-
const
|
|
1042
|
+
const insertTableObj = this.resolvedTableObject(table);
|
|
1043
|
+
const mapped = this.mapRowToRawSql(insertTableObj, data);
|
|
928
1044
|
const columns = Object.keys(mapped);
|
|
929
1045
|
const values = Object.values(mapped);
|
|
930
1046
|
const placeholders = this.buildPlaceholders(values.length, 0);
|
|
@@ -932,7 +1048,6 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
932
1048
|
const [result] = await connection.query(sql, values);
|
|
933
1049
|
const ret = options?.returning;
|
|
934
1050
|
if (Array.isArray(ret) && ret.length === 0) return;
|
|
935
|
-
const insertTableObj = this.getTableObject(table);
|
|
936
1051
|
const aliases = this.dateWallClockAliases(insertTableObj, ret ?? "*");
|
|
937
1052
|
const spelled = aliases.map((a) => `DATE_FORMAT(${this.escapeIdentifier(a.sqlName)}, '%Y-%m-%dT%H:%i:%s.%f') AS ${this.escapeIdentifier(a.alias)}`).join(", ");
|
|
938
1053
|
const projected = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(insertTableObj, ret).map((c) => this.escapeIdentifier(c)).join(", ");
|
|
@@ -950,7 +1065,7 @@ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
|
|
|
950
1065
|
if (data.length === 0) return [];
|
|
951
1066
|
const retMany = options?.returning;
|
|
952
1067
|
const skipReread = Array.isArray(retMany) && retMany.length === 0;
|
|
953
|
-
const tableObj = this.
|
|
1068
|
+
const tableObj = this.resolvedTableObject(table);
|
|
954
1069
|
const mappedRecords = data.map((r) => this.mapRowToRawSql(tableObj, r));
|
|
955
1070
|
const columns = Object.keys(mappedRecords[0]);
|
|
956
1071
|
const allValues = [];
|