dynalitics-mysql 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -2
- package/dist/driver.d.ts +6 -0
- package/dist/driver.js +142 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/package.json +53 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dynalitics contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dynalitics-mysql
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The **MySQL adapter** for [dynalitics](https://www.npmjs.com/package/dynalitics). `dynalitics` itself never loads a database driver; this package is the piece that connects it to MySQL through [`mysql2`](https://sidorares.github.io/node-mysql2/docs). It depends on `mysql2` (pinned to `3.24.6`) and nothing else, so installing it brings in exactly one driver.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install dynalitics dynalitics-mysql # Node 22+, ESM only
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Pass the exported `mysql` adapter as `adapter` when you create an instance. Everything else (table, consent, IP mode, the read API) is configured on `dynalitics`; see its README.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createDynalitics } from 'dynalitics';
|
|
15
|
+
import { mysql } from 'dynalitics-mysql';
|
|
16
|
+
|
|
17
|
+
const dyn = await createDynalitics({
|
|
18
|
+
adapter: mysql,
|
|
19
|
+
connection: {
|
|
20
|
+
writer: { connectionString: process.env.ANALYTICS_WRITER_URL! }, // mysql://user:pass@host/db, INSERT only
|
|
21
|
+
reader: { connectionString: process.env.ANALYTICS_READER_URL! }, // SELECT only
|
|
22
|
+
},
|
|
23
|
+
table: 'analytics.dynalitics_events', // on MySQL the "schema" part is the database
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Create the table first with `npx dynalitics sql --dialect mysql --table analytics.dynalitics_events --grants` (prints SQL, never connects).
|
|
28
|
+
|
|
29
|
+
## What the adapter does
|
|
30
|
+
|
|
31
|
+
**Connection strings.** For each credential it creates a `mysql2/promise` pool with:
|
|
32
|
+
|
|
33
|
+
| Setting | Value | Why |
|
|
34
|
+
| ----------------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
|
|
35
|
+
| `connectionLimit` | `poolSize` (writer 4, reader 2 by default) | |
|
|
36
|
+
| `connectTimeout` | `connectTimeoutMs` (10 s by default) | |
|
|
37
|
+
| `timezone` | `'Z'` | timestamps are written and read as UTC |
|
|
38
|
+
| `dateStrings` | `true` | no time-zone conversion on the way back |
|
|
39
|
+
| `multipleStatements` | `false` | one statement per call, always |
|
|
40
|
+
| `maxPreparedStatements` | 64 | prepared statements are counted server-wide (`max_prepared_stmt_count`) |
|
|
41
|
+
|
|
42
|
+
Every statement that carries values runs as a real prepared statement (`execute`), so values are bound by the server; only the parameterless `SHOW` statements of the startup check use a plain query. Booleans are sent as `1`/`0`. Use the `ssl` options of your connection string for anything that is not on localhost.
|
|
43
|
+
|
|
44
|
+
**Your own pool.** Pass `{ pool: yourPool }` instead of a connection string. It must be a **promise** pool (`mysql2/promise`). dynalitics never closes it on `close()`.
|
|
45
|
+
|
|
46
|
+
**Deadlines.** Each `SELECT` carries a `MAX_EXECUTION_TIME` hint, so MySQL stops a slow read by itself when the read handler's deadline passes (`limits.timeoutMs`, 10 s by default). As a backstop, the adapter destroys the connection 500 ms later if the server has not answered. A pool without `getConnection()` only gets a client-side timeout; in that case set `max_execution_time` on the reader account yourself.
|
|
47
|
+
|
|
48
|
+
**Errors.** The adapter never passes a raw `mysql2` error on. Data and constraint errors (duplicate key, truncation, bad value, NULL in a NOT NULL column and similar) forward no message text at all, because MySQL prints the offending value. Table and column permission errors become a fixed sentence. Connection, authentication and timeout errors keep their message, redacted and with quoted values removed. Lost connections, deadlocks, lock wait timeouts and "too many connections" are marked transient, so the writer retries them.
|
|
49
|
+
|
|
50
|
+
## Versions
|
|
51
|
+
|
|
52
|
+
`dynalitics-mysql` and `dynalitics` are released together with the same version number, and the adapter declares `dynalitics` as a peer dependency at exactly that version. If the two ever disagree on the adapter contract, `createDynalitics` refuses to start and names both versions.
|
|
53
|
+
|
|
54
|
+
Tested against MySQL 8.4 (integration tests, local Docker). The explorer's number filters use `CAST(... AS DOUBLE)`, which needs MySQL 8.0.17 or newer (untested on older versions). MariaDB is not supported.
|
|
55
|
+
|
|
56
|
+
MIT licensed.
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Classification, type Driver, type DriverOptions } from 'dynalitics/adapter';
|
|
2
|
+
export declare function classifyMysql(err: unknown): Classification;
|
|
3
|
+
/** MySQL kills a SELECT on its own once it exceeds the hint, so a stuck query cannot outlive the deadline. */
|
|
4
|
+
export declare function withExecutionHint(sql: string, timeoutMs: number): string;
|
|
5
|
+
export declare function createMysqlDriver(o: DriverOptions): Promise<Driver>;
|
|
6
|
+
//# sourceMappingURL=driver.d.ts.map
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import mysqlModule from 'mysql2/promise';
|
|
2
|
+
import { errCode, isTransientError, toDriverError, } from 'dynalitics/adapter';
|
|
3
|
+
const TRANSIENT_CODES = new Set([
|
|
4
|
+
'ECONNRESET',
|
|
5
|
+
'ECONNREFUSED',
|
|
6
|
+
'ETIMEDOUT',
|
|
7
|
+
'EPIPE',
|
|
8
|
+
'PROTOCOL_CONNECTION_LOST',
|
|
9
|
+
'ER_LOCK_DEADLOCK',
|
|
10
|
+
'ER_LOCK_WAIT_TIMEOUT',
|
|
11
|
+
'ER_CON_COUNT_ERROR',
|
|
12
|
+
'PROTOCOL_SEQUENCE_TIMEOUT',
|
|
13
|
+
'ER_QUERY_TIMEOUT',
|
|
14
|
+
]);
|
|
15
|
+
const TIMEOUT_CODES = new Set([
|
|
16
|
+
'PROTOCOL_SEQUENCE_TIMEOUT',
|
|
17
|
+
'ER_QUERY_TIMEOUT',
|
|
18
|
+
'ETIMEDOUT',
|
|
19
|
+
]);
|
|
20
|
+
const DATA_CODES = new Set([
|
|
21
|
+
'ER_DUP_ENTRY',
|
|
22
|
+
'ER_TRUNCATED_WRONG_VALUE',
|
|
23
|
+
'ER_TRUNCATED_WRONG_VALUE_FOR_FIELD',
|
|
24
|
+
'ER_DATA_TOO_LONG',
|
|
25
|
+
'ER_BAD_NULL_ERROR',
|
|
26
|
+
'ER_WARN_DATA_OUT_OF_RANGE',
|
|
27
|
+
'ER_NO_REFERENCED_ROW_2',
|
|
28
|
+
'ER_ROW_IS_REFERENCED_2',
|
|
29
|
+
'ER_CHECK_CONSTRAINT_VIOLATED',
|
|
30
|
+
'ER_INVALID_CHARACTER_STRING',
|
|
31
|
+
'ER_WRONG_VALUE_COUNT_ON_ROW',
|
|
32
|
+
]);
|
|
33
|
+
const DATA_ERRNOS = new Set([
|
|
34
|
+
1048, 1062, 1264, 1292, 1300, 1366, 1406, 1451, 1452, 3819,
|
|
35
|
+
]);
|
|
36
|
+
const PERMISSION_CODES = new Set([
|
|
37
|
+
'ER_TABLEACCESS_DENIED_ERROR',
|
|
38
|
+
'ER_COLUMNACCESS_DENIED_ERROR',
|
|
39
|
+
]);
|
|
40
|
+
const AUTH_CODES = new Set([
|
|
41
|
+
'ER_ACCESS_DENIED_ERROR',
|
|
42
|
+
'ER_DBACCESS_DENIED_ERROR',
|
|
43
|
+
'ENOTFOUND',
|
|
44
|
+
]);
|
|
45
|
+
/** The server stops a SELECT at the hint's deadline; the client destroys the connection this long afterwards. */
|
|
46
|
+
const BACKSTOP_MS = 500;
|
|
47
|
+
const MAX_PREPARED_STATEMENTS = 64;
|
|
48
|
+
export function classifyMysql(err) {
|
|
49
|
+
const code = errCode(err);
|
|
50
|
+
const errno = err?.errno;
|
|
51
|
+
const transient = TRANSIENT_CODES.has(code);
|
|
52
|
+
return {
|
|
53
|
+
transient,
|
|
54
|
+
timeout: TIMEOUT_CODES.has(code),
|
|
55
|
+
data: DATA_CODES.has(code) || (typeof errno === 'number' && DATA_ERRNOS.has(errno)),
|
|
56
|
+
permission: PERMISSION_CODES.has(code),
|
|
57
|
+
forward: transient || AUTH_CODES.has(code),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// mysql2 prepared statements want 1/0 for booleans.
|
|
61
|
+
const toParam = (v) => (typeof v === 'boolean' ? (v ? 1 : 0) : v);
|
|
62
|
+
/** MySQL kills a SELECT on its own once it exceeds the hint, so a stuck query cannot outlive the deadline. */
|
|
63
|
+
export function withExecutionHint(sql, timeoutMs) {
|
|
64
|
+
return /^\s*SELECT\b/i.test(sql)
|
|
65
|
+
? sql.replace(/^\s*SELECT\b/i, (m) => `${m} /*+ MAX_EXECUTION_TIME(${Math.max(1, Math.trunc(timeoutMs))}) */`)
|
|
66
|
+
: sql;
|
|
67
|
+
}
|
|
68
|
+
// async on purpose: a throwing pool constructor must become a rejection, not a synchronous throw.
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
70
|
+
export async function createMysqlDriver(o) {
|
|
71
|
+
let pool;
|
|
72
|
+
let owned = false;
|
|
73
|
+
if ('pool' in o.credential) {
|
|
74
|
+
pool = o.credential.pool;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const mysql = mysqlModule;
|
|
78
|
+
pool = mysql.createPool({
|
|
79
|
+
uri: o.credential.connectionString,
|
|
80
|
+
connectionLimit: o.max,
|
|
81
|
+
connectTimeout: o.connectTimeoutMs,
|
|
82
|
+
timezone: 'Z',
|
|
83
|
+
dateStrings: true,
|
|
84
|
+
multipleStatements: false,
|
|
85
|
+
// Prepared statements are cached per connection and counted globally by the server (max_prepared_stmt_count).
|
|
86
|
+
maxPreparedStatements: MAX_PREPARED_STATEMENTS,
|
|
87
|
+
});
|
|
88
|
+
owned = true;
|
|
89
|
+
}
|
|
90
|
+
async function viaConnection(conn, sql, params, timeoutMs) {
|
|
91
|
+
let destroyed = false;
|
|
92
|
+
try {
|
|
93
|
+
// SHOW statements cannot be prepared; they take no parameters, so plain query() is safe.
|
|
94
|
+
const running = sql.startsWith('SHOW ')
|
|
95
|
+
? conn.query(sql)
|
|
96
|
+
: conn.execute({ sql: withExecutionHint(sql, timeoutMs) }, params.map(toParam));
|
|
97
|
+
running.catch(() => undefined);
|
|
98
|
+
const [rows] = await new Promise((resolve, reject) => {
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
destroyed = true;
|
|
101
|
+
conn.destroy();
|
|
102
|
+
reject(Object.assign(new Error('Query execution was interrupted: deadline exceeded'), {
|
|
103
|
+
code: 'ER_QUERY_TIMEOUT',
|
|
104
|
+
}));
|
|
105
|
+
}, timeoutMs + BACKSTOP_MS);
|
|
106
|
+
running.then((r) => (clearTimeout(timer), resolve(r)), (e) => (clearTimeout(timer), reject(e)));
|
|
107
|
+
});
|
|
108
|
+
return rows;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (!destroyed)
|
|
112
|
+
conn.release(); // never hand a destroyed connection back to the pool
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
async query(q, { timeoutMs }) {
|
|
117
|
+
try {
|
|
118
|
+
let rows;
|
|
119
|
+
if (pool.getConnection === undefined) {
|
|
120
|
+
// Host pools without getConnection(): client-side deadline only (the host must set max_execution_time).
|
|
121
|
+
[rows] =
|
|
122
|
+
q.sql.startsWith('SHOW ') && pool.query !== undefined
|
|
123
|
+
? await pool.query(q.sql)
|
|
124
|
+
: await pool.execute({ sql: withExecutionHint(q.sql, timeoutMs), timeout: timeoutMs }, q.params.map(toParam));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
rows = await viaConnection(await pool.getConnection(), q.sql, q.params, timeoutMs);
|
|
128
|
+
}
|
|
129
|
+
return Array.isArray(rows) ? rows : [];
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw toDriverError(err, o.redact, classifyMysql);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
isTransient: isTransientError,
|
|
136
|
+
async close() {
|
|
137
|
+
if (owned)
|
|
138
|
+
await pool.end?.();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=driver.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DriverAdapter } from 'dynalitics/adapter';
|
|
2
|
+
/**
|
|
3
|
+
* The MySQL adapter for dynalitics: `createDynalitics({ adapter: mysql, ... })`.
|
|
4
|
+
* apiVersion is a literal on purpose: it states the contract THIS package was built for (spec 5.2).
|
|
5
|
+
*/
|
|
6
|
+
export declare const mysql: DriverAdapter;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createMysqlDriver } from './driver.js';
|
|
2
|
+
/**
|
|
3
|
+
* The MySQL adapter for dynalitics: `createDynalitics({ adapter: mysql, ... })`.
|
|
4
|
+
* apiVersion is a literal on purpose: it states the contract THIS package was built for (spec 5.2).
|
|
5
|
+
*/
|
|
6
|
+
export const mysql = Object.freeze({
|
|
7
|
+
apiVersion: 1,
|
|
8
|
+
dialect: 'mysql',
|
|
9
|
+
packageName: 'dynalitics-mysql',
|
|
10
|
+
createDriver: createMysqlDriver,
|
|
11
|
+
});
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dynalitics-mysql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MySQL adapter for dynalitics: brings in the mysql2 driver and nothing else.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"analytics",
|
|
7
|
+
"dynalitics",
|
|
8
|
+
"mysql",
|
|
9
|
+
"mysql2",
|
|
10
|
+
"adapter"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "dynalitics contributors",
|
|
14
|
+
"homepage": "https://github.com/lucsoftsl/dynalitics/tree/main/packages/dynalitics-mysql#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/lucsoftsl/dynalitics/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/lucsoftsl/dynalitics.git",
|
|
21
|
+
"directory": "packages/dynalitics-mysql"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"!dist/**/*.map",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -b"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"mysql2": "3.24.6"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"dynalitics": "0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"provenance": true
|
|
54
|
+
}
|
|
55
|
+
}
|