@remix-run/data-table-mysql 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/adapter.d.ts +69 -0
- package/dist/lib/adapter.d.ts.map +1 -0
- package/dist/lib/adapter.js +194 -0
- package/dist/lib/sql-compiler.d.ts +8 -0
- package/dist/lib/sql-compiler.d.ts.map +1 -0
- package/dist/lib/sql-compiler.js +339 -0
- package/package.json +48 -7
- package/src/index.ts +9 -0
- package/src/lib/adapter.ts +319 -0
- package/src/lib/sql-compiler.ts +476 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AdapterCapabilityOverrides,
|
|
3
|
+
AdapterExecuteRequest,
|
|
4
|
+
AdapterResult,
|
|
5
|
+
DatabaseAdapter,
|
|
6
|
+
TransactionOptions,
|
|
7
|
+
TransactionToken,
|
|
8
|
+
} from '@remix-run/data-table'
|
|
9
|
+
import { getTablePrimaryKey } from '@remix-run/data-table'
|
|
10
|
+
|
|
11
|
+
import { compileMysqlStatement } from './sql-compiler.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Row-array response shape for mysql query calls.
|
|
15
|
+
*/
|
|
16
|
+
export type MysqlQueryRows = Record<string, unknown>[]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metadata shape for mysql write results.
|
|
20
|
+
*/
|
|
21
|
+
export type MysqlQueryResultHeader = {
|
|
22
|
+
affectedRows: number
|
|
23
|
+
insertId: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Supported mysql `query()` response tuple.
|
|
28
|
+
*/
|
|
29
|
+
export type MysqlQueryResponse = [result: unknown, fields?: unknown]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Single mysql connection contract used by this adapter.
|
|
33
|
+
*/
|
|
34
|
+
export type MysqlDatabaseConnection = {
|
|
35
|
+
query(text: string, values?: unknown[]): Promise<MysqlQueryResponse>
|
|
36
|
+
beginTransaction(): Promise<void>
|
|
37
|
+
commit(): Promise<void>
|
|
38
|
+
rollback(): Promise<void>
|
|
39
|
+
release?: () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mysql pool contract used by this adapter.
|
|
44
|
+
*/
|
|
45
|
+
export type MysqlDatabasePool = {
|
|
46
|
+
query(text: string, values?: unknown[]): Promise<MysqlQueryResponse>
|
|
47
|
+
getConnection(): Promise<MysqlDatabaseConnection>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mysql adapter configuration.
|
|
52
|
+
*/
|
|
53
|
+
export type MysqlDatabaseAdapterOptions = {
|
|
54
|
+
capabilities?: AdapterCapabilityOverrides
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type TransactionState = {
|
|
58
|
+
connection: MysqlDatabaseConnection
|
|
59
|
+
releaseOnClose: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type MysqlQueryable = MysqlDatabasePool | MysqlDatabaseConnection
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `DatabaseAdapter` implementation for mysql-compatible clients.
|
|
66
|
+
*/
|
|
67
|
+
export class MysqlDatabaseAdapter implements DatabaseAdapter {
|
|
68
|
+
dialect = 'mysql'
|
|
69
|
+
capabilities
|
|
70
|
+
|
|
71
|
+
#client: MysqlQueryable
|
|
72
|
+
#transactions = new Map<string, TransactionState>()
|
|
73
|
+
#transactionCounter = 0
|
|
74
|
+
|
|
75
|
+
constructor(client: MysqlQueryable, options?: MysqlDatabaseAdapterOptions) {
|
|
76
|
+
this.#client = client
|
|
77
|
+
this.capabilities = {
|
|
78
|
+
returning: options?.capabilities?.returning ?? false,
|
|
79
|
+
savepoints: options?.capabilities?.savepoints ?? true,
|
|
80
|
+
upsert: options?.capabilities?.upsert ?? true,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async execute(request: AdapterExecuteRequest): Promise<AdapterResult> {
|
|
85
|
+
if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
affectedRows: 0,
|
|
88
|
+
insertId: undefined,
|
|
89
|
+
rows: request.statement.returning ? [] : undefined,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let statement = compileMysqlStatement(request.statement)
|
|
94
|
+
let client = this.#resolveClient(request.transaction)
|
|
95
|
+
let [result] = await client.query(statement.text, statement.values)
|
|
96
|
+
|
|
97
|
+
if (isRowsResult(result)) {
|
|
98
|
+
let rows = normalizeRows(result)
|
|
99
|
+
|
|
100
|
+
if (request.statement.kind === 'count' || request.statement.kind === 'exists') {
|
|
101
|
+
rows = normalizeCountRows(rows)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { rows }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let header = normalizeHeader(result)
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
affectedRows: header.affectedRows,
|
|
111
|
+
insertId: normalizeInsertId(request.statement.kind, request.statement, header),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
|
|
116
|
+
let releaseOnClose = false
|
|
117
|
+
let connection: MysqlDatabaseConnection
|
|
118
|
+
|
|
119
|
+
if (isMysqlPool(this.#client)) {
|
|
120
|
+
connection = await this.#client.getConnection()
|
|
121
|
+
releaseOnClose = true
|
|
122
|
+
} else {
|
|
123
|
+
connection = this.#client
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (options?.isolationLevel) {
|
|
127
|
+
await connection.query('set transaction isolation level ' + options.isolationLevel)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options?.readOnly !== undefined) {
|
|
131
|
+
await connection.query(
|
|
132
|
+
options.readOnly ? 'set transaction read only' : 'set transaction read write',
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await connection.beginTransaction()
|
|
137
|
+
|
|
138
|
+
this.#transactionCounter += 1
|
|
139
|
+
let token = { id: 'tx_' + String(this.#transactionCounter) }
|
|
140
|
+
|
|
141
|
+
this.#transactions.set(token.id, {
|
|
142
|
+
connection,
|
|
143
|
+
releaseOnClose,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return token
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async commitTransaction(token: TransactionToken): Promise<void> {
|
|
150
|
+
let transaction = this.#transactions.get(token.id)
|
|
151
|
+
|
|
152
|
+
if (!transaction) {
|
|
153
|
+
throw new Error('Unknown transaction token: ' + token.id)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await transaction.connection.commit()
|
|
158
|
+
} finally {
|
|
159
|
+
this.#transactions.delete(token.id)
|
|
160
|
+
|
|
161
|
+
if (transaction.releaseOnClose) {
|
|
162
|
+
transaction.connection.release?.()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async rollbackTransaction(token: TransactionToken): Promise<void> {
|
|
168
|
+
let transaction = this.#transactions.get(token.id)
|
|
169
|
+
|
|
170
|
+
if (!transaction) {
|
|
171
|
+
throw new Error('Unknown transaction token: ' + token.id)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await transaction.connection.rollback()
|
|
176
|
+
} finally {
|
|
177
|
+
this.#transactions.delete(token.id)
|
|
178
|
+
|
|
179
|
+
if (transaction.releaseOnClose) {
|
|
180
|
+
transaction.connection.release?.()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async createSavepoint(token: TransactionToken, name: string): Promise<void> {
|
|
186
|
+
let connection = this.#transactionConnection(token)
|
|
187
|
+
await connection.query('savepoint ' + quoteIdentifier(name))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {
|
|
191
|
+
let connection = this.#transactionConnection(token)
|
|
192
|
+
await connection.query('rollback to savepoint ' + quoteIdentifier(name))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {
|
|
196
|
+
let connection = this.#transactionConnection(token)
|
|
197
|
+
await connection.query('release savepoint ' + quoteIdentifier(name))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#resolveClient(token: TransactionToken | undefined): MysqlDatabaseConnection | MysqlDatabasePool {
|
|
201
|
+
if (!token) {
|
|
202
|
+
return this.#client
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return this.#transactionConnection(token)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#transactionConnection(token: TransactionToken): MysqlDatabaseConnection {
|
|
209
|
+
let transaction = this.#transactions.get(token.id)
|
|
210
|
+
|
|
211
|
+
if (!transaction) {
|
|
212
|
+
throw new Error('Unknown transaction token: ' + token.id)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return transaction.connection
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Creates a mysql `DatabaseAdapter`.
|
|
221
|
+
* @param client Mysql pool or connection.
|
|
222
|
+
* @param options Optional adapter capability overrides.
|
|
223
|
+
* @returns A configured mysql adapter.
|
|
224
|
+
*/
|
|
225
|
+
export function createMysqlDatabaseAdapter(
|
|
226
|
+
client: MysqlQueryable,
|
|
227
|
+
options?: MysqlDatabaseAdapterOptions,
|
|
228
|
+
): MysqlDatabaseAdapter {
|
|
229
|
+
return new MysqlDatabaseAdapter(client, options)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isMysqlPool(client: MysqlQueryable): client is MysqlDatabasePool {
|
|
233
|
+
return typeof (client as MysqlDatabasePool).getConnection === 'function'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isRowsResult(result: unknown): result is MysqlQueryRows {
|
|
237
|
+
return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeRows(rows: MysqlQueryRows): Record<string, unknown>[] {
|
|
241
|
+
return rows.map((row) => ({ ...row }))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeHeader(result: unknown): MysqlQueryResultHeader {
|
|
245
|
+
if (typeof result === 'object' && result !== null) {
|
|
246
|
+
let header = result as { affectedRows?: unknown; insertId?: unknown }
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
affectedRows: typeof header.affectedRows === 'number' ? header.affectedRows : 0,
|
|
250
|
+
insertId: header.insertId,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
affectedRows: 0,
|
|
256
|
+
insertId: undefined,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unknown>[] {
|
|
261
|
+
return rows.map((row) => {
|
|
262
|
+
let count = row.count
|
|
263
|
+
|
|
264
|
+
if (typeof count === 'string') {
|
|
265
|
+
let numeric = Number(count)
|
|
266
|
+
|
|
267
|
+
if (!Number.isNaN(numeric)) {
|
|
268
|
+
return {
|
|
269
|
+
...row,
|
|
270
|
+
count: numeric,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (typeof count === 'bigint') {
|
|
276
|
+
return {
|
|
277
|
+
...row,
|
|
278
|
+
count: Number(count),
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return row
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeInsertId(
|
|
287
|
+
kind: AdapterExecuteRequest['statement']['kind'],
|
|
288
|
+
statement: AdapterExecuteRequest['statement'],
|
|
289
|
+
header: MysqlQueryResultHeader,
|
|
290
|
+
): unknown {
|
|
291
|
+
if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (getTablePrimaryKey(statement.table).length !== 1) {
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return header.insertId
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function quoteIdentifier(value: string): string {
|
|
303
|
+
return '`' + value.replace(/`/g, '``') + '`'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isInsertStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean {
|
|
307
|
+
return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isInsertStatement(
|
|
311
|
+
statement: AdapterExecuteRequest['statement'],
|
|
312
|
+
): statement is Extract<
|
|
313
|
+
AdapterExecuteRequest['statement'],
|
|
314
|
+
{ kind: 'insert' | 'insertMany' | 'upsert' }
|
|
315
|
+
> {
|
|
316
|
+
return (
|
|
317
|
+
statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert'
|
|
318
|
+
)
|
|
319
|
+
}
|