mysql2-core 0.3.8 → 0.3.10
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 +1558 -2
- package/lib/index.js +307 -543
- package/package.json +1 -1
- package/src/build.ts +15 -27
- package/src/index.ts +354 -542
- package/src/metadata.ts +7 -27
package/README.md
CHANGED
|
@@ -1,11 +1,1567 @@
|
|
|
1
1
|
# mysql2-core
|
|
2
2
|
|
|
3
|
+
A lightweight TypeScript database abstraction for [mysql2](https://www.npmjs.com/package/mysql2), which has 4 main parts:
|
|
4
|
+
- A unified API for connection pools, transactions, SQL execution, queries, result mapping, batch execution, and common database utilities.
|
|
5
|
+
- Utilities for building and executing MySQL statements with [`mysql2`](https://www.npmjs.com/package/mysql2).
|
|
6
|
+
- A metadata-driven persistence layer with support for single records, batch operations, buffered streaming writes, transactions, and configurable value mapping.
|
|
7
|
+
- Health check
|
|
8
|
+
### Example
|
|
9
|
+
- [sql-modular-sample](https://github.com/source-code-template/sql-modular-sample): RESI API with express and MySQL
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
* MySQL connection pool management through `mysql2`
|
|
14
|
+
* Unified `Executor` abstraction for database operations
|
|
15
|
+
* Transaction support with automatic connection release
|
|
16
|
+
* Batch SQL execution
|
|
17
|
+
* Optional transactional batch execution
|
|
18
|
+
* Scalar and single-row queries
|
|
19
|
+
* Result-field mapping
|
|
20
|
+
* Boolean value conversion
|
|
21
|
+
* Dynamic SQL field helpers
|
|
22
|
+
* Support for JSON/string conversion of object parameters
|
|
23
|
+
* MySQL duplicate-key error normalization
|
|
24
|
+
* Compatible APIs for both `Pool` and `PoolConnection`
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install mysql2 mysql2-core
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
`mysql2-core` separates database operations from the underlying MySQL connection implementation.
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Executor
|
|
38
|
+
│
|
|
39
|
+
┌─────────┴─────────┐
|
|
40
|
+
│ │
|
|
41
|
+
DB Transaction
|
|
42
|
+
│ │
|
|
43
|
+
PoolManager PoolConnectionManager
|
|
44
|
+
│ │
|
|
45
|
+
Pool PoolConnection
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Executor
|
|
49
|
+
|
|
50
|
+
`Executor` defines the common database operations:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
interface Executor {
|
|
54
|
+
driver: string
|
|
55
|
+
param(i: number): string
|
|
56
|
+
execute(sql: string, args?: any[], ctx?: any): Promise<number>
|
|
57
|
+
executeBatch(statements: Statement[], firstAffected?: boolean, ctx?: any): Promise<number>
|
|
58
|
+
query<T>(sql: string, args?: any[], m?: StringMap, bools?: Attribute[], ctx?: any): Promise<T[]>
|
|
59
|
+
queryOne<T>(sql: string, args?: any[], m?: StringMap, bools?: Attribute[], ctx?: any): Promise<T | null>
|
|
60
|
+
executeScalar<T>(sql: string, args?: any[], ctx?: any): Promise<T | null>
|
|
61
|
+
count(sql: string, args?: any[], ctx?: any): Promise<number>
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### DB
|
|
66
|
+
|
|
67
|
+
`DB` extends `Executor` with transaction creation:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
interface DB extends Executor {
|
|
71
|
+
beginTransaction(): Promise<Transaction>
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Transaction
|
|
76
|
+
|
|
77
|
+
`Transaction` extends `Executor` with:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
interface Transaction extends Executor {
|
|
81
|
+
commit(): Promise<void>
|
|
82
|
+
rollback(): Promise<void>
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Creating a Connection Pool
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { createPool } from "mysql2-core"
|
|
90
|
+
|
|
91
|
+
const pool = createPool({
|
|
92
|
+
host: "localhost",
|
|
93
|
+
port: 3306,
|
|
94
|
+
database: "app",
|
|
95
|
+
user: "root",
|
|
96
|
+
password: "password",
|
|
97
|
+
connectionLimit: 10
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`createPool()` creates a `mysql2` pool with `rowsAsArray: true`.
|
|
102
|
+
|
|
103
|
+
The library also supports the `max` option as an alias for `connectionLimit` when `connectionLimit` is not explicitly provided.
|
|
104
|
+
|
|
105
|
+
## Using `PoolManager`
|
|
106
|
+
|
|
107
|
+
`PoolManager` provides the `DB` implementation for a MySQL connection pool.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createPool, PoolManager} from "mysql2-core"
|
|
111
|
+
|
|
112
|
+
const pool = createPool({
|
|
113
|
+
host: "localhost",
|
|
114
|
+
database: "app",
|
|
115
|
+
user: "root",
|
|
116
|
+
password: "password"
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const db = new PoolManager(pool)
|
|
120
|
+
|
|
121
|
+
const users = await db.query(
|
|
122
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
123
|
+
[1]
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The MySQL driver identifier is:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
db.driver
|
|
131
|
+
// "mysql"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Parameter placeholders are represented by:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
db.param(0)
|
|
138
|
+
// "?"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Execute SQL
|
|
142
|
+
|
|
143
|
+
`execute()` executes a parameterized statement and returns the affected-row count.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const affectedRows = await db.execute(
|
|
147
|
+
"UPDATE users SET name = ? WHERE id = ?",
|
|
148
|
+
["John", 10]
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For an `INSERT`, the returned value is the number of affected rows reported by MySQL.
|
|
153
|
+
|
|
154
|
+
## Query Rows
|
|
155
|
+
|
|
156
|
+
Use `query()` when multiple rows are expected.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const users = await db.query<User>(
|
|
160
|
+
"SELECT id, name FROM users WHERE active = ?",
|
|
161
|
+
[1]
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The result is converted from MySQL's array-row representation into objects using the returned field metadata.
|
|
166
|
+
|
|
167
|
+
For example:
|
|
168
|
+
|
|
169
|
+
```sql
|
|
170
|
+
SELECT id, name FROM users
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
is represented as:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
[
|
|
177
|
+
{
|
|
178
|
+
id: 1,
|
|
179
|
+
name: "John"
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Query One Row
|
|
185
|
+
|
|
186
|
+
`queryOne()` returns the first row or `null` when no row exists.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const user = await db.queryOne<User>(
|
|
190
|
+
"SELECT id, name FROM users WHERE id = ?",
|
|
191
|
+
[10]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (!user) {
|
|
195
|
+
// user does not exist
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Execute Scalar
|
|
200
|
+
|
|
201
|
+
`executeScalar()` returns the first column of the first row.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const username = await db.executeScalar<string>(
|
|
205
|
+
"SELECT name FROM users WHERE id = ?",
|
|
206
|
+
[10]
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
If there is no row, it returns `null`.
|
|
211
|
+
|
|
212
|
+
## Count
|
|
213
|
+
|
|
214
|
+
`count()` is a convenience method for scalar count queries.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const total = await db.count(
|
|
218
|
+
"SELECT COUNT(*) FROM users WHERE active = ?",
|
|
219
|
+
[1]
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
A missing scalar result is returned as `0`.
|
|
224
|
+
|
|
225
|
+
## Transactions
|
|
226
|
+
|
|
227
|
+
Create a transaction with:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const tx = await db.beginTransaction()
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await tx.execute(
|
|
234
|
+
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
|
|
235
|
+
[100, 1]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
await tx.execute(
|
|
239
|
+
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
|
|
240
|
+
[100, 2]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
await tx.commit()
|
|
244
|
+
} catch (err) {
|
|
245
|
+
await tx.rollback()
|
|
246
|
+
throw err
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`PoolManager.beginTransaction()` obtains a connection from the pool, starts a MySQL transaction, and returns a `PoolConnectionManager`.
|
|
251
|
+
|
|
252
|
+
The connection is released when `commit()` or `rollback()` completes.
|
|
253
|
+
|
|
254
|
+
## Batch Execution
|
|
255
|
+
|
|
256
|
+
Statements are represented by:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
interface Statement {
|
|
260
|
+
query: string
|
|
261
|
+
params?: any[]
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const statements = [
|
|
269
|
+
{
|
|
270
|
+
query: "UPDATE users SET active = ? WHERE id = ?",
|
|
271
|
+
params: [1, 10]
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
query: "INSERT INTO logs(user_id, action) VALUES (?, ?)",
|
|
275
|
+
params: [10, "activate"]
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
const affectedRows = await db.executeBatch(statements)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
For multiple statements, the pool-level `executeBatch()` obtains a connection and executes the batch inside a transaction.
|
|
283
|
+
|
|
284
|
+
The connection is released after completion.
|
|
285
|
+
|
|
286
|
+
## `firstAffected`
|
|
287
|
+
|
|
288
|
+
`executeBatch()` supports an optional `firstAffected` flag:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
await db.executeBatch(statements, true)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
When `firstAffected` is `true`, the first statement determines whether the remaining statements are executed.
|
|
295
|
+
|
|
296
|
+
If the first statement affects at least one row:
|
|
297
|
+
|
|
298
|
+
```text
|
|
299
|
+
statement 1
|
|
300
|
+
│
|
|
301
|
+
├── rowCount > 0
|
|
302
|
+
│
|
|
303
|
+
▼
|
|
304
|
+
statement 2
|
|
305
|
+
↓
|
|
306
|
+
statement 3
|
|
307
|
+
↓
|
|
308
|
+
...
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If the first statement affects zero rows:
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
statement 1
|
|
315
|
+
│
|
|
316
|
+
└── rowCount = 0
|
|
317
|
+
↓
|
|
318
|
+
stop remaining statements
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The batch still completes its transaction lifecycle.
|
|
322
|
+
|
|
323
|
+
For example:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const statements = [
|
|
327
|
+
{
|
|
328
|
+
query: "UPDATE users SET active = ? WHERE id = ?",
|
|
329
|
+
params: [1, 10]
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
query: "INSERT INTO audit_logs(user_id, action) VALUES (?, ?)",
|
|
333
|
+
params: [10, "activate"]
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
await db.executeBatch(statements, true)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
This allows subsequent operations to depend on the first statement modifying at least one row.
|
|
341
|
+
|
|
342
|
+
## Multiple Statements
|
|
343
|
+
|
|
344
|
+
When:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
resource.multipleStatements = true
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
the batch implementation can combine multiple SQL statements into a single MySQL query.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { resource } from "mysql2-core"
|
|
354
|
+
|
|
355
|
+
resource.multipleStatements = true
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Each statement is terminated with a semicolon when necessary, and parameters are flattened into a single parameter array.
|
|
359
|
+
|
|
360
|
+
## Direct Connection Operations
|
|
361
|
+
|
|
362
|
+
The library also exposes functions that work directly with a `Pool` or `PoolConnection`.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import {
|
|
366
|
+
execute,
|
|
367
|
+
query,
|
|
368
|
+
queryOne,
|
|
369
|
+
executeScalar,
|
|
370
|
+
count
|
|
371
|
+
} from "mysql2-core"
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
const rows = await query<User>(
|
|
378
|
+
pool,
|
|
379
|
+
"SELECT id, name FROM users"
|
|
380
|
+
)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For a transaction connection:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
const rows = await query<User>(
|
|
387
|
+
connection,
|
|
388
|
+
"SELECT id, name FROM users"
|
|
389
|
+
)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Connection Helpers
|
|
393
|
+
|
|
394
|
+
The library provides promise-based wrappers around MySQL connection operations.
|
|
395
|
+
|
|
396
|
+
### Get a connection
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { getConnection } from "mysql2-core"
|
|
400
|
+
|
|
401
|
+
const connection = await getConnection(pool)
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
// use connection
|
|
405
|
+
} finally {
|
|
406
|
+
connection.release()
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Begin a transaction
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { beginTransaction } from "mysql2-core"
|
|
414
|
+
|
|
415
|
+
await beginTransaction(connection)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Commit
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { commit } from "mysql2-core"
|
|
422
|
+
|
|
423
|
+
await commit(connection)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Rollback
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { rollback } from "mysql2-core"
|
|
430
|
+
|
|
431
|
+
await rollback(connection)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Result Mapping
|
|
435
|
+
|
|
436
|
+
`query()` accepts a field mapping:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
const users = await db.query<User>(
|
|
440
|
+
"SELECT user_id, user_name FROM users",
|
|
441
|
+
[],
|
|
442
|
+
{
|
|
443
|
+
user_id: "id",
|
|
444
|
+
user_name: "name"
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The result becomes:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
[
|
|
453
|
+
{
|
|
454
|
+
id: 1,
|
|
455
|
+
name: "John"
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
The mapping is performed by `mapArray()`.
|
|
461
|
+
|
|
462
|
+
## Boolean Conversion
|
|
463
|
+
|
|
464
|
+
Boolean fields can be converted using `Attribute`.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
const users = await db.query<User>(
|
|
468
|
+
"SELECT id, active FROM users",
|
|
469
|
+
[],
|
|
470
|
+
undefined,
|
|
471
|
+
[
|
|
472
|
+
{
|
|
473
|
+
name: "active"
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Without an explicit `true` value, the following values are interpreted as `true`:
|
|
480
|
+
|
|
481
|
+
```text
|
|
482
|
+
1
|
|
483
|
+
T
|
|
484
|
+
Y
|
|
485
|
+
true
|
|
486
|
+
on
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Other non-null values are interpreted as `false`.
|
|
490
|
+
|
|
491
|
+
A custom true value can also be provided:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
[
|
|
495
|
+
{
|
|
496
|
+
name: "active",
|
|
497
|
+
true: "Y"
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Parameter Conversion
|
|
503
|
+
|
|
504
|
+
Parameters are normalized by `toArray()`.
|
|
505
|
+
|
|
506
|
+
`undefined` and `null` are converted to SQL `NULL` parameters.
|
|
507
|
+
|
|
508
|
+
`Date` values are preserved as `Date` instances.
|
|
509
|
+
|
|
510
|
+
Objects can optionally be converted to JSON strings:
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
resource.string = true
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
For example:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
await db.execute(
|
|
520
|
+
"INSERT INTO documents(data) VALUES (?)",
|
|
521
|
+
[{ name: "John" }]
|
|
522
|
+
)
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
With `resource.string = true`, the object is serialized using `JSON.stringify()`.
|
|
526
|
+
|
|
527
|
+
## Dynamic SQL Field Helpers
|
|
528
|
+
|
|
529
|
+
### `getFields()`
|
|
530
|
+
|
|
531
|
+
`getFields()` optionally filters a requested field list against an allowed list.
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
const fields = getFields(
|
|
535
|
+
["id", "name"],
|
|
536
|
+
["id", "name", "email"]
|
|
537
|
+
)
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### `buildFields()`
|
|
541
|
+
|
|
542
|
+
`buildFields()` produces a comma-separated SQL field list.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
const sqlFields = buildFields(
|
|
546
|
+
["id", "name"],
|
|
547
|
+
["id", "name", "email"]
|
|
548
|
+
)
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Result:
|
|
552
|
+
|
|
553
|
+
```text
|
|
554
|
+
id,name
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
When no valid fields are available, `buildFields()` returns:
|
|
558
|
+
|
|
559
|
+
```text
|
|
560
|
+
*
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### `getMapField()`
|
|
564
|
+
|
|
565
|
+
`getMapField()` returns a mapped field name when a mapping exists:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
const field = getMapField(
|
|
569
|
+
"user_id",
|
|
570
|
+
{
|
|
571
|
+
user_id: "id"
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Result:
|
|
577
|
+
|
|
578
|
+
```text
|
|
579
|
+
id
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## Error Handling
|
|
583
|
+
|
|
584
|
+
Database errors are passed through `buildError()`.
|
|
585
|
+
|
|
586
|
+
MySQL duplicate-key errors matching:
|
|
587
|
+
|
|
588
|
+
```text
|
|
589
|
+
errno = 1062
|
|
590
|
+
code = ER_DUP_ENTRY
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
are additionally marked with:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
error = "duplicate"
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
This allows application code to identify duplicate-key errors without depending entirely on the MySQL error code.
|
|
600
|
+
|
|
601
|
+
Example:
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
try {
|
|
605
|
+
await db.execute(
|
|
606
|
+
"INSERT INTO users(email) VALUES (?)",
|
|
607
|
+
["john@example.com"]
|
|
608
|
+
)
|
|
609
|
+
} catch (err: any) {
|
|
610
|
+
if (err.error === "duplicate") {
|
|
611
|
+
// duplicate record
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
throw err
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## API Overview
|
|
619
|
+
|
|
620
|
+
### Interfaces
|
|
621
|
+
|
|
622
|
+
```text
|
|
623
|
+
StringMap
|
|
624
|
+
Statement
|
|
625
|
+
Executor
|
|
626
|
+
Transaction
|
|
627
|
+
DB
|
|
628
|
+
Attribute
|
|
629
|
+
Attributes
|
|
630
|
+
Config
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Classes
|
|
634
|
+
|
|
635
|
+
```text
|
|
636
|
+
resource
|
|
637
|
+
PoolManager
|
|
638
|
+
PoolConnectionManager
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Main functions
|
|
642
|
+
|
|
643
|
+
```text
|
|
644
|
+
createPool()
|
|
645
|
+
getConnection()
|
|
646
|
+
beginTransaction()
|
|
647
|
+
commit()
|
|
648
|
+
rollback()
|
|
649
|
+
|
|
650
|
+
execute()
|
|
651
|
+
executeBatch()
|
|
652
|
+
executeBatchConnectionTx()
|
|
653
|
+
executeBatchConnection()
|
|
654
|
+
|
|
655
|
+
query()
|
|
656
|
+
queryOne()
|
|
657
|
+
executeScalar()
|
|
658
|
+
count()
|
|
659
|
+
|
|
660
|
+
toArray()
|
|
661
|
+
handleResults()
|
|
662
|
+
handleBool()
|
|
663
|
+
formatData()
|
|
664
|
+
mapArray()
|
|
665
|
+
|
|
666
|
+
getFields()
|
|
667
|
+
buildFields()
|
|
668
|
+
getMapField()
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Example
|
|
672
|
+
|
|
673
|
+
A simple repository-style implementation can be built on top of `DB`:
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
import { DB } from "mysql2-core"
|
|
677
|
+
|
|
678
|
+
export interface User {
|
|
679
|
+
id: number
|
|
680
|
+
name: string
|
|
681
|
+
active: boolean
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export class UserRepository {
|
|
685
|
+
constructor(private readonly db: DB) {}
|
|
686
|
+
|
|
687
|
+
async findById(id: number): Promise<User | null> {
|
|
688
|
+
return this.db.queryOne<User>(
|
|
689
|
+
`SELECT id, name, active FROM users WHERE id = ?`,
|
|
690
|
+
[id],
|
|
691
|
+
undefined,
|
|
692
|
+
[
|
|
693
|
+
{
|
|
694
|
+
name: "active"
|
|
695
|
+
}
|
|
696
|
+
]
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async countActive(): Promise<number> {
|
|
701
|
+
return this.db.count(
|
|
702
|
+
"SELECT COUNT(*) FROM users WHERE active = ?",
|
|
703
|
+
[1]
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async updateName(id: number, name: string): Promise<number> {
|
|
708
|
+
return this.db.execute(
|
|
709
|
+
"UPDATE users SET name = ? WHERE id = ?",
|
|
710
|
+
[name, id]
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
## SQL Builder
|
|
717
|
+
|
|
718
|
+
The library is built around three main concepts:
|
|
719
|
+
|
|
720
|
+
```text
|
|
721
|
+
Attributes
|
|
722
|
+
│
|
|
723
|
+
▼
|
|
724
|
+
SQL Builder
|
|
725
|
+
│
|
|
726
|
+
▼
|
|
727
|
+
Statement
|
|
728
|
+
│
|
|
729
|
+
▼
|
|
730
|
+
mysql2
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
The high-level writers build on top of this:
|
|
734
|
+
|
|
735
|
+
```text
|
|
736
|
+
mysql2-core
|
|
737
|
+
│
|
|
738
|
+
┌────────────┼────────────┐
|
|
739
|
+
│ │ │
|
|
740
|
+
Writer BatchWriter StreamWriter
|
|
741
|
+
│ │ │
|
|
742
|
+
└────────────┴────────────┘
|
|
743
|
+
│
|
|
744
|
+
SQL / Execution
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
It is intentionally a small persistence utility rather than a full ORM.
|
|
748
|
+
|
|
749
|
+
## Attributes
|
|
750
|
+
|
|
751
|
+
The persistence mapping is described using `Attributes`.
|
|
752
|
+
|
|
753
|
+
```ts
|
|
754
|
+
import type { Attributes } from "mysql2-core"
|
|
755
|
+
|
|
756
|
+
const attributes: Attributes = {
|
|
757
|
+
id: {
|
|
758
|
+
type: "integer",
|
|
759
|
+
key: true
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
name: {
|
|
763
|
+
type: "string"
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
active: {
|
|
767
|
+
type: "boolean"
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
An `Attribute` supports:
|
|
773
|
+
|
|
774
|
+
| Property | Description |
|
|
775
|
+
| ---------- | --------------------------------------- |
|
|
776
|
+
| `column` | Database column name |
|
|
777
|
+
| `type` | Declared data type |
|
|
778
|
+
| `default` | Default value |
|
|
779
|
+
| `key` | Marks a key attribute |
|
|
780
|
+
| `noinsert` | Excludes the attribute from inserts |
|
|
781
|
+
| `noupdate` | Excludes the attribute from updates |
|
|
782
|
+
| `version` | Marks the version attribute |
|
|
783
|
+
| `ignored` | Excludes the attribute from persistence |
|
|
784
|
+
| `true` | Value used when a boolean is `true` |
|
|
785
|
+
| `false` | Value used when a boolean is `false` |
|
|
786
|
+
|
|
787
|
+
The supported `DataType` values include:
|
|
788
|
+
|
|
789
|
+
```text
|
|
790
|
+
ObjectId
|
|
791
|
+
date
|
|
792
|
+
datetime
|
|
793
|
+
time
|
|
794
|
+
boolean
|
|
795
|
+
number
|
|
796
|
+
integer
|
|
797
|
+
string
|
|
798
|
+
text
|
|
799
|
+
object
|
|
800
|
+
array
|
|
801
|
+
binary
|
|
802
|
+
primitives
|
|
803
|
+
booleans
|
|
804
|
+
numbers
|
|
805
|
+
integers
|
|
806
|
+
strings
|
|
807
|
+
dates
|
|
808
|
+
datetimes
|
|
809
|
+
times
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
## Column Mapping
|
|
813
|
+
|
|
814
|
+
A property can use a different database column name.
|
|
815
|
+
|
|
816
|
+
```ts
|
|
817
|
+
const attributes: Attributes = {
|
|
818
|
+
userId: {
|
|
819
|
+
column: "user_id",
|
|
820
|
+
type: "integer",
|
|
821
|
+
key: true
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
firstName: {
|
|
825
|
+
column: "first_name",
|
|
826
|
+
type: "string"
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
This maps:
|
|
832
|
+
|
|
833
|
+
```text
|
|
834
|
+
userId → user_id
|
|
835
|
+
firstName → first_name
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## SQL Generation
|
|
839
|
+
|
|
840
|
+
### Parameters
|
|
841
|
+
|
|
842
|
+
The default parameter builder returns `?`:
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
import { param, params } from "mysql2-core"
|
|
846
|
+
|
|
847
|
+
param(1)
|
|
848
|
+
// ?
|
|
849
|
+
|
|
850
|
+
params(3)
|
|
851
|
+
// ["?", "?", "?"]
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
A custom parameter builder can be supplied:
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
const buildParam = (i: number) => `$${i}`
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
This allows SQL generation to use a different parameter syntax when necessary.
|
|
861
|
+
|
|
862
|
+
### Statements
|
|
863
|
+
|
|
864
|
+
The library represents generated SQL as:
|
|
865
|
+
|
|
866
|
+
```ts
|
|
867
|
+
interface Statement {
|
|
868
|
+
query: string
|
|
869
|
+
params?: any[]
|
|
870
|
+
}
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
For example:
|
|
874
|
+
|
|
875
|
+
```text
|
|
876
|
+
{
|
|
877
|
+
query: "insert into users(id,name)values(?,?)",
|
|
878
|
+
params: [1, "John"]
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
## Insert and Upsert
|
|
883
|
+
|
|
884
|
+
`buildToSave()` generates an `INSERT` statement when no key attributes are defined.
|
|
885
|
+
|
|
886
|
+
```sql
|
|
887
|
+
insert into users(name,active)values(?,?)
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
When one or more attributes are marked with `key: true`, the generated statement uses MySQL's duplicate-key update syntax:
|
|
891
|
+
|
|
892
|
+
```sql
|
|
893
|
+
insert into users(id,name)values(?,?) on duplicate key update name=?
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
For example:
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
const attributes: Attributes = {
|
|
900
|
+
id: {
|
|
901
|
+
type: "integer",
|
|
902
|
+
key: true
|
|
903
|
+
},
|
|
904
|
+
|
|
905
|
+
name: {
|
|
906
|
+
type: "string"
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
This allows the same metadata to represent both insert and upsert behavior.
|
|
912
|
+
|
|
913
|
+
## Boolean Values
|
|
914
|
+
|
|
915
|
+
Boolean values support both string and numeric representations.
|
|
916
|
+
|
|
917
|
+
The default representation is:
|
|
918
|
+
|
|
919
|
+
```text
|
|
920
|
+
true → "1"
|
|
921
|
+
false → "0"
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
For example:
|
|
925
|
+
|
|
926
|
+
```ts
|
|
927
|
+
const attributes: Attributes = {
|
|
928
|
+
active: {
|
|
929
|
+
type: "boolean"
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
The values are still passed through MySQL parameters.
|
|
935
|
+
|
|
936
|
+
Custom string values are supported:
|
|
937
|
+
|
|
938
|
+
```ts
|
|
939
|
+
const attributes: Attributes = {
|
|
940
|
+
active: {
|
|
941
|
+
type: "boolean",
|
|
942
|
+
true: "Y",
|
|
943
|
+
false: "N"
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
Numeric values are supported as well:
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
const attributes: Attributes = {
|
|
952
|
+
active: {
|
|
953
|
+
type: "boolean",
|
|
954
|
+
true: 1,
|
|
955
|
+
false: 0
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
Thus `Attribute.true` and `Attribute.false` can be either `string` or `number`.
|
|
961
|
+
|
|
962
|
+
## Default Values
|
|
963
|
+
|
|
964
|
+
An attribute can define a default value:
|
|
965
|
+
|
|
966
|
+
```ts
|
|
967
|
+
const attributes: Attributes = {
|
|
968
|
+
status: {
|
|
969
|
+
type: "string",
|
|
970
|
+
default: "active"
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
When a value is not provided, the configured default can be used when constructing the insert statement.
|
|
976
|
+
|
|
977
|
+
## Version
|
|
978
|
+
|
|
979
|
+
An attribute can be marked as a version field:
|
|
980
|
+
|
|
981
|
+
```ts
|
|
982
|
+
const attributes: Attributes = {
|
|
983
|
+
id: {
|
|
984
|
+
type: "integer",
|
|
985
|
+
key: true
|
|
986
|
+
},
|
|
987
|
+
|
|
988
|
+
name: {
|
|
989
|
+
type: "string"
|
|
990
|
+
},
|
|
991
|
+
|
|
992
|
+
version: {
|
|
993
|
+
type: "integer",
|
|
994
|
+
version: true
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
When building a new record, the version field is initialized to `1`.
|
|
1000
|
+
|
|
1001
|
+
The source provides version-field identification and initialization; it does not implement a complete optimistic-locking mechanism.
|
|
1002
|
+
|
|
1003
|
+
## Ignored Fields
|
|
1004
|
+
|
|
1005
|
+
Use `ignored` to exclude a property from persistence:
|
|
1006
|
+
|
|
1007
|
+
```ts
|
|
1008
|
+
const attributes: Attributes = {
|
|
1009
|
+
id: {
|
|
1010
|
+
type: "integer",
|
|
1011
|
+
key: true
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
name: {
|
|
1015
|
+
type: "string"
|
|
1016
|
+
},
|
|
1017
|
+
|
|
1018
|
+
temporaryValue: {
|
|
1019
|
+
ignored: true
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
## Insert and Update Controls
|
|
1025
|
+
|
|
1026
|
+
### `noinsert`
|
|
1027
|
+
|
|
1028
|
+
Prevent an attribute from being included in an insert:
|
|
1029
|
+
|
|
1030
|
+
```ts
|
|
1031
|
+
const attributes: Attributes = {
|
|
1032
|
+
id: {
|
|
1033
|
+
type: "integer",
|
|
1034
|
+
key: true,
|
|
1035
|
+
noinsert: true
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### `noupdate`
|
|
1041
|
+
|
|
1042
|
+
Prevent an attribute from being included in duplicate-key updates:
|
|
1043
|
+
|
|
1044
|
+
```ts
|
|
1045
|
+
const attributes: Attributes = {
|
|
1046
|
+
createdAt: {
|
|
1047
|
+
type: "datetime",
|
|
1048
|
+
noupdate: true
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
## `MySQLWriter`
|
|
1054
|
+
|
|
1055
|
+
`MySQLWriter<T>` writes one object.
|
|
1056
|
+
|
|
1057
|
+
```ts
|
|
1058
|
+
import { MySQLWriter } from "mysql2-core"
|
|
1059
|
+
|
|
1060
|
+
const writer = new MySQLWriter(pool, "users", attributes)
|
|
1061
|
+
|
|
1062
|
+
const count = await writer.write({
|
|
1063
|
+
id: 1,
|
|
1064
|
+
name: "John",
|
|
1065
|
+
active: true
|
|
1066
|
+
})
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
The operation is:
|
|
1070
|
+
|
|
1071
|
+
```text
|
|
1072
|
+
write()
|
|
1073
|
+
│
|
|
1074
|
+
├── map()
|
|
1075
|
+
│
|
|
1076
|
+
▼
|
|
1077
|
+
buildToSave()
|
|
1078
|
+
│
|
|
1079
|
+
▼
|
|
1080
|
+
execute()
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Mapping
|
|
1084
|
+
|
|
1085
|
+
A mapping function can transform the object before the SQL statement is created:
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
const writer = new MySQLWriter(
|
|
1089
|
+
pool,
|
|
1090
|
+
"users",
|
|
1091
|
+
attributes,
|
|
1092
|
+
false,
|
|
1093
|
+
(user) => ({
|
|
1094
|
+
...user,
|
|
1095
|
+
name: user.name.trim()
|
|
1096
|
+
})
|
|
1097
|
+
)
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
### `oneIfSuccess`
|
|
1101
|
+
|
|
1102
|
+
When `oneIfSuccess` is enabled, the result is normalized to `0` or `1`:
|
|
1103
|
+
|
|
1104
|
+
```ts
|
|
1105
|
+
const writer = new MySQLWriter(
|
|
1106
|
+
pool,
|
|
1107
|
+
"users",
|
|
1108
|
+
attributes,
|
|
1109
|
+
true
|
|
1110
|
+
)
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
The result becomes:
|
|
1114
|
+
|
|
1115
|
+
```text
|
|
1116
|
+
0 → zero affected rows
|
|
1117
|
+
1 → one or more affected rows
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
## `MySQLBatchWriter`
|
|
1121
|
+
|
|
1122
|
+
`MySQLBatchWriter<T>` writes an array of objects.
|
|
1123
|
+
|
|
1124
|
+
```ts
|
|
1125
|
+
import { MySQLBatchWriter } from "mysql2-core"
|
|
1126
|
+
|
|
1127
|
+
const writer = new MySQLBatchWriter(pool, "users", attributes)
|
|
1128
|
+
|
|
1129
|
+
const count = await writer.write([
|
|
1130
|
+
{
|
|
1131
|
+
id: 1,
|
|
1132
|
+
name: "John",
|
|
1133
|
+
active: true
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: 2,
|
|
1137
|
+
name: "Jane",
|
|
1138
|
+
active: true
|
|
1139
|
+
}
|
|
1140
|
+
])
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
The objects are converted into multiple statements using `buildToSaveBatch()` and executed by the batch execution layer.
|
|
1144
|
+
|
|
1145
|
+
## `MySQLStreamWriter`
|
|
1146
|
+
|
|
1147
|
+
`MySQLStreamWriter<T>` buffers objects and executes them when the configured buffer size is reached.
|
|
1148
|
+
|
|
1149
|
+
```ts
|
|
1150
|
+
import { MySQLStreamWriter } from "mysql2-core"
|
|
1151
|
+
|
|
1152
|
+
const writer = new MySQLStreamWriter(pool, "users", attributes, 1000)
|
|
1153
|
+
|
|
1154
|
+
for (const user of users) {
|
|
1155
|
+
await writer.write(user)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
await writer.flush()
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
The default size is `1000`.
|
|
1162
|
+
|
|
1163
|
+
The execution flow is:
|
|
1164
|
+
|
|
1165
|
+
```text
|
|
1166
|
+
write()
|
|
1167
|
+
│
|
|
1168
|
+
▼
|
|
1169
|
+
buffer
|
|
1170
|
+
│
|
|
1171
|
+
├── buffer < size ──► wait
|
|
1172
|
+
│
|
|
1173
|
+
└── buffer >= size
|
|
1174
|
+
│
|
|
1175
|
+
▼
|
|
1176
|
+
flush()
|
|
1177
|
+
│
|
|
1178
|
+
▼
|
|
1179
|
+
buildToSaveBatch()
|
|
1180
|
+
│
|
|
1181
|
+
▼
|
|
1182
|
+
executeBatch()
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
The final buffered records must be flushed explicitly:
|
|
1186
|
+
|
|
1187
|
+
```ts
|
|
1188
|
+
await writer.flush()
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
## Execution
|
|
1192
|
+
|
|
1193
|
+
### `execute`
|
|
1194
|
+
|
|
1195
|
+
Execute one SQL statement and return its affected-row count:
|
|
1196
|
+
|
|
1197
|
+
```ts
|
|
1198
|
+
import { execute } from "mysql2-core"
|
|
1199
|
+
|
|
1200
|
+
const count = await execute(
|
|
1201
|
+
pool,
|
|
1202
|
+
"update users set name=? where id=?",
|
|
1203
|
+
["John", 1]
|
|
1204
|
+
)
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
`execute()` accepts either a `Pool` or `PoolConnection`.
|
|
1208
|
+
|
|
1209
|
+
## Batch Execution
|
|
1210
|
+
|
|
1211
|
+
### `executeBatch`
|
|
1212
|
+
|
|
1213
|
+
`executeBatch()` obtains a connection, runs the batch in a transaction, and releases the connection afterward.
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
import { executeBatch } from "mysql2-core"
|
|
1217
|
+
|
|
1218
|
+
const count = await executeBatch(
|
|
1219
|
+
pool,
|
|
1220
|
+
statements
|
|
1221
|
+
)
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
Its lifecycle is:
|
|
1225
|
+
|
|
1226
|
+
```text
|
|
1227
|
+
getConnection()
|
|
1228
|
+
│
|
|
1229
|
+
▼
|
|
1230
|
+
beginTransaction()
|
|
1231
|
+
│
|
|
1232
|
+
▼
|
|
1233
|
+
execute statements
|
|
1234
|
+
│
|
|
1235
|
+
├── success ──► commit()
|
|
1236
|
+
│
|
|
1237
|
+
└── error ────► rollback()
|
|
1238
|
+
│
|
|
1239
|
+
▼
|
|
1240
|
+
connection.release()
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
The connection is released even when execution fails.
|
|
1244
|
+
|
|
1245
|
+
### `executeBatchConnectionTx`
|
|
1246
|
+
|
|
1247
|
+
Use `executeBatchConnectionTx()` when the caller already owns a `PoolConnection` but wants the function to manage the transaction.
|
|
1248
|
+
|
|
1249
|
+
```ts
|
|
1250
|
+
await executeBatchConnectionTx(
|
|
1251
|
+
connection,
|
|
1252
|
+
statements
|
|
1253
|
+
)
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### `executeBatchConnection`
|
|
1257
|
+
|
|
1258
|
+
Use `executeBatchConnection()` when the caller owns both the connection and transaction lifecycle.
|
|
1259
|
+
|
|
1260
|
+
```ts
|
|
1261
|
+
await executeBatchConnection(
|
|
1262
|
+
connection,
|
|
1263
|
+
statements
|
|
1264
|
+
)
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
This allows the statements to participate in an existing transaction.
|
|
1268
|
+
|
|
1269
|
+
## Transactions
|
|
1270
|
+
|
|
1271
|
+
The library exposes transaction helpers:
|
|
1272
|
+
|
|
1273
|
+
```ts
|
|
1274
|
+
import { beginTransaction, commit, rollback } from "mysql2-core"
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
Example:
|
|
1278
|
+
|
|
1279
|
+
```ts
|
|
1280
|
+
const connection = await getConnection(pool)
|
|
1281
|
+
|
|
1282
|
+
try {
|
|
1283
|
+
await beginTransaction(connection)
|
|
1284
|
+
|
|
1285
|
+
await execute(
|
|
1286
|
+
connection,
|
|
1287
|
+
"insert into users(name) values(?)",
|
|
1288
|
+
["John"]
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
await commit(connection)
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
await rollback(connection)
|
|
1294
|
+
throw err
|
|
1295
|
+
} finally {
|
|
1296
|
+
connection.release()
|
|
1297
|
+
}
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
## Multiple Statements
|
|
1301
|
+
|
|
1302
|
+
The batch transaction executor can optionally combine multiple statements into a single MySQL request.
|
|
1303
|
+
|
|
1304
|
+
Enable:
|
|
1305
|
+
|
|
1306
|
+
```ts
|
|
1307
|
+
import { resource } from "mysql2-core"
|
|
1308
|
+
|
|
1309
|
+
resource.multipleStatements = true
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
The statements are combined and executed through `connection.query()`.
|
|
1313
|
+
|
|
1314
|
+
This can reduce the number of database round trips for large batches.
|
|
1315
|
+
|
|
1316
|
+
## Serialization
|
|
1317
|
+
|
|
1318
|
+
`toArray()` prepares parameter values before they are passed to `mysql2`.
|
|
1319
|
+
|
|
1320
|
+
`undefined` and `null` are normalized to `null`.
|
|
1321
|
+
|
|
1322
|
+
`Date` values remain `Date` objects.
|
|
1323
|
+
|
|
1324
|
+
Objects and arrays can optionally be serialized to JSON strings:
|
|
1325
|
+
|
|
1326
|
+
```ts
|
|
1327
|
+
resource.string = true
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
When enabled:
|
|
1331
|
+
|
|
1332
|
+
```ts
|
|
1333
|
+
const value = {
|
|
1334
|
+
name: "John",
|
|
1335
|
+
age: 30
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
is converted using:
|
|
1340
|
+
|
|
1341
|
+
```ts
|
|
1342
|
+
JSON.stringify(value)
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
## Error Handling
|
|
1346
|
+
|
|
1347
|
+
MySQL duplicate-key errors are normalized by adding:
|
|
1348
|
+
|
|
1349
|
+
```ts
|
|
1350
|
+
error = "duplicate"
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
to an error when it has:
|
|
1354
|
+
|
|
1355
|
+
```text
|
|
1356
|
+
errno = 1062
|
|
1357
|
+
code = ER_DUP_ENTRY
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
Example:
|
|
1361
|
+
|
|
1362
|
+
```ts
|
|
1363
|
+
try {
|
|
1364
|
+
await writer.write(user)
|
|
1365
|
+
} catch (err: any) {
|
|
1366
|
+
if (err.error === "duplicate") {
|
|
1367
|
+
// duplicate key
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
The original error is otherwise propagated.
|
|
1373
|
+
|
|
1374
|
+
## API
|
|
1375
|
+
|
|
1376
|
+
### Types
|
|
1377
|
+
|
|
1378
|
+
```ts
|
|
1379
|
+
StringMap
|
|
1380
|
+
Statement
|
|
1381
|
+
DataType
|
|
1382
|
+
Attribute
|
|
1383
|
+
Attributes
|
|
1384
|
+
Metadata
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
### SQL Builders
|
|
1388
|
+
|
|
1389
|
+
```ts
|
|
1390
|
+
param()
|
|
1391
|
+
params()
|
|
1392
|
+
metadata()
|
|
1393
|
+
buildToSave()
|
|
1394
|
+
buildToSaveBatch()
|
|
1395
|
+
toString()
|
|
1396
|
+
version()
|
|
1397
|
+
toArray()
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
### Execution
|
|
1401
|
+
|
|
1402
|
+
```ts
|
|
1403
|
+
execute()
|
|
1404
|
+
executeBatch()
|
|
1405
|
+
executeBatchConnectionTx()
|
|
1406
|
+
executeBatchConnection()
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
### Connection and Transactions
|
|
1410
|
+
|
|
1411
|
+
```ts
|
|
1412
|
+
getConnection()
|
|
1413
|
+
beginTransaction()
|
|
1414
|
+
commit()
|
|
1415
|
+
rollback()
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
### Writers
|
|
1419
|
+
|
|
1420
|
+
```ts
|
|
1421
|
+
MySQLWriter<T>
|
|
1422
|
+
MySQLBatchWriter<T>
|
|
1423
|
+
MySQLStreamWriter<T>
|
|
1424
|
+
```
|
|
1425
|
+
|
|
1426
|
+
## Health Check
|
|
1427
|
+
|
|
1428
|
+
Built-in MySQL health checker.
|
|
1429
|
+
|
|
1430
|
+
Designed for cloud-native deployments.
|
|
1431
|
+
|
|
1432
|
+
Features:
|
|
1433
|
+
|
|
1434
|
+
* Connection validation
|
|
1435
|
+
* Query validation
|
|
1436
|
+
* Response time measurement
|
|
1437
|
+
* Configurable timeout
|
|
1438
|
+
* Kubernetes readiness and liveness probes
|
|
1439
|
+
|
|
1440
|
+
Example:
|
|
1441
|
+
|
|
1442
|
+
```typescript
|
|
1443
|
+
const checker = new MySQLChecker(pool.promise());
|
|
1444
|
+
|
|
1445
|
+
const result = await checker.check();
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
Example response
|
|
1449
|
+
|
|
1450
|
+
```json
|
|
1451
|
+
{
|
|
1452
|
+
"status": "UP",
|
|
1453
|
+
"details": {
|
|
1454
|
+
"mysql": {
|
|
1455
|
+
"status": "UP"
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
## Design Philosophy
|
|
1462
|
+
|
|
1463
|
+
### Database Abstraction
|
|
1464
|
+
|
|
1465
|
+
`mysql2-core` keeps the database layer small while providing a consistent abstraction over the lower-level `mysql2` API.
|
|
1466
|
+
|
|
1467
|
+
The main design is based on three concepts:
|
|
1468
|
+
|
|
1469
|
+
```text
|
|
1470
|
+
Executor
|
|
1471
|
+
│
|
|
1472
|
+
├── DB
|
|
1473
|
+
│ └── beginTransaction()
|
|
1474
|
+
│
|
|
1475
|
+
└── Transaction
|
|
1476
|
+
├── commit()
|
|
1477
|
+
└── rollback()
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
This allows application code to depend on `DB` and `Transaction` rather than directly depending on `Pool` and `PoolConnection`.
|
|
1481
|
+
|
|
1482
|
+
### SQL Builder
|
|
1483
|
+
|
|
1484
|
+
`mysql2-core` sits between raw `mysql2` and a full ORM.
|
|
1485
|
+
|
|
1486
|
+
```text
|
|
1487
|
+
Raw mysql2
|
|
1488
|
+
│
|
|
1489
|
+
▼
|
|
1490
|
+
mysql2-core
|
|
1491
|
+
│
|
|
1492
|
+
├── metadata
|
|
1493
|
+
├── SQL generation
|
|
1494
|
+
├── persistence writers
|
|
1495
|
+
├── batch execution
|
|
1496
|
+
└── transaction helpers
|
|
1497
|
+
│
|
|
1498
|
+
▼
|
|
1499
|
+
Application
|
|
1500
|
+
```
|
|
1501
|
+
|
|
1502
|
+
It provides reusable persistence mechanics without introducing entity tracking, relationships, lazy loading, or a large ORM model.
|
|
1503
|
+
|
|
1504
|
+
## Ecosystem
|
|
1505
|
+
[`mysql2-core`](https://www.npmjs.com/package/mysql2-core) can work with [`sql-core`](https://www.npmjs.com/package/sql-core) and [`query-mappers`](https://www.npmjs.com/package/query-mappers). They separate responsibilities into independent layers.
|
|
1506
|
+
|
|
1507
|
+
* SQL generation belongs to [`sql-core`](https://www.npmjs.com/package/sql-core)
|
|
1508
|
+
* Object mapping belongs to [`query-mappers`](https://www.npmjs.com/package/query-mappers)
|
|
1509
|
+
* MySQL execution belongs to [`mysql2-core`](https://www.npmjs.com/package/mysql2-core)
|
|
1510
|
+
|
|
1511
|
+
This architecture keeps applications lightweight, modular, and easy to maintain.
|
|
1512
|
+
|
|
1513
|
+
```text
|
|
1514
|
+
Application
|
|
1515
|
+
│
|
|
1516
|
+
▼
|
|
1517
|
+
Repository (sql-core)
|
|
1518
|
+
│
|
|
1519
|
+
▼
|
|
1520
|
+
mysql2-core
|
|
1521
|
+
│
|
|
1522
|
+
▼
|
|
1523
|
+
MySQL
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
### Responsibilities
|
|
1527
|
+
|
|
1528
|
+
| Package | Responsibility |
|
|
1529
|
+
|----------------------------------------------------------------|---------------------------------------------------------------------|
|
|
1530
|
+
| [`mysql2-core`](https://www.npmjs.com/package/mysql2-core) | MySQL execution, repositories, writers, streaming, health checks |
|
|
1531
|
+
| [`sql-core`](https://www.npmjs.com/package/sql-core) | Database-independent repositories, CRUD, SQL builders, transactions |
|
|
1532
|
+
| [`query-mappers`](https://www.npmjs.com/package/query-mappers) | Maps database rows to TypeScript models |
|
|
1533
|
+
|
|
1534
|
+
## Requirements
|
|
1535
|
+
|
|
1536
|
+
* Node.js
|
|
1537
|
+
* TypeScript
|
|
1538
|
+
* MySQL
|
|
1539
|
+
* [`mysql2`](https://www.npmjs.com/package/mysql2)
|
|
1540
|
+
|
|
1541
|
+
## License
|
|
1542
|
+
|
|
1543
|
+
MIT
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
# mysql2-core
|
|
1548
|
+
|
|
1549
|
+
Lightweight TypeScript utilities for building and executing MySQL statements with [`mysql2`](https://www.npmjs.com/package/mysql2).
|
|
1550
|
+
|
|
1551
|
+
`mysql2-core` provides a metadata-driven persistence layer with support for single records, batch operations, buffered streaming writes, transactions, and configurable value mapping.
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
# mysql2-core
|
|
1555
|
+
|
|
3
1556
|
A lightweight MySQL database adapter built on top of **mysql2** for the **SQL Repository** framework.
|
|
4
1557
|
|
|
5
1558
|
This library provides connection management, transaction handling, query execution, object mapping, batch operations, streaming, exporting, and health checking while remaining completely independent from SQL generation.
|
|
6
1559
|
|
|
7
1560
|
Unlike traditional ORMs, this library **does not build SQL**. Instead, it executes SQL generated by the SQL Repository library (or any SQL source) and provides a clean, type-safe runtime layer.
|
|
8
1561
|
|
|
1562
|
+
### Example
|
|
1563
|
+
- [sql-modular-sample](https://github.com/source-code-template/sql-modular-sample): RESI API with express and MySQL
|
|
1564
|
+
|
|
9
1565
|
---
|
|
10
1566
|
|
|
11
1567
|
# Philosophy
|
|
@@ -73,9 +1629,9 @@ It provides:
|
|
|
73
1629
|
|
|
74
1630
|
- Connection management
|
|
75
1631
|
- Transactions
|
|
76
|
-
- Streaming
|
|
77
|
-
- Batch execution
|
|
78
1632
|
- Repository integration
|
|
1633
|
+
- Batch execution
|
|
1634
|
+
- Streaming
|
|
79
1635
|
|
|
80
1636
|
without hiding SQL from developers.
|
|
81
1637
|
|