@benjosivo/mysql 1.0.3
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 +110 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +423 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
This package is published to **GitHub Packages**, not the public npm registry, so every machine needs a one-time setup before `npm install` will work — even though the repo and package are public. GitHub Packages never allows anonymous installs; a valid GitHub token is always required.
|
|
8
|
+
|
|
9
|
+
### One-time setup (per machine)
|
|
10
|
+
|
|
11
|
+
1. **Create a GitHub Personal Access Token.**
|
|
12
|
+
GitHub → click your avatar → **Settings** → **Developer settings** → **Personal access tokens** → **Tokens (classic)** → **Generate new token (classic)**.
|
|
13
|
+
Scope needed: **`read:packages`**. Copy the token — GitHub only shows it once.
|
|
14
|
+
|
|
15
|
+
2. **Add two lines to your global npm config.**
|
|
16
|
+
Find the file path:
|
|
17
|
+
```
|
|
18
|
+
npm config get globalconfig
|
|
19
|
+
```
|
|
20
|
+
It's typically `C:\Users\<you>\AppData\Roaming\npm\etc\npmrc` on Windows. Open that file in a text editor and add:
|
|
21
|
+
```
|
|
22
|
+
@benjosivo:registry=https://npm.pkg.github.com
|
|
23
|
+
//npm.pkg.github.com/:_authToken=YOUR_TOKEN_HERE
|
|
24
|
+
```
|
|
25
|
+
Editing the file directly is more reliable than `npm config set` — on some npm versions, `npm config set @benjosivo:registry=... --global` fails with a cryptic `` `` is not a valid npm option `` error. If you'd rather use the CLI, use the space-separated form instead of `=`:
|
|
26
|
+
```
|
|
27
|
+
npm config set @benjosivo:registry https://npm.pkg.github.com --global
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. **Verify both lines took effect:**
|
|
31
|
+
```
|
|
32
|
+
npm config list
|
|
33
|
+
```
|
|
34
|
+
You should see both `@benjosivo:registry = "https://npm.pkg.github.com"` and `//npm.pkg.github.com/:_authToken = (protected)` in the output.
|
|
35
|
+
|
|
36
|
+
### Install
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
npm install @benjosivo/mysql
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Troubleshooting
|
|
43
|
+
|
|
44
|
+
- **`404 Not Found - GET https://registry.npmjs.org/@benjosivo%2fmysql`** — npm is checking the default npm registry instead of GitHub Packages. This means the `@benjosivo:registry` line from step 2 above isn't set (check with `npm config get @benjosivo:registry`). Add it and retry.
|
|
45
|
+
- **401/403 or `ENEEDAUTH`** — the auth token is missing, expired, or lacks `read:packages` scope. Regenerate a token and re-add it.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { init, executeMySQLQuery, closeMySQLConnection } from '@benjosivo/mysql';
|
|
51
|
+
|
|
52
|
+
await init({
|
|
53
|
+
host: process.env.DB_HOST!,
|
|
54
|
+
user: process.env.DB_USER!,
|
|
55
|
+
password: process.env.DB_PASSWORD!,
|
|
56
|
+
database: process.env.DB_NAME!,
|
|
57
|
+
onError: (error, context) => myLogger.error(context, error), // optional, defaults to console.error
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const rows = await executeMySQLQuery('SELECT * FROM users WHERE id = ?', [userId]);
|
|
61
|
+
|
|
62
|
+
// on shutdown
|
|
63
|
+
await closeMySQLConnection();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
### Config
|
|
69
|
+
|
|
70
|
+
| Field | Required | Default | Description |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `host`, `user`, `password`, `database` | yes | — | MySQL connection details |
|
|
73
|
+
| `waitForConnections` | no | `true` | passed through to `mysql2` pool |
|
|
74
|
+
| `connectionLimit` | no | `50` | passed through to `mysql2` pool |
|
|
75
|
+
| `queueLimit` | no | `0` | passed through to `mysql2` pool |
|
|
76
|
+
| `multipleStatements` | no | `true` | passed through to `mysql2` pool |
|
|
77
|
+
| `staleTransactionMs` | no | `300000` (5 min) | idle transactions older than this are auto rolled back |
|
|
78
|
+
| `onError` | no | `console.error` | called with `(error, context)` whenever the client can't throw the error directly (e.g. background retry loops) |
|
|
79
|
+
|
|
80
|
+
Calling `init()` again (e.g. to reconfigure) closes the previous pool first.
|
|
81
|
+
|
|
82
|
+
### Queries
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
executeMySQLQuery(query, values?, returnFieldTypes?, connKey?)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Returns the rows directly on success. On failure it returns `{ error, values, connKey }` instead of throwing (matches `mysql2`'s row-result shape so existing call sites don't need try/catch).
|
|
89
|
+
|
|
90
|
+
### Transactions
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const { connKey, error } = await executeMySQLQuery2({ query: 'INSERT ...', values, connKey: true });
|
|
94
|
+
if (error) { /* handle */ }
|
|
95
|
+
|
|
96
|
+
await executeMySQLQuery2({ query: 'UPDATE ...', values, connKey }); // reuse the same transaction
|
|
97
|
+
|
|
98
|
+
await connectionCommit(connKey);
|
|
99
|
+
// or: await connectionRollback(connKey);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Transactions left open longer than `staleTransactionMs` are automatically rolled back by a background sweep.
|
|
103
|
+
|
|
104
|
+
### Shutdown
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
await closeMySQLConnection();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
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
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface MySQLClientConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
user: string;
|
|
4
|
+
password: string;
|
|
5
|
+
database: string;
|
|
6
|
+
waitForConnections?: boolean;
|
|
7
|
+
connectionLimit?: number;
|
|
8
|
+
queueLimit?: number;
|
|
9
|
+
multipleStatements?: boolean;
|
|
10
|
+
/** How long an open transaction can sit idle before it's automatically rolled back. Default: 5 minutes. */
|
|
11
|
+
staleTransactionMs?: number;
|
|
12
|
+
/** Called whenever this client encounters an error it can't throw directly (e.g. inside retry loops). Defaults to console.error. */
|
|
13
|
+
onError?: (error: unknown, context: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function init(clientConfig: MySQLClientConfig): Promise<void>;
|
|
16
|
+
export declare function closeMySQLConnection(): Promise<void>;
|
|
17
|
+
export declare function loopKillOpenTRX(): void;
|
|
18
|
+
export declare function stopLoopKillOpenTRX(): void;
|
|
19
|
+
export declare function executeMySQLQuery(query: string, values?: any[], returnFieldTypes?: boolean, connKey?: boolean | string): Promise<any>;
|
|
20
|
+
export declare function executeMySQLQuery2(opt: {
|
|
21
|
+
query: string;
|
|
22
|
+
values?: any[];
|
|
23
|
+
connKey?: boolean | string;
|
|
24
|
+
returnFieldTypes?: boolean;
|
|
25
|
+
returnListTables?: boolean | string;
|
|
26
|
+
}): Promise<any>;
|
|
27
|
+
export declare function connectionCommit(keyConn: string): Promise<{
|
|
28
|
+
error?: unknown;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function connectionRollback(keyConn: string): Promise<{
|
|
31
|
+
error?: unknown;
|
|
32
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.init = init;
|
|
40
|
+
exports.closeMySQLConnection = closeMySQLConnection;
|
|
41
|
+
exports.loopKillOpenTRX = loopKillOpenTRX;
|
|
42
|
+
exports.stopLoopKillOpenTRX = stopLoopKillOpenTRX;
|
|
43
|
+
exports.executeMySQLQuery = executeMySQLQuery;
|
|
44
|
+
exports.executeMySQLQuery2 = executeMySQLQuery2;
|
|
45
|
+
exports.connectionCommit = connectionCommit;
|
|
46
|
+
exports.connectionRollback = connectionRollback;
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const parserModule = __importStar(require("node-sql-parser"));
|
|
49
|
+
const promise_1 = __importDefault(require("mysql2/promise"));
|
|
50
|
+
let config;
|
|
51
|
+
let pool;
|
|
52
|
+
let lastConnectionCheck = 0;
|
|
53
|
+
function defaultOnError(error, context) {
|
|
54
|
+
console.error(`[mysql-client] ${context}`, error);
|
|
55
|
+
}
|
|
56
|
+
function handleError(error, context) {
|
|
57
|
+
(config?.onError ?? defaultOnError)(error, context);
|
|
58
|
+
}
|
|
59
|
+
function requireConfig() {
|
|
60
|
+
if (!config)
|
|
61
|
+
throw new Error('MySQL client not initialized. Call init(config) before using this package.');
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
function requirePool() {
|
|
65
|
+
if (!pool)
|
|
66
|
+
throw new Error('MySQL client not initialized. Call init(config) before using this package.');
|
|
67
|
+
return pool;
|
|
68
|
+
}
|
|
69
|
+
function createPool() {
|
|
70
|
+
const cfg = requireConfig();
|
|
71
|
+
pool = promise_1.default.createPool({
|
|
72
|
+
host: cfg.host,
|
|
73
|
+
user: cfg.user,
|
|
74
|
+
password: cfg.password,
|
|
75
|
+
database: cfg.database,
|
|
76
|
+
waitForConnections: cfg.waitForConnections ?? true,
|
|
77
|
+
connectionLimit: cfg.connectionLimit ?? 50,
|
|
78
|
+
queueLimit: cfg.queueLimit ?? 0,
|
|
79
|
+
multipleStatements: cfg.multipleStatements ?? true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function init(clientConfig) {
|
|
83
|
+
if (pool)
|
|
84
|
+
await closeMySQLConnection();
|
|
85
|
+
config = clientConfig;
|
|
86
|
+
createPool();
|
|
87
|
+
loopKillOpenTRX();
|
|
88
|
+
}
|
|
89
|
+
async function closeMySQLConnection() {
|
|
90
|
+
stopLoopKillOpenTRX();
|
|
91
|
+
if (!pool)
|
|
92
|
+
return;
|
|
93
|
+
const rollbackPromises = [...listTransaction.keys()].map(connectionRollback);
|
|
94
|
+
await Promise.all(rollbackPromises);
|
|
95
|
+
try {
|
|
96
|
+
await pool.end();
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
pool = undefined;
|
|
100
|
+
config = undefined;
|
|
101
|
+
lastConnectionCheck = 0;
|
|
102
|
+
}
|
|
103
|
+
const listTransaction = new Map();
|
|
104
|
+
let killOpenTRXInterval = null;
|
|
105
|
+
async function killOpenTRX() {
|
|
106
|
+
const staleTransactionMs = config?.staleTransactionMs ?? 5 * 60000;
|
|
107
|
+
const rollbackPromises = [];
|
|
108
|
+
for (const [key, value] of listTransaction) {
|
|
109
|
+
if (value.timestamp + staleTransactionMs < Date.now()) {
|
|
110
|
+
rollbackPromises.push(connectionRollback(key));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await Promise.all(rollbackPromises);
|
|
114
|
+
}
|
|
115
|
+
function loopKillOpenTRX() {
|
|
116
|
+
if (killOpenTRXInterval)
|
|
117
|
+
return;
|
|
118
|
+
killOpenTRXInterval = setInterval(killOpenTRX, 1000);
|
|
119
|
+
}
|
|
120
|
+
function stopLoopKillOpenTRX() {
|
|
121
|
+
if (!killOpenTRXInterval)
|
|
122
|
+
return;
|
|
123
|
+
clearInterval(killOpenTRXInterval);
|
|
124
|
+
killOpenTRXInterval = null;
|
|
125
|
+
}
|
|
126
|
+
async function ensurePool() {
|
|
127
|
+
try {
|
|
128
|
+
const conn = await requirePool().getConnection();
|
|
129
|
+
conn.release();
|
|
130
|
+
lastConnectionCheck = Date.now();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
try {
|
|
134
|
+
await requirePool().end();
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
try {
|
|
138
|
+
createPool();
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
handleError(err, 'MySQL: ensurePool — could not recreate pool');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function executeMySQLQuery(query, values = [], returnFieldTypes = false, connKey) {
|
|
146
|
+
return executeMySQLQuery2({ query, values, returnFieldTypes, connKey });
|
|
147
|
+
}
|
|
148
|
+
async function executeMySQLQuery2(opt) {
|
|
149
|
+
requireConfig();
|
|
150
|
+
const { query, values = [], returnFieldTypes = false, returnListTables = false } = opt;
|
|
151
|
+
let { connKey } = opt;
|
|
152
|
+
async function executeWithRetry(opt) {
|
|
153
|
+
/**
|
|
154
|
+
* Executes a query with automatic retry on deadlock and lock timeout.
|
|
155
|
+
*
|
|
156
|
+
* - `callerOwnsConnection`: caller holds a transaction and manages commit/rollback.
|
|
157
|
+
* - Deadlock → transaction already rolled back by MySQL, throw immediately.
|
|
158
|
+
* - Timeout → statement rolled back, transaction still alive — wait and retry
|
|
159
|
+
* up to MAX_EXECUTE_ATTEMPTS; throw on last attempt so caller rollbacks.
|
|
160
|
+
* - `createsNewTransaction`: we own the transaction internally.
|
|
161
|
+
* - Deadlock → rollback + recreate transaction, retry.
|
|
162
|
+
* - Timeout → rollback + recreate transaction, retry with longer delay.
|
|
163
|
+
* - Neither (plain pool):
|
|
164
|
+
* - Both → wait (with appropriate delay) and retry against the pool.
|
|
165
|
+
*/
|
|
166
|
+
const { query, values, callerOwnsConnection, createsNewTransaction } = opt;
|
|
167
|
+
let { conn, connKey } = opt;
|
|
168
|
+
function isConnectionLost(error) {
|
|
169
|
+
return (error?.code === 'PROTOCOL_CONNECTION_LOST' ||
|
|
170
|
+
error?.code === 'ECONNRESET' ||
|
|
171
|
+
error?.code === 'ECONNREFUSED' ||
|
|
172
|
+
error?.code === 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR' ||
|
|
173
|
+
error?.fatal === true);
|
|
174
|
+
}
|
|
175
|
+
for (let attempt = 1; attempt <= MAX_EXECUTE_ATTEMPTS; attempt++) {
|
|
176
|
+
try {
|
|
177
|
+
const [rows, fields] = await (conn ?? requirePool()).execute(query, values);
|
|
178
|
+
if (conn && connKey) {
|
|
179
|
+
const activeTr = listTransaction.get(connKey);
|
|
180
|
+
if (activeTr) {
|
|
181
|
+
activeTr.lastValidConn = conn;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { rows, fields: fields, connKey };
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const isDeadlock = error?.code === 'ER_LOCK_DEADLOCK';
|
|
188
|
+
const isLockTimeout = error?.code === 'ER_LOCK_WAIT_TIMEOUT';
|
|
189
|
+
const isLockError = isDeadlock || isLockTimeout;
|
|
190
|
+
const isConnLost = isConnectionLost(error);
|
|
191
|
+
if (attempt === MAX_EXECUTE_ATTEMPTS) {
|
|
192
|
+
if (createsNewTransaction && connKey) {
|
|
193
|
+
try {
|
|
194
|
+
await connectionRollback(connKey);
|
|
195
|
+
}
|
|
196
|
+
catch { }
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
// Fatal connection loss requires cleanup.
|
|
201
|
+
if (isConnLost) {
|
|
202
|
+
if (createsNewTransaction && connKey) {
|
|
203
|
+
try {
|
|
204
|
+
await connectionRollback(connKey);
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
conn = undefined;
|
|
208
|
+
const reqTr = await createTransaction();
|
|
209
|
+
if ('error' in reqTr)
|
|
210
|
+
throw new Error('Failed to recreate transaction after lost connection');
|
|
211
|
+
conn = reqTr.conn;
|
|
212
|
+
connKey = reqTr.connKey;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (callerOwnsConnection) {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
await ensurePool();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Deadlock + caller-owned: MySQL already rolled back the entire transaction,
|
|
222
|
+
// retrying on the same connection is not possible — throw so the caller rollbacks.
|
|
223
|
+
if (isDeadlock && callerOwnsConnection) {
|
|
224
|
+
if (connKey)
|
|
225
|
+
connectionRollback(connKey);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
// Non-lock error: re-check the pool and retry
|
|
229
|
+
if (!isLockError) {
|
|
230
|
+
if (!conn)
|
|
231
|
+
await ensurePool(); // pool health is irrelevant when we own a connection
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Timeout + caller-owned: only the statement was rolled back, the transaction
|
|
235
|
+
// is still alive — wait and retry the statement on the same connection.
|
|
236
|
+
// The caller will rollback the transaction itself if we ultimately throw.
|
|
237
|
+
if (isLockTimeout && callerOwnsConnection) {
|
|
238
|
+
await new Promise((r) => setTimeout(r, LOCK_TIMEOUT_RETRY_DELAY_MS * attempt));
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Deadlock: MySQL already rolled back the transaction — retry quickly.
|
|
242
|
+
// Timeout: something is holding a lock — wait longer before retrying.
|
|
243
|
+
const retryDelay = isDeadlock ? DEADLOCK_RETRY_DELAY_MS : LOCK_TIMEOUT_RETRY_DELAY_MS;
|
|
244
|
+
await new Promise((r) => setTimeout(r, retryDelay * attempt));
|
|
245
|
+
if (createsNewTransaction) {
|
|
246
|
+
// For both error types, our transaction is no longer usable — recreate it
|
|
247
|
+
try {
|
|
248
|
+
conn?.release();
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
conn = undefined;
|
|
252
|
+
const reqTr = await createTransaction();
|
|
253
|
+
if ('error' in reqTr)
|
|
254
|
+
throw new Error(`Failed to recreate transaction after ${isDeadlock ? 'deadlock' : 'lock timeout'}`);
|
|
255
|
+
conn = reqTr.conn;
|
|
256
|
+
connKey = reqTr.connKey;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error('MySQL: executeWithRetry reached unexpected end');
|
|
261
|
+
}
|
|
262
|
+
function checkStoredProcedureResult(rows, query) {
|
|
263
|
+
const message = rows?.[0]?.[0]?.['@message'] ?? '';
|
|
264
|
+
if (!message.toLowerCase().startsWith('ok')) {
|
|
265
|
+
return message || `No message returned from procedure: ${query}`;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
function resolveFieldTypes(fields) {
|
|
270
|
+
if (!promise_1.default.Types)
|
|
271
|
+
throw new Error('mysql.Types is not available');
|
|
272
|
+
return fields.map((field) => {
|
|
273
|
+
const typeName = Object.keys(promise_1.default.Types).find((key) => promise_1.default.Types[key] === field.type);
|
|
274
|
+
return { fieldName: field.name, fieldType: typeName ?? '' };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function getTablesFromQuery(query) {
|
|
278
|
+
try {
|
|
279
|
+
const { Parser } = parserModule.default ?? parserModule;
|
|
280
|
+
const parser = new Parser();
|
|
281
|
+
const { tableList } = parser.parse(query);
|
|
282
|
+
const cleanTableList = tableList.map((table) => {
|
|
283
|
+
const parts = table.split('::');
|
|
284
|
+
return parts[parts.length - 1];
|
|
285
|
+
});
|
|
286
|
+
return cleanTableList;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const DEADLOCK_RETRY_DELAY_MS = 100;
|
|
293
|
+
const LOCK_TIMEOUT_RETRY_DELAY_MS = 500;
|
|
294
|
+
const MAX_EXECUTE_ATTEMPTS = 5;
|
|
295
|
+
const createsNewTransaction = connKey === true;
|
|
296
|
+
const usesExistingTransaction = !!connKey && typeof connKey === 'string';
|
|
297
|
+
if (lastConnectionCheck + 5 * 60000 <= Date.now() && !usesExistingTransaction) {
|
|
298
|
+
await ensurePool();
|
|
299
|
+
}
|
|
300
|
+
let conn;
|
|
301
|
+
try {
|
|
302
|
+
if (createsNewTransaction) {
|
|
303
|
+
const reqTr = await createTransaction();
|
|
304
|
+
if ('error' in reqTr)
|
|
305
|
+
return { error: reqTr.error };
|
|
306
|
+
connKey = reqTr.connKey;
|
|
307
|
+
conn = reqTr.conn;
|
|
308
|
+
}
|
|
309
|
+
else if (usesExistingTransaction && typeof connKey === 'string') {
|
|
310
|
+
const tr = listTransaction.get(connKey);
|
|
311
|
+
if (!tr) {
|
|
312
|
+
const msg = `Transaction key not found: ${connKey}`;
|
|
313
|
+
handleError(msg, 'MySQL: executeMySQLQuery2');
|
|
314
|
+
return { error: msg };
|
|
315
|
+
}
|
|
316
|
+
tr.lastUpdate = Date.now();
|
|
317
|
+
conn = tr.conn;
|
|
318
|
+
}
|
|
319
|
+
const { rows, fields, connKey: updatedConnKey, } = await executeWithRetry({
|
|
320
|
+
query,
|
|
321
|
+
values,
|
|
322
|
+
conn,
|
|
323
|
+
connKey: typeof connKey === 'string' ? connKey : undefined,
|
|
324
|
+
callerOwnsConnection: usesExistingTransaction,
|
|
325
|
+
createsNewTransaction,
|
|
326
|
+
});
|
|
327
|
+
if (updatedConnKey)
|
|
328
|
+
connKey = updatedConnKey;
|
|
329
|
+
if (query.trim().toLowerCase().startsWith('call')) {
|
|
330
|
+
const procError = checkStoredProcedureResult(rows, query);
|
|
331
|
+
if (procError) {
|
|
332
|
+
if (createsNewTransaction && typeof connKey === 'string') {
|
|
333
|
+
try {
|
|
334
|
+
await connectionRollback(connKey);
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
}
|
|
338
|
+
return { error: procError, values };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const fieldsType = returnFieldTypes ? resolveFieldTypes(fields) : [];
|
|
342
|
+
const tables = returnListTables ? getTablesFromQuery(query) : [];
|
|
343
|
+
if (createsNewTransaction || returnFieldTypes || returnListTables) {
|
|
344
|
+
return { rows, fieldsType, connKey, tables };
|
|
345
|
+
}
|
|
346
|
+
return rows;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
try {
|
|
350
|
+
const errorMsg = String(error?.message ?? error);
|
|
351
|
+
const queryStr = query.length > 200 ? `${query.substring(0, 200)}...` : query;
|
|
352
|
+
if (connKey && typeof connKey === 'string' && errorMsg.includes('new command when connection is in closed state')) {
|
|
353
|
+
const activeTr = listTransaction.get(connKey);
|
|
354
|
+
if (activeTr) {
|
|
355
|
+
console.log(`lastValidConn (lastUpdate : ${activeTr.lastUpdate}) : `);
|
|
356
|
+
console.log(activeTr.lastValidConn);
|
|
357
|
+
console.log(`conn (creation : ${activeTr.creation}) : `);
|
|
358
|
+
console.log(conn);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
handleError(error, `MySQL${error?.code ? ` [${error.code}]` : ''}${connKey ? ' (using connection)' : ''}\n${errorMsg}\n\nparams: ${values}\n\nquery: ${queryStr}`);
|
|
362
|
+
}
|
|
363
|
+
catch (handlingError) {
|
|
364
|
+
handleError(handlingError, 'MySQL: error inside error handler');
|
|
365
|
+
}
|
|
366
|
+
if (error?.code === 'ER_LOCK_DEADLOCK' || error?.code === 'ER_LOCK_WAIT_TIMEOUT') {
|
|
367
|
+
return { error: 'A lock error occurred, please retry', values, connKey: connKey ?? null };
|
|
368
|
+
}
|
|
369
|
+
return { error: error?.message ?? error, values, connKey: connKey ?? null };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function createTransaction() {
|
|
373
|
+
const keyConn = (0, crypto_1.randomUUID)();
|
|
374
|
+
try {
|
|
375
|
+
await ensurePool();
|
|
376
|
+
const conn = await requirePool().getConnection();
|
|
377
|
+
await conn.beginTransaction();
|
|
378
|
+
listTransaction.set(keyConn, { conn, timestamp: Date.now(), lastUpdate: Date.now(), lastValidConn: conn, creation: Date.now() });
|
|
379
|
+
return { conn, connKey: keyConn };
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
handleError(error, 'MySQL: createTransaction');
|
|
383
|
+
try {
|
|
384
|
+
listTransaction.delete(keyConn);
|
|
385
|
+
}
|
|
386
|
+
catch { }
|
|
387
|
+
return { error };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function connectionCommit(keyConn) {
|
|
391
|
+
const tr = listTransaction.get(keyConn);
|
|
392
|
+
if (!tr) {
|
|
393
|
+
const msg = `Transaction key not found: ${keyConn}`;
|
|
394
|
+
handleError(msg, 'MySQL: connectionCommit');
|
|
395
|
+
return { error: msg };
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
await tr.conn.commit();
|
|
399
|
+
await tr.conn.release();
|
|
400
|
+
listTransaction.delete(keyConn);
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
handleError(error, 'MySQL: connectionCommit');
|
|
405
|
+
return { error };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function connectionRollback(keyConn) {
|
|
409
|
+
const tr = listTransaction.get(keyConn);
|
|
410
|
+
if (!tr) {
|
|
411
|
+
handleError(`Transaction key not found: ${keyConn}`, 'MySQL: connectionRollback');
|
|
412
|
+
return { error: `Transaction key not found: ${keyConn}` };
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
await tr.conn.rollback();
|
|
416
|
+
await tr.conn.release();
|
|
417
|
+
listTransaction.delete(keyConn);
|
|
418
|
+
return {};
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return { error };
|
|
422
|
+
}
|
|
423
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@benjosivo/mysql",
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
+
"license": "MIT"
|
|
22
|
+
}
|