@benjosivo/mysql 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,176 +1,176 @@
1
- # @benjosivo/mysql
2
-
3
- Shared MySQL client (pooling, automatic retry on deadlock/lock-timeout, transaction helpers) for use across multiple projects. Nothing connects until you call `init()` — the package has no side effects at import time and no dependency on any particular env var naming.
4
-
5
- ## Install
6
-
7
- ```
8
- npm install @benjosivo/mysql
9
- ```
10
-
11
- ## Usage
12
-
13
- ```ts
14
- import { init, executeMySQLQuery, closeMySQLConnection } from '@benjosivo/mysql';
15
-
16
- await init({
17
- host: process.env.DB_HOST!,
18
- user: process.env.DB_USER!,
19
- password: process.env.DB_PASSWORD!,
20
- database: process.env.DB_NAME!,
21
- onError: (error, context) => myLogger.error(context, error), // optional, defaults to console.error
22
- });
23
-
24
- const rows = await executeMySQLQuery('SELECT * FROM users WHERE id = ?', [userId]);
25
-
26
- // on shutdown
27
- await closeMySQLConnection();
28
- ```
29
-
30
- Call `init()` once per process, at startup. Each consuming project supplies its own credentials and (optionally) its own error reporting — this package has no opinion on either.
31
-
32
- ### Config
33
-
34
- | Field | Required | Default | Description |
35
- |---|---|---|---|
36
- | `host`, `user`, `password`, `database` | yes | — | MySQL connection details |
37
- | `waitForConnections` | no | `true` | passed through to `mysql2` pool |
38
- | `connectionLimit` | no | `50` | passed through to `mysql2` pool |
39
- | `queueLimit` | no | `0` | passed through to `mysql2` pool |
40
- | `multipleStatements` | no | `true` | passed through to `mysql2` pool |
41
- | `staleTransactionMs` | no | `300000` (5 min) | a transaction with no statement activity for this long is auto rolled back |
42
- | `defaultQueryTimeoutMs` | no | — (off) | default per-statement timeout; overridable per call with `timeoutMs` |
43
- | `onError` | no | `console.error` | called with `(error, context)` whenever the client can't throw the error directly (e.g. background retry loops) |
44
-
45
- Calling `init()` again (e.g. to reconfigure) closes the previous pool first.
46
-
47
- ### Queries
48
-
49
- ```ts
50
- executeMySQLQuery3<T>({ query, values?, connKey?, returnFieldTypes?, returnListTables?, timeoutMs? })
51
- ```
52
-
53
- **Preferred for new code.** Always resolves to a wrapper — it never throws and never returns
54
- bare rows, so every result is safe to inspect before use:
55
-
56
- ```ts
57
- const res = await executeMySQLQuery3<User[]>({ query: 'SELECT * FROM users WHERE id = ?', values: [userId] });
58
-
59
- if (!res.ok) {
60
- // res.error is always a non-empty string
61
- logger.error(res.error, res.code, res.errno, res.sqlState);
62
- return;
63
- }
64
-
65
- res.rows[0].email; // narrowed by `ok` — TypeScript knows rows is present
66
- ```
67
-
68
- `T` types the rows: `User[]` for a SELECT, `ResultSetHeader` for an INSERT/UPDATE. It defaults to
69
- `any`, so untyped calls keep working.
70
-
71
- | On success | On failure |
72
- |---|---|
73
- | `ok: true` | `ok: false` |
74
- | `rows: T` | `error: string` (never empty), `code?`, `errno?`, `sqlState?`, `cause?` |
75
- | `fieldsType: []` unless `returnFieldTypes` | `stage`: which step failed (`execute`, `transaction-lookup`, `procedure`, …) |
76
- | `tables: []` unless `returnListTables` | `values`: the parameters that were sent |
77
- | `connKey: string \| null` | `connKey: string \| null` |
78
-
79
- Because the union is discriminated on `ok`, TypeScript will not let you read `rows` off a result
80
- you haven't checked.
81
-
82
- #### Retries
83
-
84
- Only *transient* failures are retried (up to 5 attempts, with a backoff): deadlocks, lock timeouts,
85
- lost connections, and a server that is out of connections. Deterministic failures — duplicate key,
86
- bad syntax, constraint violations, unknown column, access denied — are returned on the **first**
87
- attempt, since re-sending them can only produce the same error.
88
-
89
- #### Timeouts
90
-
91
- `timeoutMs` (or `defaultQueryTimeoutMs` in the config) bounds a statement, covering both the wait
92
- for a pooled connection and the query itself. On expiry the statement is aborted server-side with
93
- `KILL QUERY` so it stops consuming resources, and the failure comes back as
94
- `{ ok: false, code: 'ETIMEDOUT' }`. Timed-out statements are never retried — a killed write must
95
- not be silently re-applied.
96
-
97
- ```ts
98
- const res = await executeMySQLQuery3({ query: 'SELECT ...', values, timeoutMs: 10_000 });
99
- ```
100
-
101
- #### Legacy shape
102
-
103
- ```ts
104
- executeMySQLQuery(query, values?, returnFieldTypes?, connKey?, timeoutMs?)
105
- executeMySQLQuery2({ query, values?, connKey?, returnFieldTypes?, returnListTables?, timeoutMs? })
106
- ```
107
-
108
- Unchanged: these return the rows directly on success (or `{ rows, fieldsType, connKey, tables }`
109
- when a transaction or extra metadata was requested), and `{ error, values, connKey }` on failure
110
- instead of throwing. Failures now also carry `code`, `errno` and `sqlState` alongside the message.
111
- They share the retry, timeout and transaction handling described above; `executeMySQLQuery2` is a
112
- thin adapter over `executeMySQLQuery3`.
113
-
114
- ### Statements prepared statements can't run
115
-
116
- `executeMySQLQuery` uses `execute()` (MySQL's prepared-statement protocol), and MySQL rejects a
117
- number of statements there with *"This command is not supported in the prepared statement protocol
118
- yet"* — `SET GLOBAL ...`, `USE ...`, `LOCK TABLES`, several `SHOW` variants, and so on. For those,
119
- use `runMySQLQuery`, which goes through `query()` (text protocol) instead:
120
-
121
- ```ts
122
- import { runMySQLQuery } from '@benjosivo/mysql';
123
-
124
- await runMySQLQuery("SET GLOBAL general_log = 'OFF'");
125
-
126
- // placeholders still work — mysql2 escapes and interpolates them client-side
127
- await runMySQLQuery('SET GLOBAL general_log = ?', ['OFF']);
128
-
129
- // optionally run inside an existing transaction
130
- await runMySQLQuery('SELECT * FROM users WHERE id = ?', [userId], connKey);
131
- ```
132
-
133
- ```ts
134
- runMySQLQuery(query, values?, connKey?, timeoutMs?)
135
- ```
136
-
137
- Same result shape as `executeMySQLQuery`: rows on success, `{ error, values, connKey }` on failure
138
- (with `code`/`errno`/`sqlState` when MySQL supplied them).
139
- It's deliberately simple — no deadlock/lock-timeout retry — so prefer `executeMySQLQuery` for
140
- regular application queries and keep this one for statements the prepared-statement protocol
141
- refuses.
142
-
143
- ### Transactions
144
-
145
- ```ts
146
- const opened = await executeMySQLQuery3({ query: 'INSERT ...', values, connKey: true });
147
- if (!opened.ok) return opened.error;
148
-
149
- await executeMySQLQuery3({ query: 'UPDATE ...', values, connKey: opened.connKey }); // same transaction
150
-
151
- await connectionCommit(opened.connKey!);
152
- // or: await connectionRollback(opened.connKey!);
153
- ```
154
-
155
- A transaction is retired as soon as it can no longer be used — MySQL rolled it back after a
156
- deadlock, its connection was lost, or the background sweep found it idle. Its connection is
157
- released immediately, and any later use reports *why*:
158
-
159
- ```ts
160
- // Transaction <key> is no longer usable: MySQL rolled it back after a deadlock
161
- ```
162
-
163
- `connectionRollback` on a transaction that was already retired succeeds (there is nothing left to
164
- undo); `connectionCommit` reports an error, because the data did not make it. Both always free the
165
- connection, even if the COMMIT or ROLLBACK itself fails.
166
-
167
- `staleTransactionMs` measures **idle** time: a transaction is swept only when no statement has run
168
- on it for that long, so a long-running but active transaction is left alone.
169
-
170
- ### Shutdown
171
-
172
- ```ts
173
- await closeMySQLConnection();
174
- ```
175
-
176
- Rolls back any open transactions, ends the pool, and stops the background sweep. Safe to call even if `init()` was never called.
1
+ # @benjosivo/mysql
2
+
3
+ Shared MySQL client (pooling, automatic retry on deadlock/lock-timeout, transaction helpers) for use across multiple projects. Nothing connects until you call `init()` — the package has no side effects at import time and no dependency on any particular env var naming.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install @benjosivo/mysql
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { init, executeMySQLQuery, closeMySQLConnection } from '@benjosivo/mysql';
15
+
16
+ await init({
17
+ host: process.env.DB_HOST!,
18
+ user: process.env.DB_USER!,
19
+ password: process.env.DB_PASSWORD!,
20
+ database: process.env.DB_NAME!,
21
+ onError: (error, context) => myLogger.error(context, error), // optional, defaults to console.error
22
+ });
23
+
24
+ const rows = await executeMySQLQuery('SELECT * FROM users WHERE id = ?', [userId]);
25
+
26
+ // on shutdown
27
+ await closeMySQLConnection();
28
+ ```
29
+
30
+ Call `init()` once per process, at startup. Each consuming project supplies its own credentials and (optionally) its own error reporting — this package has no opinion on either.
31
+
32
+ ### Config
33
+
34
+ | Field | Required | Default | Description |
35
+ |---|---|---|---|
36
+ | `host`, `user`, `password`, `database` | yes | — | MySQL connection details |
37
+ | `waitForConnections` | no | `true` | passed through to `mysql2` pool |
38
+ | `connectionLimit` | no | `50` | passed through to `mysql2` pool |
39
+ | `queueLimit` | no | `0` | passed through to `mysql2` pool |
40
+ | `multipleStatements` | no | `true` | passed through to `mysql2` pool |
41
+ | `staleTransactionMs` | no | `300000` (5 min) | a transaction with no statement activity for this long is auto rolled back |
42
+ | `defaultQueryTimeoutMs` | no | — (off) | default per-statement timeout; overridable per call with `timeoutMs` |
43
+ | `onError` | no | `console.error` | called with `(error, context)` whenever the client can't throw the error directly (e.g. background retry loops) |
44
+
45
+ Calling `init()` again (e.g. to reconfigure) closes the previous pool first.
46
+
47
+ ### Queries
48
+
49
+ ```ts
50
+ executeMySQLQuery3<T>({ query, values?, connKey?, returnFieldTypes?, returnListTables?, timeoutMs? })
51
+ ```
52
+
53
+ **Preferred for new code.** Always resolves to a wrapper — it never throws and never returns
54
+ bare rows, so every result is safe to inspect before use:
55
+
56
+ ```ts
57
+ const res = await executeMySQLQuery3<User[]>({ query: 'SELECT * FROM users WHERE id = ?', values: [userId] });
58
+
59
+ if (!res.ok) {
60
+ // res.error is always a non-empty string
61
+ logger.error(res.error, res.code, res.errno, res.sqlState);
62
+ return;
63
+ }
64
+
65
+ res.rows[0].email; // narrowed by `ok` — TypeScript knows rows is present
66
+ ```
67
+
68
+ `T` types the rows: `User[]` for a SELECT, `ResultSetHeader` for an INSERT/UPDATE. It defaults to
69
+ `any`, so untyped calls keep working.
70
+
71
+ | On success | On failure |
72
+ |---|---|
73
+ | `ok: true` | `ok: false` |
74
+ | `rows: T` | `error: string` (never empty), `code?`, `errno?`, `sqlState?`, `cause?` |
75
+ | `fieldsType: []` unless `returnFieldTypes` | `stage`: which step failed (`execute`, `transaction-lookup`, `procedure`, …) |
76
+ | `tables: []` unless `returnListTables` | `values`: the parameters that were sent |
77
+ | `connKey: string \| null` | `connKey: string \| null` |
78
+
79
+ Because the union is discriminated on `ok`, TypeScript will not let you read `rows` off a result
80
+ you haven't checked.
81
+
82
+ #### Retries
83
+
84
+ Only *transient* failures are retried (up to 5 attempts, with a backoff): deadlocks, lock timeouts,
85
+ lost connections, and a server that is out of connections. Deterministic failures — duplicate key,
86
+ bad syntax, constraint violations, unknown column, access denied — are returned on the **first**
87
+ attempt, since re-sending them can only produce the same error.
88
+
89
+ #### Timeouts
90
+
91
+ `timeoutMs` (or `defaultQueryTimeoutMs` in the config) bounds a statement, covering both the wait
92
+ for a pooled connection and the query itself. On expiry the statement is aborted server-side with
93
+ `KILL QUERY` so it stops consuming resources, and the failure comes back as
94
+ `{ ok: false, code: 'ETIMEDOUT' }`. Timed-out statements are never retried — a killed write must
95
+ not be silently re-applied.
96
+
97
+ ```ts
98
+ const res = await executeMySQLQuery3({ query: 'SELECT ...', values, timeoutMs: 10_000 });
99
+ ```
100
+
101
+ #### Legacy shape
102
+
103
+ ```ts
104
+ executeMySQLQuery(query, values?, returnFieldTypes?, connKey?, timeoutMs?)
105
+ executeMySQLQuery2({ query, values?, connKey?, returnFieldTypes?, returnListTables?, timeoutMs? })
106
+ ```
107
+
108
+ Unchanged: these return the rows directly on success (or `{ rows, fieldsType, connKey, tables }`
109
+ when a transaction or extra metadata was requested), and `{ error, values, connKey }` on failure
110
+ instead of throwing. Failures now also carry `code`, `errno` and `sqlState` alongside the message.
111
+ They share the retry, timeout and transaction handling described above; `executeMySQLQuery2` is a
112
+ thin adapter over `executeMySQLQuery3`.
113
+
114
+ ### Statements prepared statements can't run
115
+
116
+ `executeMySQLQuery` uses `execute()` (MySQL's prepared-statement protocol), and MySQL rejects a
117
+ number of statements there with *"This command is not supported in the prepared statement protocol
118
+ yet"* — `SET GLOBAL ...`, `USE ...`, `LOCK TABLES`, several `SHOW` variants, and so on. For those,
119
+ use `runMySQLQuery`, which goes through `query()` (text protocol) instead:
120
+
121
+ ```ts
122
+ import { runMySQLQuery } from '@benjosivo/mysql';
123
+
124
+ await runMySQLQuery("SET GLOBAL general_log = 'OFF'");
125
+
126
+ // placeholders still work — mysql2 escapes and interpolates them client-side
127
+ await runMySQLQuery('SET GLOBAL general_log = ?', ['OFF']);
128
+
129
+ // optionally run inside an existing transaction
130
+ await runMySQLQuery('SELECT * FROM users WHERE id = ?', [userId], connKey);
131
+ ```
132
+
133
+ ```ts
134
+ runMySQLQuery(query, values?, connKey?, timeoutMs?)
135
+ ```
136
+
137
+ Same result shape as `executeMySQLQuery`: rows on success, `{ error, values, connKey }` on failure
138
+ (with `code`/`errno`/`sqlState` when MySQL supplied them).
139
+ It's deliberately simple — no deadlock/lock-timeout retry — so prefer `executeMySQLQuery` for
140
+ regular application queries and keep this one for statements the prepared-statement protocol
141
+ refuses.
142
+
143
+ ### Transactions
144
+
145
+ ```ts
146
+ const opened = await executeMySQLQuery3({ query: 'INSERT ...', values, connKey: true });
147
+ if (!opened.ok) return opened.error;
148
+
149
+ await executeMySQLQuery3({ query: 'UPDATE ...', values, connKey: opened.connKey }); // same transaction
150
+
151
+ await connectionCommit(opened.connKey!);
152
+ // or: await connectionRollback(opened.connKey!);
153
+ ```
154
+
155
+ A transaction is retired as soon as it can no longer be used — MySQL rolled it back after a
156
+ deadlock, its connection was lost, or the background sweep found it idle. Its connection is
157
+ released immediately, and any later use reports *why*:
158
+
159
+ ```ts
160
+ // Transaction <key> is no longer usable: MySQL rolled it back after a deadlock
161
+ ```
162
+
163
+ `connectionRollback` on a transaction that was already retired succeeds (there is nothing left to
164
+ undo); `connectionCommit` reports an error, because the data did not make it. Both always free the
165
+ connection, even if the COMMIT or ROLLBACK itself fails.
166
+
167
+ `staleTransactionMs` measures **idle** time: a transaction is swept only when no statement has run
168
+ on it for that long, so a long-running but active transaction is left alone.
169
+
170
+ ### Shutdown
171
+
172
+ ```ts
173
+ await closeMySQLConnection();
174
+ ```
175
+
176
+ Rolls back any open transactions, ends the pool, and stops the background sweep. Safe to call even if `init()` was never called.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import mysql from 'mysql2/promise';
1
2
  export interface MySQLClientConfig {
2
3
  host: string;
3
4
  user: string;
@@ -16,6 +17,7 @@ export interface MySQLClientConfig {
16
17
  defaultQueryTimeoutMs?: number;
17
18
  /** Called whenever this client encounters an error it can't throw directly (e.g. inside retry loops). Defaults to console.error. */
18
19
  onError?: (error: unknown, context: string) => void;
20
+ otherOtions: mysql.PoolOptions;
19
21
  }
20
22
  export interface MySQLFieldType {
21
23
  fieldName: string;
package/dist/index.js CHANGED
@@ -93,6 +93,7 @@ function createPool() {
93
93
  connectionLimit: cfg.connectionLimit ?? 50,
94
94
  queueLimit: cfg.queueLimit ?? 0,
95
95
  multipleStatements: cfg.multipleStatements ?? true,
96
+ ...cfg.otherOtions
96
97
  });
97
98
  }
98
99
  async function init(clientConfig) {
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
- {
2
- "name": "@benjosivo/mysql",
3
- "version": "1.3.2",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "files": [
7
- "dist"
8
- ],
9
- "scripts": {
10
- "build": "tsc",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "dependencies": {
14
- "mysql2": "^3.x",
15
- "node-sql-parser": "^5.4.0"
16
- },
17
- "devDependencies": {
18
- "@types/node": "^26.1.2",
19
- "typescript": "^7.0.2"
20
- },
21
- "repository": {
22
- "type": "git",
23
- "url": "git+https://github.com/benjosivo/mysql.git"
24
- },
25
- "license": "MIT"
26
- }
1
+ {
2
+ "name": "@benjosivo/mysql",
3
+ "version": "1.4.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "dependencies": {
14
+ "mysql2": "^3.x",
15
+ "node-sql-parser": "^5.4.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^26.1.2",
19
+ "typescript": "^7.0.2"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/benjosivo/mysql.git"
24
+ },
25
+ "license": "MIT"
26
+ }