@remix-run/data-table-mysql 0.4.0 → 0.5.0
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/README.md +47 -24
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/database.d.ts +36 -0
- package/dist/lib/database.d.ts.map +1 -0
- package/dist/lib/database.js +32 -0
- package/dist/lib/{adapter.d.ts → driver.d.ts} +39 -39
- package/dist/lib/driver.d.ts.map +1 -0
- package/dist/lib/driver.js +581 -0
- package/dist/lib/sql-compiler.d.ts +2 -1
- package/dist/lib/sql-compiler.d.ts.map +1 -1
- package/dist/lib/sql-compiler.js +6 -6
- package/package.json +12 -12
- package/src/index.ts +3 -1
- package/src/lib/database.ts +46 -0
- package/src/lib/{adapter.ts → driver.ts} +359 -55
- package/src/lib/sql-compiler.ts +8 -7
- package/dist/lib/adapter.d.ts.map +0 -1
- package/dist/lib/adapter.js +0 -344
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { getTablePrimaryKey } from '@remix-run/data-table';
|
|
3
|
+
import mysql from 'mysql2/promise';
|
|
4
|
+
import { compileMysqlOperation } from './sql-compiler.js';
|
|
5
|
+
const mysqlCapabilities = Object.freeze({
|
|
6
|
+
returning: false,
|
|
7
|
+
savepoints: true,
|
|
8
|
+
upsert: true,
|
|
9
|
+
transactionalDdl: false,
|
|
10
|
+
migrationLock: true,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* MySQL database driver backed by a mysql-compatible client.
|
|
14
|
+
*/
|
|
15
|
+
export class MysqlDatabaseDriver {
|
|
16
|
+
/**
|
|
17
|
+
* The SQL dialect identifier reported by this database.
|
|
18
|
+
*/
|
|
19
|
+
get dialect() {
|
|
20
|
+
return 'mysql';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Feature flags describing the MySQL behaviors supported by this database.
|
|
24
|
+
*/
|
|
25
|
+
get capabilities() {
|
|
26
|
+
return mysqlCapabilities;
|
|
27
|
+
}
|
|
28
|
+
#config;
|
|
29
|
+
#client;
|
|
30
|
+
#characterSet;
|
|
31
|
+
#collation;
|
|
32
|
+
#transactions = new Map();
|
|
33
|
+
#transactionCounter = 0;
|
|
34
|
+
#migrationLockQueue = Promise.resolve();
|
|
35
|
+
#migrationLockStore = new AsyncLocalStorage();
|
|
36
|
+
#poolClosed = false;
|
|
37
|
+
constructor(config, options = {}) {
|
|
38
|
+
if (isMysqlQueryable(config)) {
|
|
39
|
+
this.#client = config;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.#config = config;
|
|
43
|
+
this.#client = createMysqlPool(config);
|
|
44
|
+
}
|
|
45
|
+
this.#characterSet = options.characterSet;
|
|
46
|
+
this.#collation = options.collation;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Compiles a data-manipulation operation to mysql SQL statements.
|
|
50
|
+
* @param operation Operation to compile.
|
|
51
|
+
* @returns Compiled SQL statements.
|
|
52
|
+
*/
|
|
53
|
+
compileSql(operation) {
|
|
54
|
+
let compiled = compileMysqlOperation(operation);
|
|
55
|
+
return [{ text: compiled.text, values: compiled.values }];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Executes a mysql data-manipulation request.
|
|
59
|
+
* @param request Request to execute.
|
|
60
|
+
* @returns Execution result.
|
|
61
|
+
*/
|
|
62
|
+
async execute(request) {
|
|
63
|
+
if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
affectedRows: 0,
|
|
66
|
+
insertId: undefined,
|
|
67
|
+
rows: request.operation.returning ? [] : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let statements = this.compileSql(request.operation);
|
|
71
|
+
let statement = statements[0];
|
|
72
|
+
let client = this.#resolveClient(request.transaction);
|
|
73
|
+
let [result] = await client.query(statement.text, statement.values);
|
|
74
|
+
if (isRowsResult(result)) {
|
|
75
|
+
let rows = normalizeRows(result);
|
|
76
|
+
if (request.operation.kind === 'count' || request.operation.kind === 'exists') {
|
|
77
|
+
rows = normalizeCountRows(rows);
|
|
78
|
+
}
|
|
79
|
+
return { rows };
|
|
80
|
+
}
|
|
81
|
+
let header = normalizeHeader(result);
|
|
82
|
+
return {
|
|
83
|
+
affectedRows: header.affectedRows,
|
|
84
|
+
insertId: normalizeInsertId(request.operation.kind, request.operation, header),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Executes a multi-statement mysql SQL script.
|
|
89
|
+
*
|
|
90
|
+
* mysql2 only accepts multi-statement scripts when the underlying connection
|
|
91
|
+
* was created with `multipleStatements: true`.
|
|
92
|
+
* @param sql SQL script to execute.
|
|
93
|
+
* @param transaction Optional transaction token.
|
|
94
|
+
* @returns A promise that resolves once execution completes.
|
|
95
|
+
*/
|
|
96
|
+
async executeScript(sql, transaction) {
|
|
97
|
+
let client = this.#resolveClient(transaction);
|
|
98
|
+
await client.query(sql);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Checks whether a table exists in mysql.
|
|
102
|
+
* @param table Table reference to inspect.
|
|
103
|
+
* @param transaction Optional transaction token.
|
|
104
|
+
* @returns `true` when the table exists.
|
|
105
|
+
*/
|
|
106
|
+
async hasTable(table, transaction) {
|
|
107
|
+
let schema = table.schema;
|
|
108
|
+
let sql = schema
|
|
109
|
+
? 'select exists(select 1 from information_schema.tables where table_schema = ? and table_name = ?) as `exists`'
|
|
110
|
+
: 'select exists(select 1 from information_schema.tables where table_schema = database() and table_name = ?) as `exists`';
|
|
111
|
+
let values = schema ? [schema, table.name] : [table.name];
|
|
112
|
+
let client = this.#resolveClient(transaction);
|
|
113
|
+
let [result] = await client.query(sql, values);
|
|
114
|
+
if (!isRowsResult(result)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return toBooleanExists(result[0]?.exists);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Checks whether a column exists in mysql.
|
|
121
|
+
* @param table Table reference to inspect.
|
|
122
|
+
* @param column Column name to look up.
|
|
123
|
+
* @param transaction Optional transaction token.
|
|
124
|
+
* @returns `true` when the column exists.
|
|
125
|
+
*/
|
|
126
|
+
async hasColumn(table, column, transaction) {
|
|
127
|
+
let schema = table.schema;
|
|
128
|
+
let sql = schema
|
|
129
|
+
? 'select exists(select 1 from information_schema.columns where table_schema = ? and table_name = ? and column_name = ?) as `exists`'
|
|
130
|
+
: 'select exists(select 1 from information_schema.columns where table_schema = database() and table_name = ? and column_name = ?) as `exists`';
|
|
131
|
+
let values = schema ? [schema, table.name, column] : [table.name, column];
|
|
132
|
+
let client = this.#resolveClient(transaction);
|
|
133
|
+
let [result] = await client.query(sql, values);
|
|
134
|
+
if (!isRowsResult(result)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return toBooleanExists(result[0]?.exists);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Starts a mysql transaction.
|
|
141
|
+
* @param options Transaction options.
|
|
142
|
+
* @returns Transaction token.
|
|
143
|
+
*/
|
|
144
|
+
async beginTransaction(options) {
|
|
145
|
+
let releaseOnClose = false;
|
|
146
|
+
let connection;
|
|
147
|
+
if (isMysqlPool(this.#client)) {
|
|
148
|
+
connection = await this.#client.getConnection();
|
|
149
|
+
releaseOnClose = true;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
connection = this.#client;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
if (options?.isolationLevel) {
|
|
156
|
+
await connection.query('set transaction isolation level ' + options.isolationLevel);
|
|
157
|
+
}
|
|
158
|
+
if (options?.readOnly !== undefined) {
|
|
159
|
+
await connection.query(options.readOnly ? 'set transaction read only' : 'set transaction read write');
|
|
160
|
+
}
|
|
161
|
+
await connection.beginTransaction();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (releaseOnClose) {
|
|
165
|
+
destroyMysqlConnection(connection);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
this.#transactionCounter += 1;
|
|
170
|
+
let token = { id: 'tx_' + String(this.#transactionCounter) };
|
|
171
|
+
this.#transactions.set(token.id, {
|
|
172
|
+
connection,
|
|
173
|
+
releaseOnClose,
|
|
174
|
+
});
|
|
175
|
+
return token;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Commits an open mysql transaction.
|
|
179
|
+
* @param token Transaction token to commit.
|
|
180
|
+
* @returns A promise that resolves when the transaction is committed.
|
|
181
|
+
*/
|
|
182
|
+
async commitTransaction(token) {
|
|
183
|
+
let transaction = this.#transactions.get(token.id);
|
|
184
|
+
if (!transaction) {
|
|
185
|
+
throw new Error('Unknown transaction token: ' + token.id);
|
|
186
|
+
}
|
|
187
|
+
let failed = false;
|
|
188
|
+
try {
|
|
189
|
+
await transaction.connection.commit();
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
failed = true;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
this.#transactions.delete(token.id);
|
|
197
|
+
if (transaction.releaseOnClose) {
|
|
198
|
+
if (failed) {
|
|
199
|
+
destroyMysqlConnection(transaction.connection);
|
|
200
|
+
}
|
|
201
|
+
else if (isMysqlPoolConnection(transaction.connection)) {
|
|
202
|
+
transaction.connection.release();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Rolls back an open mysql transaction.
|
|
209
|
+
* @param token Transaction token to roll back.
|
|
210
|
+
* @returns A promise that resolves when the transaction is rolled back.
|
|
211
|
+
*/
|
|
212
|
+
async rollbackTransaction(token) {
|
|
213
|
+
let transaction = this.#transactions.get(token.id);
|
|
214
|
+
if (!transaction) {
|
|
215
|
+
throw new Error('Unknown transaction token: ' + token.id);
|
|
216
|
+
}
|
|
217
|
+
let failed = false;
|
|
218
|
+
try {
|
|
219
|
+
await transaction.connection.rollback();
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
failed = true;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
this.#transactions.delete(token.id);
|
|
227
|
+
if (transaction.releaseOnClose) {
|
|
228
|
+
if (failed) {
|
|
229
|
+
destroyMysqlConnection(transaction.connection);
|
|
230
|
+
}
|
|
231
|
+
else if (isMysqlPoolConnection(transaction.connection)) {
|
|
232
|
+
transaction.connection.release();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Creates a savepoint in an open mysql transaction.
|
|
239
|
+
* @param token Transaction token to use.
|
|
240
|
+
* @param name Savepoint name.
|
|
241
|
+
* @returns A promise that resolves when the savepoint is created.
|
|
242
|
+
*/
|
|
243
|
+
async createSavepoint(token, name) {
|
|
244
|
+
let connection = this.#transactionConnection(token);
|
|
245
|
+
await connection.query('savepoint ' + quoteIdentifier(name));
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Rolls back to a savepoint in an open mysql transaction.
|
|
249
|
+
* @param token Transaction token to use.
|
|
250
|
+
* @param name Savepoint name.
|
|
251
|
+
* @returns A promise that resolves when the rollback completes.
|
|
252
|
+
*/
|
|
253
|
+
async rollbackToSavepoint(token, name) {
|
|
254
|
+
let connection = this.#transactionConnection(token);
|
|
255
|
+
await connection.query('rollback to savepoint ' + quoteIdentifier(name));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Releases a savepoint in an open mysql transaction.
|
|
259
|
+
* @param token Transaction token to use.
|
|
260
|
+
* @param name Savepoint name.
|
|
261
|
+
* @returns A promise that resolves when the savepoint is released.
|
|
262
|
+
*/
|
|
263
|
+
async releaseSavepoint(token, name) {
|
|
264
|
+
let connection = this.#transactionConnection(token);
|
|
265
|
+
await connection.query('release savepoint ' + quoteIdentifier(name));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Destructively recreates the configured MySQL database.
|
|
269
|
+
* @returns A promise that resolves when the database is ready for use.
|
|
270
|
+
*/
|
|
271
|
+
async wipe() {
|
|
272
|
+
let config = this.#configOrThrow('wipe');
|
|
273
|
+
this.#assertNoOpenTransactions('wipe');
|
|
274
|
+
let database = resolveMysqlDatabaseName(config);
|
|
275
|
+
await this.#closePool();
|
|
276
|
+
let connection;
|
|
277
|
+
try {
|
|
278
|
+
connection = await createMysqlConnection(toMysqlServerConfig(config));
|
|
279
|
+
await connection.query('drop database if exists ' + quoteIdentifier(database));
|
|
280
|
+
let sql = 'create database ' + quoteIdentifier(database);
|
|
281
|
+
if (this.#characterSet) {
|
|
282
|
+
sql += ' character set ' + quoteIdentifier(this.#characterSet);
|
|
283
|
+
}
|
|
284
|
+
if (this.#collation) {
|
|
285
|
+
sql += ' collate ' + quoteIdentifier(this.#collation);
|
|
286
|
+
}
|
|
287
|
+
await connection.query(sql);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
try {
|
|
291
|
+
await connection?.end();
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
await this.#replacePool();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Closes a pool created from configuration. Supplied connections and pools remain caller-owned. */
|
|
299
|
+
async close() {
|
|
300
|
+
this.#assertNoOpenTransactions('close');
|
|
301
|
+
if (this.#config) {
|
|
302
|
+
await this.#closePool();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Runs migration work on the mysql connection that owns the named lock.
|
|
307
|
+
*
|
|
308
|
+
* Lock acquisition waits up to 60 seconds and throws when the lock cannot
|
|
309
|
+
* be acquired. Re-entering this method from inside `run` throws instead of
|
|
310
|
+
* deadlocking, and a failed run destroys the reserved connection instead of
|
|
311
|
+
* returning it to the pool.
|
|
312
|
+
* @param name Logical migration lock name.
|
|
313
|
+
* @param run Migration work to run with a connection-bound driver.
|
|
314
|
+
* @returns The callback result.
|
|
315
|
+
*/
|
|
316
|
+
async withMigrationLock(name, run) {
|
|
317
|
+
if (this.#migrationLockStore.getStore()) {
|
|
318
|
+
throw new Error('MySQL migration lock is already held by this database');
|
|
319
|
+
}
|
|
320
|
+
let waitForPreviousLock = this.#migrationLockQueue;
|
|
321
|
+
let releaseQueue = () => undefined;
|
|
322
|
+
this.#migrationLockQueue = new Promise((resolve) => {
|
|
323
|
+
releaseQueue = resolve;
|
|
324
|
+
});
|
|
325
|
+
await waitForPreviousLock;
|
|
326
|
+
try {
|
|
327
|
+
let releaseOnClose = false;
|
|
328
|
+
let connection;
|
|
329
|
+
if (isMysqlPool(this.#client)) {
|
|
330
|
+
connection = await this.#client.getConnection();
|
|
331
|
+
releaseOnClose = true;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
connection = this.#client;
|
|
335
|
+
}
|
|
336
|
+
let driver = releaseOnClose ? new MysqlDatabaseDriver(connection) : this;
|
|
337
|
+
try {
|
|
338
|
+
let value = await this.#migrationLockStore.run(true, () => runWithMysqlMigrationLock(connection, name, driver, run));
|
|
339
|
+
if (releaseOnClose && isMysqlPoolConnection(connection)) {
|
|
340
|
+
connection.release();
|
|
341
|
+
}
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
// A failed run can leave the reserved connection dirty (open
|
|
346
|
+
// transaction, still-held named lock), so destroy the connection
|
|
347
|
+
// instead of returning it to the pool.
|
|
348
|
+
if (releaseOnClose) {
|
|
349
|
+
destroyMysqlConnection(connection);
|
|
350
|
+
}
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
releaseQueue();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async #closePool() {
|
|
359
|
+
this.#transactions.clear();
|
|
360
|
+
// mysql2 pools error on end() when called twice, so ending must be
|
|
361
|
+
// tracked to keep close() idempotent.
|
|
362
|
+
if (isMysqlPool(this.#client) && !this.#poolClosed) {
|
|
363
|
+
this.#poolClosed = true;
|
|
364
|
+
await this.#client.end();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async #replacePool() {
|
|
368
|
+
await this.#closePool().catch(() => undefined);
|
|
369
|
+
if (this.#config) {
|
|
370
|
+
this.#client = createMysqlPool(this.#config);
|
|
371
|
+
this.#poolClosed = false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
#configOrThrow(method) {
|
|
375
|
+
if (!this.#config) {
|
|
376
|
+
throw new Error('MySQL database ' + method + '() requires config-based construction');
|
|
377
|
+
}
|
|
378
|
+
return this.#config;
|
|
379
|
+
}
|
|
380
|
+
#assertNoOpenTransactions(method) {
|
|
381
|
+
if (this.#transactions.size > 0) {
|
|
382
|
+
throw new Error('MySQL database cannot ' + method + ' while transactions are open');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
#resolveClient(token) {
|
|
386
|
+
if (!token) {
|
|
387
|
+
return this.#client;
|
|
388
|
+
}
|
|
389
|
+
return this.#transactionConnection(token);
|
|
390
|
+
}
|
|
391
|
+
#transactionConnection(token) {
|
|
392
|
+
let transaction = this.#transactions.get(token.id);
|
|
393
|
+
if (!transaction) {
|
|
394
|
+
throw new Error('Unknown transaction token: ' + token.id);
|
|
395
|
+
}
|
|
396
|
+
return transaction.connection;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function isMysqlQueryable(value) {
|
|
400
|
+
return typeof value === 'object' && value !== null && 'query' in value;
|
|
401
|
+
}
|
|
402
|
+
function createMysqlPool(config) {
|
|
403
|
+
return typeof config === 'string' ? mysql.createPool(config) : mysql.createPool(config);
|
|
404
|
+
}
|
|
405
|
+
function createMysqlConnection(config) {
|
|
406
|
+
return typeof config === 'string'
|
|
407
|
+
? mysql.createConnection(config)
|
|
408
|
+
: mysql.createConnection(config);
|
|
409
|
+
}
|
|
410
|
+
function isMysqlPool(client) {
|
|
411
|
+
return 'getConnection' in client && typeof client.getConnection === 'function';
|
|
412
|
+
}
|
|
413
|
+
function resolveMysqlDatabaseName(config) {
|
|
414
|
+
let database = typeof config === 'string'
|
|
415
|
+
? resolveDatabaseNameFromUrl(config)
|
|
416
|
+
: (config.database ?? resolveDatabaseNameFromUrl(config.uri ?? ''));
|
|
417
|
+
if (!database) {
|
|
418
|
+
throw new Error('MySQL database config requires a database name');
|
|
419
|
+
}
|
|
420
|
+
return database;
|
|
421
|
+
}
|
|
422
|
+
function resolveDatabaseNameFromUrl(value) {
|
|
423
|
+
try {
|
|
424
|
+
let url = new URL(value);
|
|
425
|
+
let database = decodeURIComponent(url.pathname.replace(/^\//, ''));
|
|
426
|
+
return database || undefined;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function toMysqlServerConfig(config) {
|
|
433
|
+
if (typeof config === 'string') {
|
|
434
|
+
try {
|
|
435
|
+
let url = new URL(config);
|
|
436
|
+
url.pathname = '/';
|
|
437
|
+
return url.toString();
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return config;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Pool-only options make mysql2's createConnection() log "Ignoring invalid
|
|
444
|
+
// configuration option" warnings, so strip them from the maintenance
|
|
445
|
+
// connection config.
|
|
446
|
+
let { database: _database, uri, connectionLimit: _connectionLimit, maxIdle: _maxIdle, idleTimeout: _idleTimeout, queueLimit: _queueLimit, waitForConnections: _waitForConnections, ...serverConfig } = config;
|
|
447
|
+
if (uri === undefined) {
|
|
448
|
+
return serverConfig;
|
|
449
|
+
}
|
|
450
|
+
return { ...serverConfig, uri: removeDatabaseFromUrl(uri) };
|
|
451
|
+
}
|
|
452
|
+
function removeDatabaseFromUrl(value) {
|
|
453
|
+
try {
|
|
454
|
+
let url = new URL(value);
|
|
455
|
+
url.pathname = '/';
|
|
456
|
+
return url.toString();
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function isMysqlPoolConnection(connection) {
|
|
463
|
+
return 'release' in connection && typeof connection.release === 'function';
|
|
464
|
+
}
|
|
465
|
+
function destroyMysqlConnection(connection) {
|
|
466
|
+
let destroy = connection.destroy;
|
|
467
|
+
if (typeof destroy === 'function') {
|
|
468
|
+
destroy.call(connection);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
void connection.end().catch(() => undefined);
|
|
472
|
+
}
|
|
473
|
+
async function runWithMysqlMigrationLock(connection, name, driver, run) {
|
|
474
|
+
// sha2(..., 256) yields 64 hex characters, exactly GET_LOCK's 64-character
|
|
475
|
+
// lock name limit, so any additional prefix must go inside the hash input.
|
|
476
|
+
let [lockRows] = await connection.query("select lock_name, get_lock(lock_name, 60) as `acquired` from (select sha2(concat(coalesce(database(), ''), ':', ?), 256) as lock_name) as migration_lock", [name]);
|
|
477
|
+
let lockRow = isRowsResult(lockRows) ? lockRows[0] : undefined;
|
|
478
|
+
let lockName = lockRow?.lock_name;
|
|
479
|
+
if (typeof lockName !== 'string' || !toBooleanExists(lockRow?.acquired)) {
|
|
480
|
+
throw new Error('MySQL migration lock could not be acquired');
|
|
481
|
+
}
|
|
482
|
+
let outcome;
|
|
483
|
+
try {
|
|
484
|
+
outcome = { status: 'success', value: await run(driver) };
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
outcome = { status: 'failure', error };
|
|
488
|
+
}
|
|
489
|
+
let unlockFailed = false;
|
|
490
|
+
let unlockError;
|
|
491
|
+
try {
|
|
492
|
+
let [unlockRows] = await connection.query('select release_lock(?) as `released`', [lockName]);
|
|
493
|
+
if (!isRowsResult(unlockRows) || !toBooleanExists(unlockRows[0]?.released)) {
|
|
494
|
+
throw new Error('MySQL migration lock was not held by the reserved connection');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
unlockFailed = true;
|
|
499
|
+
unlockError = error;
|
|
500
|
+
}
|
|
501
|
+
if (outcome.status === 'failure') {
|
|
502
|
+
throw outcome.error;
|
|
503
|
+
}
|
|
504
|
+
if (unlockFailed) {
|
|
505
|
+
throw unlockError;
|
|
506
|
+
}
|
|
507
|
+
return outcome.value;
|
|
508
|
+
}
|
|
509
|
+
function isRowsResult(result) {
|
|
510
|
+
return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]));
|
|
511
|
+
}
|
|
512
|
+
function toBooleanExists(value) {
|
|
513
|
+
if (typeof value === 'boolean') {
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
if (typeof value === 'number') {
|
|
517
|
+
return value > 0;
|
|
518
|
+
}
|
|
519
|
+
if (typeof value === 'bigint') {
|
|
520
|
+
return value > 0n;
|
|
521
|
+
}
|
|
522
|
+
if (typeof value === 'string') {
|
|
523
|
+
return value === '1' || value.toLowerCase() === 'true';
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
function normalizeRows(rows) {
|
|
528
|
+
return rows.map((row) => ({ ...row }));
|
|
529
|
+
}
|
|
530
|
+
function normalizeHeader(result) {
|
|
531
|
+
if (typeof result === 'object' && result !== null) {
|
|
532
|
+
let header = result;
|
|
533
|
+
return {
|
|
534
|
+
affectedRows: typeof header.affectedRows === 'number' ? header.affectedRows : 0,
|
|
535
|
+
insertId: header.insertId,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
affectedRows: 0,
|
|
540
|
+
insertId: undefined,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function normalizeCountRows(rows) {
|
|
544
|
+
return rows.map((row) => {
|
|
545
|
+
let count = row.count;
|
|
546
|
+
if (typeof count === 'string') {
|
|
547
|
+
let numeric = Number(count);
|
|
548
|
+
if (!Number.isNaN(numeric)) {
|
|
549
|
+
return {
|
|
550
|
+
...row,
|
|
551
|
+
count: numeric,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (typeof count === 'bigint') {
|
|
556
|
+
return {
|
|
557
|
+
...row,
|
|
558
|
+
count: Number(count),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return row;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function normalizeInsertId(kind, operation, header) {
|
|
565
|
+
if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
if (getTablePrimaryKey(operation.table).length !== 1) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
return header.insertId;
|
|
572
|
+
}
|
|
573
|
+
function quoteIdentifier(value) {
|
|
574
|
+
return '`' + value.replace(/`/g, '``') + '`';
|
|
575
|
+
}
|
|
576
|
+
function isInsertOperationKind(kind) {
|
|
577
|
+
return kind === 'insert' || kind === 'insertMany' || kind === 'upsert';
|
|
578
|
+
}
|
|
579
|
+
function isInsertOperation(operation) {
|
|
580
|
+
return (operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert');
|
|
581
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SqlStatement } from '@remix-run/data-table';
|
|
2
|
+
import type { DataManipulationOperation } from '@remix-run/data-table';
|
|
2
3
|
export declare function compileMysqlOperation(operation: DataManipulationOperation): SqlStatement;
|
|
3
4
|
//# sourceMappingURL=sql-compiler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"sql-compiler.d.ts","sourceRoot":"","sources":["../../src/lib/sql-compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAetE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,yBAAyB,GAAG,YAAY,CAgGxF"}
|
package/dist/lib/sql-compiler.js
CHANGED
|
@@ -24,8 +24,8 @@ export function compileMysqlOperation(operation) {
|
|
|
24
24
|
compileGroupByClause(operation.groupBy) +
|
|
25
25
|
compileHavingClause(operation.having, context) +
|
|
26
26
|
compileOrderByClause(operation.orderBy) +
|
|
27
|
-
compileLimitClause(operation.limit) +
|
|
28
|
-
compileOffsetClause(operation.offset),
|
|
27
|
+
compileLimitClause(operation.limit, context) +
|
|
28
|
+
compileOffsetClause(operation.offset, context),
|
|
29
29
|
values: context.values,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -195,17 +195,17 @@ function compileOrderByClause(orderBy) {
|
|
|
195
195
|
.map((clause) => quotePath(clause.column) + ' ' + clause.direction.toUpperCase())
|
|
196
196
|
.join(', '));
|
|
197
197
|
}
|
|
198
|
-
function compileLimitClause(limit) {
|
|
198
|
+
function compileLimitClause(limit, context) {
|
|
199
199
|
if (limit === undefined) {
|
|
200
200
|
return '';
|
|
201
201
|
}
|
|
202
|
-
return ' limit ' +
|
|
202
|
+
return ' limit ' + pushValue(context, limit);
|
|
203
203
|
}
|
|
204
|
-
function compileOffsetClause(offset) {
|
|
204
|
+
function compileOffsetClause(offset, context) {
|
|
205
205
|
if (offset === undefined) {
|
|
206
206
|
return '';
|
|
207
207
|
}
|
|
208
|
-
return ' offset ' +
|
|
208
|
+
return ' offset ' + pushValue(context, offset);
|
|
209
209
|
}
|
|
210
210
|
function compilePredicate(predicate, context) {
|
|
211
211
|
if (predicate.type === 'comparison') {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/data-table-mysql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MySQL
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "MySQL database implementation for remix/data-table",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "^24.6.0",
|
|
31
|
-
"
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
32
|
"mysql2": "^3.15.3",
|
|
33
|
-
"@remix-run/
|
|
34
|
-
"@remix-run/data-table": "0.
|
|
35
|
-
"@remix-run/
|
|
33
|
+
"@remix-run/test": "0.6.0",
|
|
34
|
+
"@remix-run/data-table": "0.4.0",
|
|
35
|
+
"@remix-run/assert": "0.3.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@remix-run/data-table": "^0.
|
|
38
|
+
"@remix-run/data-table": "^0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"mysql2": "^3.15.3"
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"sql"
|
|
54
54
|
],
|
|
55
55
|
"scripts": {
|
|
56
|
-
"build": "
|
|
56
|
+
"build": "tsc -p tsconfig.build.json",
|
|
57
57
|
"clean": "git clean -fdX",
|
|
58
|
-
"test": "remix
|
|
59
|
-
"test:bun": "bun x --bun remix
|
|
60
|
-
"test:coverage": "remix
|
|
61
|
-
"typecheck": "
|
|
58
|
+
"test": "remix test",
|
|
59
|
+
"test:bun": "bun x --bun remix test",
|
|
60
|
+
"test:coverage": "remix test --coverage",
|
|
61
|
+
"typecheck": "tsc --noEmit"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createMysqlDatabase, MysqlDatabase } from './lib/database.ts'
|
|
2
|
+
export type { MysqlDatabaseOptions } from './lib/database.ts'
|
|
3
|
+
export type { MysqlDatabaseInput } from './lib/driver.ts'
|