@byline/db-mysql 4.8.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 +373 -0
- package/README.md +283 -0
- package/dist/database/schema/auth.d.ts +951 -0
- package/dist/database/schema/auth.js +226 -0
- package/dist/database/schema/common.d.ts +168 -0
- package/dist/database/schema/common.js +130 -0
- package/dist/database/schema/index.d.ts +3419 -0
- package/dist/database/schema/index.js +895 -0
- package/dist/database/schema/schema-pins.test.node.d.ts +52 -0
- package/dist/database/schema/schema-pins.test.node.js +398 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +151 -0
- package/dist/lib/boot-check.d.ts +22 -0
- package/dist/lib/boot-check.js +42 -0
- package/dist/lib/boot-check.test.node.d.ts +8 -0
- package/dist/lib/boot-check.test.node.js +46 -0
- package/dist/lib/db-manager.d.ts +54 -0
- package/dist/lib/db-manager.js +49 -0
- package/dist/lib/test-db.d.ts +37 -0
- package/dist/lib/test-db.js +111 -0
- package/dist/lib/test-helper.d.ts +33 -0
- package/dist/lib/test-helper.js +68 -0
- package/dist/modules/admin/admin-permissions-repository.d.ts +35 -0
- package/dist/modules/admin/admin-permissions-repository.js +101 -0
- package/dist/modules/admin/admin-preferences-repository.d.ts +62 -0
- package/dist/modules/admin/admin-preferences-repository.js +156 -0
- package/dist/modules/admin/admin-roles-repository.d.ts +11 -0
- package/dist/modules/admin/admin-roles-repository.js +209 -0
- package/dist/modules/admin/admin-store.d.ts +20 -0
- package/dist/modules/admin/admin-store.js +30 -0
- package/dist/modules/admin/admin-users-repository.d.ts +11 -0
- package/dist/modules/admin/admin-users-repository.js +303 -0
- package/dist/modules/admin/index.d.ts +27 -0
- package/dist/modules/admin/index.js +27 -0
- package/dist/modules/admin/refresh-tokens-repository.d.ts +34 -0
- package/dist/modules/admin/refresh-tokens-repository.js +160 -0
- package/dist/modules/audit/audit-commands.d.ts +29 -0
- package/dist/modules/audit/audit-commands.js +40 -0
- package/dist/modules/audit/audit-queries.d.ts +41 -0
- package/dist/modules/audit/audit-queries.js +169 -0
- package/dist/modules/counters/counters-commands.d.ts +54 -0
- package/dist/modules/counters/counters-commands.js +121 -0
- package/dist/modules/counters/tests/counters-concurrency.test.d.ts +8 -0
- package/dist/modules/counters/tests/counters-concurrency.test.js +111 -0
- package/dist/modules/storage/classify-error.d.ts +34 -0
- package/dist/modules/storage/classify-error.js +50 -0
- package/dist/modules/storage/classify-error.test.node.d.ts +8 -0
- package/dist/modules/storage/classify-error.test.node.js +89 -0
- package/dist/modules/storage/normalize-row.d.ts +55 -0
- package/dist/modules/storage/normalize-row.js +135 -0
- package/dist/modules/storage/normalize-row.test.node.d.ts +8 -0
- package/dist/modules/storage/normalize-row.test.node.js +89 -0
- package/dist/modules/storage/storage-commands.d.ts +443 -0
- package/dist/modules/storage/storage-commands.js +1263 -0
- package/dist/modules/storage/storage-insert.d.ts +21 -0
- package/dist/modules/storage/storage-insert.js +152 -0
- package/dist/modules/storage/storage-queries.d.ts +805 -0
- package/dist/modules/storage/storage-queries.js +1815 -0
- package/dist/modules/storage/storage-store-manifest.d.ts +77 -0
- package/dist/modules/storage/storage-store-manifest.js +168 -0
- package/dist/modules/storage/storage-utils.d.ts +49 -0
- package/dist/modules/storage/storage-utils.js +76 -0
- package/dist/modules/storage/tests/dialect-pins.integration.test.d.ts +8 -0
- package/dist/modules/storage/tests/dialect-pins.integration.test.js +266 -0
- package/dist/modules/storage/tests/storage-commands.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-commands.test.js +324 -0
- package/dist/modules/storage/tests/storage-document-paths.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-paths.test.js +214 -0
- package/dist/modules/storage/tests/storage-document-tree.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-tree.test.js +361 -0
- package/dist/modules/storage/tests/storage-queries.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-queries.test.js +685 -0
- package/dist/modules/storage/tests/storage-status-and-lifecycle.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-status-and-lifecycle.test.js +268 -0
- package/package.json +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { drizzle } from 'drizzle-orm/mysql2';
|
|
9
|
+
import mysql from 'mysql2/promise';
|
|
10
|
+
import * as schema from './database/schema/index.js';
|
|
11
|
+
import { assertMySqlVersion } from './lib/boot-check.js';
|
|
12
|
+
import { DBManagerImpl, TXManagerImpl } from './lib/db-manager.js';
|
|
13
|
+
import { createAuditCommands } from './modules/audit/audit-commands.js';
|
|
14
|
+
import { createAuditQueries } from './modules/audit/audit-queries.js';
|
|
15
|
+
import { createCounterCommands } from './modules/counters/counters-commands.js';
|
|
16
|
+
import { classifyError } from './modules/storage/classify-error.js';
|
|
17
|
+
import { createCommandBuilders } from './modules/storage/storage-commands.js';
|
|
18
|
+
import { createQueryBuilders } from './modules/storage/storage-queries.js';
|
|
19
|
+
export const mysqlAdapter = ({ connectionString, collections, defaultContentLocale, connectionLimit = 20, idleTimeout = 2000, connectTimeout = 30000, maxIdle, }) => {
|
|
20
|
+
const pool = mysql.createPool({
|
|
21
|
+
uri: connectionString,
|
|
22
|
+
connectionLimit,
|
|
23
|
+
idleTimeout,
|
|
24
|
+
connectTimeout,
|
|
25
|
+
...(maxIdle != null ? { maxIdle } : {}),
|
|
26
|
+
// Every DATETIME column is UTC by convention (spec §2). 'Z' stops
|
|
27
|
+
// mysql2 from reinterpreting stored UTC values against the server's or
|
|
28
|
+
// session's local timezone on the way in and out.
|
|
29
|
+
timezone: 'Z',
|
|
30
|
+
// Keep DECIMAL columns as strings instead of coercing to JS `number`,
|
|
31
|
+
// which loses precision on money/decimal values. The storage layer
|
|
32
|
+
// (Task 9) treats decimal store values as strings end to end, matching
|
|
33
|
+
// the pg adapter's `numeric` handling.
|
|
34
|
+
decimalNumbers: false,
|
|
35
|
+
// Task 10A divergence, found live: mysql2 negotiates
|
|
36
|
+
// `utf8mb4_unicode_ci` as the connection's default collation unless told
|
|
37
|
+
// otherwise — it does NOT inherit the schema/database default
|
|
38
|
+
// (`utf8mb4_0900_ai_ci`, see `database/schema/common.ts`). A typed
|
|
39
|
+
// `CAST(NULL AS CHAR)` expression (the UNION ALL null-cast machinery in
|
|
40
|
+
// `storage-store-manifest.ts`) carries the *connection's* collation, so
|
|
41
|
+
// without this, `getAllFieldValuesForMultipleVersions`'s 7-way UNION ALL
|
|
42
|
+
// fails with `ER_CANT_AGGREGATE_NCOLLATIONS` ("Illegal mix of
|
|
43
|
+
// collations") the moment a CAST'd column and a real schema column with
|
|
44
|
+
// a different collation land in the same UNION output position.
|
|
45
|
+
// Pinning the connection's collation to match the schema fixes it at
|
|
46
|
+
// the source rather than adding a `COLLATE` clause to every cast.
|
|
47
|
+
charset: 'UTF8MB4_0900_AI_CI',
|
|
48
|
+
});
|
|
49
|
+
// `drizzle-orm/mysql2` requires `mode` whenever `schema` is supplied.
|
|
50
|
+
// 'default' matches the pg adapter's un-prefixed-key mode (as opposed to
|
|
51
|
+
// 'planetscale', which changes how relational queries build).
|
|
52
|
+
const db = drizzle(pool, {
|
|
53
|
+
schema,
|
|
54
|
+
mode: 'default',
|
|
55
|
+
});
|
|
56
|
+
// Request-scoped transaction propagation (docs/03-architecture/03-transactions.md), mirroring
|
|
57
|
+
// the pg adapter. Exported via ./lib/db-manager.js for Tasks 9-12: command
|
|
58
|
+
// builders land on the DBManager so each `this.db` access resolves to the
|
|
59
|
+
// ambient transaction when a `withTransaction` boundary is open, else the
|
|
60
|
+
// pool.
|
|
61
|
+
const dbManager = new DBManagerImpl({ dbPool: db });
|
|
62
|
+
const txManager = new TXManagerImpl({ db: dbManager });
|
|
63
|
+
const commandBuilders = createCommandBuilders(dbManager, defaultContentLocale);
|
|
64
|
+
// Most reads run on the raw `db` (not the DBManager) — they don't need to
|
|
65
|
+
// join an ambient `withTransaction`. `dbManager` is still threaded through
|
|
66
|
+
// as the 4th argument (matching pg's `storage-queries.ts`) because
|
|
67
|
+
// `DocumentQueries` accepts it as `transactionDb` for the one read that
|
|
68
|
+
// DOES need the ambient transaction: `getDocumentSystemFieldsForUpdate`
|
|
69
|
+
// (Task 10B) takes a `SELECT … FOR UPDATE` lock that must run inside the
|
|
70
|
+
// caller's transaction to serialise concurrent system-field writers —
|
|
71
|
+
// dropping `dbManager` here would silently run that lock outside the
|
|
72
|
+
// transaction and defeat the concurrency guard it exists to provide.
|
|
73
|
+
// `transactionDb` is a required parameter (no default) precisely so this
|
|
74
|
+
// can never be omitted by accident — see the §H ruling in the Task 10B
|
|
75
|
+
// report.
|
|
76
|
+
const queryBuilders = createQueryBuilders(db, collections, defaultContentLocale, dbManager);
|
|
77
|
+
// Counters run on the raw mysql2 `pool` — never `dbManager` — so they never
|
|
78
|
+
// join an ambient `withTransaction`: a long document-create transaction
|
|
79
|
+
// holding the counter row would serialise every other writer in that
|
|
80
|
+
// group (Task 11). Audit appends run on `dbManager` so they DO join the
|
|
81
|
+
// ambient transaction and commit atomically with the mutation they
|
|
82
|
+
// record; audit reads run on the plain `db` (the pool) since they never
|
|
83
|
+
// need to join that transaction. See ./modules/counters/counters-commands.js
|
|
84
|
+
// and ./modules/audit/{audit-commands,audit-queries}.js.
|
|
85
|
+
const counterCommands = createCounterCommands(pool);
|
|
86
|
+
const auditCommands = createAuditCommands(dbManager);
|
|
87
|
+
const auditQueries = createAuditQueries(db);
|
|
88
|
+
// Boot check: run lazily on the pool's first physical connection rather
|
|
89
|
+
// than eagerly here, because `mysqlAdapter` is synchronous (mirroring
|
|
90
|
+
// `pgAdapter`) and cannot await a round trip before returning. mysql2
|
|
91
|
+
// pools open no connections at construction time — the `'connection'`
|
|
92
|
+
// event fires the first time a query actually needs one, which in
|
|
93
|
+
// practice is during `initBylineCore()`'s own boot sequence. A too-old
|
|
94
|
+
// server or MariaDB is a configuration error, not a recoverable runtime
|
|
95
|
+
// condition, so a failed check is rethrown on the next tick to surface as
|
|
96
|
+
// a loud, fail-fast crash instead of a silently swallowed rejection.
|
|
97
|
+
//
|
|
98
|
+
// The `mysql2/promise` typings claim this event hands back a
|
|
99
|
+
// promise-wrapped `PoolConnection`, but at runtime it is the underlying
|
|
100
|
+
// callback-style connection (confirmed against a live server) — calling
|
|
101
|
+
// `.query()` on it directly returns an `EventEmitter`, not a `Promise`.
|
|
102
|
+
// `.promise()` is the callback API's own escape hatch back to the
|
|
103
|
+
// promise wrapper; see https://sidorares.github.io/node-mysql2/docs/documentation/promise-wrapper.
|
|
104
|
+
let versionCheckStarted = false;
|
|
105
|
+
pool.on('connection', (connection) => {
|
|
106
|
+
if (versionCheckStarted)
|
|
107
|
+
return;
|
|
108
|
+
versionCheckStarted = true;
|
|
109
|
+
const rawConnection = connection;
|
|
110
|
+
const promiseConnection = rawConnection.promise();
|
|
111
|
+
assertMySqlVersion(async (sql) => {
|
|
112
|
+
const [rows] = await promiseConnection.query(sql);
|
|
113
|
+
return rows;
|
|
114
|
+
}).catch((err) => {
|
|
115
|
+
process.nextTick(() => {
|
|
116
|
+
throw err;
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
commands: {
|
|
122
|
+
// `commandBuilders.collections` fully implements `ICollectionCommands`
|
|
123
|
+
// (Task 9A) — see `./modules/storage/storage-commands.js`.
|
|
124
|
+
collections: commandBuilders.collections,
|
|
125
|
+
// `commandBuilders.documents` (`DocumentCommands`) fully implements
|
|
126
|
+
// `IDocumentCommands` as of Task 9B — see that class's docblock.
|
|
127
|
+
documents: commandBuilders.documents,
|
|
128
|
+
// `counterCommands` fully implements `ICounterCommands` as of Task 11
|
|
129
|
+
// — see `./modules/counters/counters-commands.js`.
|
|
130
|
+
counters: counterCommands,
|
|
131
|
+
// `auditCommands` fully implements `IAuditCommands` as of Task 11 —
|
|
132
|
+
// see `./modules/audit/audit-commands.js`.
|
|
133
|
+
audit: auditCommands,
|
|
134
|
+
},
|
|
135
|
+
queries: {
|
|
136
|
+
// `queryBuilders.collections` fully implements `ICollectionQueries`
|
|
137
|
+
// (Task 10A) — see `./modules/storage/storage-queries.js`.
|
|
138
|
+
collections: queryBuilders.collections,
|
|
139
|
+
// `queryBuilders.documents` (`DocumentQueries`) fully implements
|
|
140
|
+
// `IDocumentQueries` as of Task 10B — see that class's docblock.
|
|
141
|
+
documents: queryBuilders.documents,
|
|
142
|
+
// `auditQueries` fully implements `IAuditQueries` as of Task 11 — see
|
|
143
|
+
// `./modules/audit/audit-queries.js`.
|
|
144
|
+
audit: auditQueries,
|
|
145
|
+
},
|
|
146
|
+
withTransaction: (fn) => txManager.withTransaction(fn),
|
|
147
|
+
classifyError,
|
|
148
|
+
drizzle: db,
|
|
149
|
+
pool,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*
|
|
8
|
+
* Boot-time engine check for the MySQL adapter. `mysqlAdapter` runs this
|
|
9
|
+
* lazily against the first connection the pool hands out (see
|
|
10
|
+
* `src/index.ts`) so a too-old server or a MariaDB instance fails fast at
|
|
11
|
+
* `initBylineCore()` boot rather than surfacing as an obscure SQL error the
|
|
12
|
+
* first time the storage layer emits a LATERAL join (Task 10+).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Assert the connected server is MySQL (not MariaDB) at or above the
|
|
16
|
+
* supported floor. `query` is injected so this is unit-testable without a
|
|
17
|
+
* live server — the adapter passes a thin wrapper around the pool's
|
|
18
|
+
* `SELECT VERSION()` round trip.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertMySqlVersion(query: (sql: string) => Promise<Array<{
|
|
21
|
+
v: string;
|
|
22
|
+
}>>): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*
|
|
8
|
+
* Boot-time engine check for the MySQL adapter. `mysqlAdapter` runs this
|
|
9
|
+
* lazily against the first connection the pool hands out (see
|
|
10
|
+
* `src/index.ts`) so a too-old server or a MariaDB instance fails fast at
|
|
11
|
+
* `initBylineCore()` boot rather than surfacing as an obscure SQL error the
|
|
12
|
+
* first time the storage layer emits a LATERAL join (Task 10+).
|
|
13
|
+
*/
|
|
14
|
+
const MIN = { major: 8, minor: 0, patch: 14 };
|
|
15
|
+
const unsupportedEngineError = (reported) => new Error(`@byline/db-mysql requires MySQL ${MIN.major}.${MIN.minor}.${MIN.patch}+ (LATERAL joins); server reports ${reported}. MariaDB is not supported.`);
|
|
16
|
+
/**
|
|
17
|
+
* Assert the connected server is MySQL (not MariaDB) at or above the
|
|
18
|
+
* supported floor. `query` is injected so this is unit-testable without a
|
|
19
|
+
* live server — the adapter passes a thin wrapper around the pool's
|
|
20
|
+
* `SELECT VERSION()` round trip.
|
|
21
|
+
*/
|
|
22
|
+
export async function assertMySqlVersion(query) {
|
|
23
|
+
const rows = await query('SELECT VERSION() AS v');
|
|
24
|
+
const v = rows?.[0]?.v;
|
|
25
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
26
|
+
throw new Error("@byline/db-mysql: could not determine the MySQL server version — 'SELECT VERSION()' returned no usable result.");
|
|
27
|
+
}
|
|
28
|
+
// MariaDB reports version strings that satisfy the numeric floor below
|
|
29
|
+
// (it commonly claims a 10.x/11.x series, sometimes fronted by the
|
|
30
|
+
// legacy `5.5.5-` replication-handshake prefix), so it must be rejected
|
|
31
|
+
// explicitly before the numeric comparison, not merely by chance.
|
|
32
|
+
if (/mariadb/i.test(v)) {
|
|
33
|
+
throw unsupportedEngineError(v);
|
|
34
|
+
}
|
|
35
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
36
|
+
const [major = 0, minor = 0, patch = 0] = m ? m.slice(1).map(Number) : [];
|
|
37
|
+
const ok = major > MIN.major ||
|
|
38
|
+
(major === MIN.major && (minor > MIN.minor || (minor === MIN.minor && patch >= MIN.patch)));
|
|
39
|
+
if (!ok) {
|
|
40
|
+
throw unsupportedEngineError(v);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { assertMySqlVersion } from './boot-check.js';
|
|
10
|
+
const queryReturning = (v) => async () => [{ v }];
|
|
11
|
+
describe('assertMySqlVersion', () => {
|
|
12
|
+
describe('accepts supported MySQL server versions', () => {
|
|
13
|
+
it.each([
|
|
14
|
+
['8.0.14', '8.0.14 — the exact floor'],
|
|
15
|
+
['8.4.0', '8.4.0'],
|
|
16
|
+
['9.7.1', "9.7.1 — this repo's dev container"],
|
|
17
|
+
['8.0.35-0ubuntu0.22.04.1', 'a distro-suffixed release string'],
|
|
18
|
+
])('%s (%s)', async (version) => {
|
|
19
|
+
await expect(assertMySqlVersion(queryReturning(version))).resolves.toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('rejects unsupported servers', () => {
|
|
23
|
+
it.each([
|
|
24
|
+
['8.0.13', 'one patch below the floor'],
|
|
25
|
+
['8.0.0', 'the 8.0 GA release, below the floor'],
|
|
26
|
+
['5.7.44', 'MySQL 5.7'],
|
|
27
|
+
['11.4.2-MariaDB', 'a MariaDB version string'],
|
|
28
|
+
['5.5.5-10.11.2-MariaDB', 'the MariaDB replication-handshake version shape'],
|
|
29
|
+
])('%s (%s)', async (version) => {
|
|
30
|
+
await expect(assertMySqlVersion(queryReturning(version))).rejects.toThrow(/MariaDB is not supported/);
|
|
31
|
+
});
|
|
32
|
+
it('names MariaDB explicitly, not just a failed numeric comparison', async () => {
|
|
33
|
+
await expect(assertMySqlVersion(queryReturning('11.4.2-MariaDB'))).rejects.toThrow(/MariaDB/);
|
|
34
|
+
await expect(assertMySqlVersion(queryReturning('5.5.5-10.11.2-MariaDB'))).rejects.toThrow(/MariaDB/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('malformed results', () => {
|
|
38
|
+
it('throws a clear error (not a destructuring TypeError) when the query returns no rows', async () => {
|
|
39
|
+
await expect(assertMySqlVersion(async () => [])).rejects.toThrow(/@byline\/db-mysql/);
|
|
40
|
+
await expect(assertMySqlVersion(async () => [])).rejects.not.toThrow(TypeError);
|
|
41
|
+
});
|
|
42
|
+
it('throws a clear error when the query returns a row without a usable version string', async () => {
|
|
43
|
+
await expect(assertMySqlVersion(async () => [{ v: undefined }])).rejects.toThrow(/@byline\/db-mysql/);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*
|
|
8
|
+
* Request-scoped transaction propagation via AsyncLocalStorage.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the Postgres adapter's `db-manager.ts` — the same ALS mechanism
|
|
11
|
+
* Byline's logger already uses (`packages/core/src/lib/logger.ts`,
|
|
12
|
+
* `withLogContext`). The full design — the service-owned `withTransaction`
|
|
13
|
+
* boundary, the DB↔DB vs DB↔external distinction, the incremental-adoption
|
|
14
|
+
* caveat, and the serverless db-contract-seam decisions — lives in
|
|
15
|
+
* `docs/03-architecture/03-transactions.md`. This machinery is deliberately adapter-internal:
|
|
16
|
+
* transactions are driver-specific, so `@byline/core` only declares the
|
|
17
|
+
* `withTransaction` capability on `IDbAdapter`, never the implementation.
|
|
18
|
+
*/
|
|
19
|
+
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
|
20
|
+
import type * as schema from '../database/schema/index.js';
|
|
21
|
+
export type DBExecutor = MySql2Database<typeof schema>;
|
|
22
|
+
export interface DBManager {
|
|
23
|
+
/**
|
|
24
|
+
* The current executor: the ambient transaction when a `withTransaction`
|
|
25
|
+
* boundary is open in this async context, otherwise the pool.
|
|
26
|
+
*/
|
|
27
|
+
get(): DBExecutor;
|
|
28
|
+
}
|
|
29
|
+
export declare class DBManagerImpl implements DBManager {
|
|
30
|
+
private readonly dbPool;
|
|
31
|
+
constructor(deps: {
|
|
32
|
+
dbPool: DBExecutor;
|
|
33
|
+
});
|
|
34
|
+
get(): DBExecutor;
|
|
35
|
+
}
|
|
36
|
+
export interface TXManager {
|
|
37
|
+
/**
|
|
38
|
+
* Run `fn` inside a single database transaction. Every `DBManager.get()`
|
|
39
|
+
* call made during `fn` (transitively, across `await`s) returns that
|
|
40
|
+
* transaction, so the commands `fn` invokes commit or roll back together.
|
|
41
|
+
*
|
|
42
|
+
* Nesting: when already inside a `withTransaction`, the inner call opens a
|
|
43
|
+
* SAVEPOINT (Drizzle nested transaction) — an inner throw rolls back to the
|
|
44
|
+
* savepoint, an outer throw rolls back everything.
|
|
45
|
+
*/
|
|
46
|
+
withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
47
|
+
}
|
|
48
|
+
export declare class TXManagerImpl implements TXManager {
|
|
49
|
+
private readonly db;
|
|
50
|
+
constructor(deps: {
|
|
51
|
+
db: DBManager;
|
|
52
|
+
});
|
|
53
|
+
withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*
|
|
8
|
+
* Request-scoped transaction propagation via AsyncLocalStorage.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the Postgres adapter's `db-manager.ts` — the same ALS mechanism
|
|
11
|
+
* Byline's logger already uses (`packages/core/src/lib/logger.ts`,
|
|
12
|
+
* `withLogContext`). The full design — the service-owned `withTransaction`
|
|
13
|
+
* boundary, the DB↔DB vs DB↔external distinction, the incremental-adoption
|
|
14
|
+
* caveat, and the serverless db-contract-seam decisions — lives in
|
|
15
|
+
* `docs/03-architecture/03-transactions.md`. This machinery is deliberately adapter-internal:
|
|
16
|
+
* transactions are driver-specific, so `@byline/core` only declares the
|
|
17
|
+
* `withTransaction` capability on `IDbAdapter`, never the implementation.
|
|
18
|
+
*/
|
|
19
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
20
|
+
const transactionALS = new AsyncLocalStorage();
|
|
21
|
+
export class DBManagerImpl {
|
|
22
|
+
dbPool;
|
|
23
|
+
constructor(deps) {
|
|
24
|
+
this.dbPool = deps.dbPool;
|
|
25
|
+
}
|
|
26
|
+
get() {
|
|
27
|
+
return transactionALS.getStore() ?? this.dbPool;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class TXManagerImpl {
|
|
31
|
+
db;
|
|
32
|
+
constructor(deps) {
|
|
33
|
+
this.db = deps.db;
|
|
34
|
+
}
|
|
35
|
+
withTransaction(fn) {
|
|
36
|
+
return this.db.get().transaction((tx) =>
|
|
37
|
+
// `tx` is Drizzle's MySqlTransaction; it carries the full
|
|
38
|
+
// query-builder surface every command uses. The cast bridges the
|
|
39
|
+
// structural gap to MySql2Database the same way the pg adapter
|
|
40
|
+
// bridges to NodePgDatabase. See docs/03-architecture/03-transactions.md.
|
|
41
|
+
transactionALS.run(tx, fn), {
|
|
42
|
+
// Spec decision: pin the isolation level to match the pg adapter's
|
|
43
|
+
// assumptions (Postgres's default is READ COMMITTED; MySQL/InnoDB
|
|
44
|
+
// defaults to REPEATABLE READ). Canonical adapters must behave
|
|
45
|
+
// identically for the shared db-conformance suite.
|
|
46
|
+
isolationLevel: 'read committed',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { type MySql2Database } from 'drizzle-orm/mysql2';
|
|
9
|
+
import * as schema from '../database/schema/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Belt for the script-level braces in `common.sh`. Parses the connection
|
|
12
|
+
* string and refuses to continue unless the database name ends in `_test`.
|
|
13
|
+
* Called at every test-process entry point so a stray `.env` pointed at
|
|
14
|
+
* `byline_dev` (or anything else) trips the guard before any DDL runs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertTestDatabase(connectionString: string | undefined): string;
|
|
17
|
+
/**
|
|
18
|
+
* Run Drizzle migrations against the configured connection. Idempotent —
|
|
19
|
+
* Drizzle tracks applied migrations in `__drizzle_migrations`. Opens and
|
|
20
|
+
* closes its own pool; safe to call from a vitest globalSetup.
|
|
21
|
+
*/
|
|
22
|
+
export declare function migrateTestDatabase(connectionString: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Wipe every user table so each test file starts from a known state. MySQL
|
|
25
|
+
* has no `TRUNCATE ... CASCADE` — foreign keys must be disabled around the
|
|
26
|
+
* truncates instead, and restored afterwards (including on failure), or a
|
|
27
|
+
* later test file inherits a permanently-disabled FK session setting.
|
|
28
|
+
* Skips Drizzle's own `__drizzle_migrations` ledger. Self-maintaining as the
|
|
29
|
+
* schema grows — new tables come along for the ride without any code change.
|
|
30
|
+
*/
|
|
31
|
+
export declare function truncateAllTables(db: MySql2Database<typeof schema>): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Convenience: assert + open a short-lived pool + truncate + close. Useful
|
|
34
|
+
* from a vitest setupFile (`beforeAll`) where the caller doesn't otherwise
|
|
35
|
+
* need a long-lived db handle.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resetTestDatabase(connectionString: string): Promise<void>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { drizzle } from 'drizzle-orm/mysql2';
|
|
11
|
+
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
|
12
|
+
import mysql from 'mysql2/promise';
|
|
13
|
+
import * as schema from '../database/schema/index.js';
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
/**
|
|
16
|
+
* Drizzle migrations folder. Migrations (`*.sql` + `meta/_journal.json`)
|
|
17
|
+
* live only under `src/` — the TypeScript build doesn't copy them into
|
|
18
|
+
* `dist/`. Anchor on `src/database/migrations` from either location:
|
|
19
|
+
*
|
|
20
|
+
* src/lib/test-db.ts → ../../src/database/migrations ✓
|
|
21
|
+
* dist/lib/test-db.js → ../../src/database/migrations ✓
|
|
22
|
+
*
|
|
23
|
+
* `path.resolve` normalises the `../..` away, so the same string works
|
|
24
|
+
* for both build modes. Mirrors `packages/db-postgres/src/lib/test-db.ts`.
|
|
25
|
+
*/
|
|
26
|
+
const MIGRATIONS_FOLDER = path.resolve(__dirname, '../../src/database/migrations');
|
|
27
|
+
/**
|
|
28
|
+
* Belt for the script-level braces in `common.sh`. Parses the connection
|
|
29
|
+
* string and refuses to continue unless the database name ends in `_test`.
|
|
30
|
+
* Called at every test-process entry point so a stray `.env` pointed at
|
|
31
|
+
* `byline_dev` (or anything else) trips the guard before any DDL runs.
|
|
32
|
+
*/
|
|
33
|
+
export function assertTestDatabase(connectionString) {
|
|
34
|
+
if (!connectionString) {
|
|
35
|
+
throw new Error('BYLINE_DB_MYSQL_CONNECTION_STRING is not set. Copy .env.test.example to .env.test.');
|
|
36
|
+
}
|
|
37
|
+
let dbName;
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(connectionString);
|
|
40
|
+
dbName = url.pathname.replace(/^\//, '');
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw new Error(`BYLINE_DB_MYSQL_CONNECTION_STRING is not a valid URL: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
if (!dbName.endsWith('_test')) {
|
|
46
|
+
throw new Error(`Refusing to run tests against database '${dbName}'. ` +
|
|
47
|
+
`Integration tests require a database whose name ends in '_test'. ` +
|
|
48
|
+
`Update BYLINE_DB_MYSQL_CONNECTION_STRING in .env.test.`);
|
|
49
|
+
}
|
|
50
|
+
return dbName;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run Drizzle migrations against the configured connection. Idempotent —
|
|
54
|
+
* Drizzle tracks applied migrations in `__drizzle_migrations`. Opens and
|
|
55
|
+
* closes its own pool; safe to call from a vitest globalSetup.
|
|
56
|
+
*/
|
|
57
|
+
export async function migrateTestDatabase(connectionString) {
|
|
58
|
+
assertTestDatabase(connectionString);
|
|
59
|
+
const pool = mysql.createPool({ uri: connectionString, connectionLimit: 1, timezone: 'Z' });
|
|
60
|
+
try {
|
|
61
|
+
const db = drizzle(pool, { schema, mode: 'default' });
|
|
62
|
+
await migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await pool.end();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Wipe every user table so each test file starts from a known state. MySQL
|
|
70
|
+
* has no `TRUNCATE ... CASCADE` — foreign keys must be disabled around the
|
|
71
|
+
* truncates instead, and restored afterwards (including on failure), or a
|
|
72
|
+
* later test file inherits a permanently-disabled FK session setting.
|
|
73
|
+
* Skips Drizzle's own `__drizzle_migrations` ledger. Self-maintaining as the
|
|
74
|
+
* schema grows — new tables come along for the ride without any code change.
|
|
75
|
+
*/
|
|
76
|
+
export async function truncateAllTables(db) {
|
|
77
|
+
const [rows] = await db.execute(`
|
|
78
|
+
SELECT TABLE_NAME AS table_name FROM information_schema.tables
|
|
79
|
+
WHERE table_schema = DATABASE()
|
|
80
|
+
AND table_type = 'BASE TABLE'
|
|
81
|
+
AND table_name <> '__drizzle_migrations'
|
|
82
|
+
`);
|
|
83
|
+
const tables = rows.map((r) => `\`${r.table_name}\``);
|
|
84
|
+
if (tables.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
await db.execute('SET FOREIGN_KEY_CHECKS = 0');
|
|
87
|
+
try {
|
|
88
|
+
for (const table of tables) {
|
|
89
|
+
await db.execute(`TRUNCATE TABLE ${table}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await db.execute('SET FOREIGN_KEY_CHECKS = 1');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Convenience: assert + open a short-lived pool + truncate + close. Useful
|
|
98
|
+
* from a vitest setupFile (`beforeAll`) where the caller doesn't otherwise
|
|
99
|
+
* need a long-lived db handle.
|
|
100
|
+
*/
|
|
101
|
+
export async function resetTestDatabase(connectionString) {
|
|
102
|
+
assertTestDatabase(connectionString);
|
|
103
|
+
const pool = mysql.createPool({ uri: connectionString, connectionLimit: 1, timezone: 'Z' });
|
|
104
|
+
try {
|
|
105
|
+
const db = drizzle(pool, { schema, mode: 'default' });
|
|
106
|
+
await truncateAllTables(db);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await pool.end();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { CollectionDefinition } from '@byline/core';
|
|
9
|
+
import { type MySql2Database } from 'drizzle-orm/mysql2';
|
|
10
|
+
import mysql from 'mysql2/promise';
|
|
11
|
+
import * as schema from '../database/schema/index.js';
|
|
12
|
+
import { DBManagerImpl, TXManagerImpl } from './db-manager.js';
|
|
13
|
+
/**
|
|
14
|
+
* Mirrors `packages/db-postgres/src/lib/test-helper.ts`. `queryBuilders` is
|
|
15
|
+
* wired as of Task 10A (the UNION ALL reconstruction path) and fully
|
|
16
|
+
* implements `IDocumentQueries` as of Task 10B (the predicate compiler,
|
|
17
|
+
* `findDocuments`' full surface, tree reads, and `getDocumentSystemFieldsForUpdate`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function setupTestDB(collections?: CollectionDefinition[]): {
|
|
20
|
+
pool: mysql.Pool;
|
|
21
|
+
db: MySql2Database<typeof schema>;
|
|
22
|
+
dbManager: DBManagerImpl;
|
|
23
|
+
txManager: TXManagerImpl;
|
|
24
|
+
commandBuilders: {
|
|
25
|
+
collections: import("../modules/storage/storage-commands.js").CollectionCommands;
|
|
26
|
+
documents: import("../modules/storage/storage-commands.js").DocumentCommands;
|
|
27
|
+
};
|
|
28
|
+
queryBuilders: {
|
|
29
|
+
collections: import("../modules/storage/storage-queries.js").CollectionQueries;
|
|
30
|
+
documents: import("../modules/storage/storage-queries.js").DocumentQueries;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export declare function teardownTestDB(): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { drizzle } from 'drizzle-orm/mysql2';
|
|
9
|
+
import mysql from 'mysql2/promise';
|
|
10
|
+
import * as schema from '../database/schema/index.js';
|
|
11
|
+
import { createCommandBuilders } from '../modules/storage/storage-commands.js';
|
|
12
|
+
import { createQueryBuilders } from '../modules/storage/storage-queries.js';
|
|
13
|
+
import { DBManagerImpl, TXManagerImpl } from './db-manager.js';
|
|
14
|
+
import { assertTestDatabase } from './test-db.js';
|
|
15
|
+
let pool;
|
|
16
|
+
let db;
|
|
17
|
+
let dbManager;
|
|
18
|
+
let txManager;
|
|
19
|
+
let commandBuilders;
|
|
20
|
+
let queryBuilders;
|
|
21
|
+
/**
|
|
22
|
+
* Mirrors `packages/db-postgres/src/lib/test-helper.ts`. `queryBuilders` is
|
|
23
|
+
* wired as of Task 10A (the UNION ALL reconstruction path) and fully
|
|
24
|
+
* implements `IDocumentQueries` as of Task 10B (the predicate compiler,
|
|
25
|
+
* `findDocuments`' full surface, tree reads, and `getDocumentSystemFieldsForUpdate`).
|
|
26
|
+
*/
|
|
27
|
+
export function setupTestDB(collections = []) {
|
|
28
|
+
if (!pool) {
|
|
29
|
+
assertTestDatabase(process.env.BYLINE_DB_MYSQL_CONNECTION_STRING);
|
|
30
|
+
pool = mysql.createPool({
|
|
31
|
+
uri: process.env.BYLINE_DB_MYSQL_CONNECTION_STRING,
|
|
32
|
+
// Mirrors the pg test helper: tests are serial and run one query at a
|
|
33
|
+
// time, so a small pool per test file is sufficient.
|
|
34
|
+
connectionLimit: 4,
|
|
35
|
+
timezone: 'Z',
|
|
36
|
+
decimalNumbers: false,
|
|
37
|
+
// Match the schema's collation — see the identical option on the
|
|
38
|
+
// production pool in `src/index.ts` for why this is required (not
|
|
39
|
+
// just a nicety) once the storage-queries UNION ALL is in play.
|
|
40
|
+
charset: 'UTF8MB4_0900_AI_CI',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (!db) {
|
|
44
|
+
db = drizzle(pool, { schema, mode: 'default' });
|
|
45
|
+
}
|
|
46
|
+
if (!dbManager) {
|
|
47
|
+
dbManager = new DBManagerImpl({ dbPool: db });
|
|
48
|
+
txManager = new TXManagerImpl({ db: dbManager });
|
|
49
|
+
}
|
|
50
|
+
if (!commandBuilders) {
|
|
51
|
+
commandBuilders = createCommandBuilders(dbManager, 'en');
|
|
52
|
+
}
|
|
53
|
+
// Recreate queryBuilders when collections are provided so that
|
|
54
|
+
// DocumentQueries can resolve collection definitions by path.
|
|
55
|
+
queryBuilders = createQueryBuilders(db, collections, 'en', dbManager);
|
|
56
|
+
return { pool, db, dbManager, txManager, commandBuilders, queryBuilders };
|
|
57
|
+
}
|
|
58
|
+
export async function teardownTestDB() {
|
|
59
|
+
if (pool) {
|
|
60
|
+
await pool.end();
|
|
61
|
+
pool = undefined;
|
|
62
|
+
db = undefined;
|
|
63
|
+
dbManager = undefined;
|
|
64
|
+
txManager = undefined;
|
|
65
|
+
commandBuilders = undefined;
|
|
66
|
+
queryBuilders = undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|