mysql2-core 0.3.8 → 0.3.9

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 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[], firstSuccess?: 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
+ ## `firstSuccess`
287
+
288
+ `executeBatch()` supports an optional `firstSuccess` flag:
289
+
290
+ ```ts
291
+ await db.executeBatch(statements, true)
292
+ ```
293
+
294
+ When `firstSuccess` 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