@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.
Files changed (75) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +283 -0
  3. package/dist/database/schema/auth.d.ts +951 -0
  4. package/dist/database/schema/auth.js +226 -0
  5. package/dist/database/schema/common.d.ts +168 -0
  6. package/dist/database/schema/common.js +130 -0
  7. package/dist/database/schema/index.d.ts +3419 -0
  8. package/dist/database/schema/index.js +895 -0
  9. package/dist/database/schema/schema-pins.test.node.d.ts +52 -0
  10. package/dist/database/schema/schema-pins.test.node.js +398 -0
  11. package/dist/index.d.ts +79 -0
  12. package/dist/index.js +151 -0
  13. package/dist/lib/boot-check.d.ts +22 -0
  14. package/dist/lib/boot-check.js +42 -0
  15. package/dist/lib/boot-check.test.node.d.ts +8 -0
  16. package/dist/lib/boot-check.test.node.js +46 -0
  17. package/dist/lib/db-manager.d.ts +54 -0
  18. package/dist/lib/db-manager.js +49 -0
  19. package/dist/lib/test-db.d.ts +37 -0
  20. package/dist/lib/test-db.js +111 -0
  21. package/dist/lib/test-helper.d.ts +33 -0
  22. package/dist/lib/test-helper.js +68 -0
  23. package/dist/modules/admin/admin-permissions-repository.d.ts +35 -0
  24. package/dist/modules/admin/admin-permissions-repository.js +101 -0
  25. package/dist/modules/admin/admin-preferences-repository.d.ts +62 -0
  26. package/dist/modules/admin/admin-preferences-repository.js +156 -0
  27. package/dist/modules/admin/admin-roles-repository.d.ts +11 -0
  28. package/dist/modules/admin/admin-roles-repository.js +209 -0
  29. package/dist/modules/admin/admin-store.d.ts +20 -0
  30. package/dist/modules/admin/admin-store.js +30 -0
  31. package/dist/modules/admin/admin-users-repository.d.ts +11 -0
  32. package/dist/modules/admin/admin-users-repository.js +303 -0
  33. package/dist/modules/admin/index.d.ts +27 -0
  34. package/dist/modules/admin/index.js +27 -0
  35. package/dist/modules/admin/refresh-tokens-repository.d.ts +34 -0
  36. package/dist/modules/admin/refresh-tokens-repository.js +160 -0
  37. package/dist/modules/audit/audit-commands.d.ts +29 -0
  38. package/dist/modules/audit/audit-commands.js +40 -0
  39. package/dist/modules/audit/audit-queries.d.ts +41 -0
  40. package/dist/modules/audit/audit-queries.js +169 -0
  41. package/dist/modules/counters/counters-commands.d.ts +54 -0
  42. package/dist/modules/counters/counters-commands.js +121 -0
  43. package/dist/modules/counters/tests/counters-concurrency.test.d.ts +8 -0
  44. package/dist/modules/counters/tests/counters-concurrency.test.js +111 -0
  45. package/dist/modules/storage/classify-error.d.ts +34 -0
  46. package/dist/modules/storage/classify-error.js +50 -0
  47. package/dist/modules/storage/classify-error.test.node.d.ts +8 -0
  48. package/dist/modules/storage/classify-error.test.node.js +89 -0
  49. package/dist/modules/storage/normalize-row.d.ts +55 -0
  50. package/dist/modules/storage/normalize-row.js +135 -0
  51. package/dist/modules/storage/normalize-row.test.node.d.ts +8 -0
  52. package/dist/modules/storage/normalize-row.test.node.js +89 -0
  53. package/dist/modules/storage/storage-commands.d.ts +443 -0
  54. package/dist/modules/storage/storage-commands.js +1263 -0
  55. package/dist/modules/storage/storage-insert.d.ts +21 -0
  56. package/dist/modules/storage/storage-insert.js +152 -0
  57. package/dist/modules/storage/storage-queries.d.ts +805 -0
  58. package/dist/modules/storage/storage-queries.js +1815 -0
  59. package/dist/modules/storage/storage-store-manifest.d.ts +77 -0
  60. package/dist/modules/storage/storage-store-manifest.js +168 -0
  61. package/dist/modules/storage/storage-utils.d.ts +49 -0
  62. package/dist/modules/storage/storage-utils.js +76 -0
  63. package/dist/modules/storage/tests/dialect-pins.integration.test.d.ts +8 -0
  64. package/dist/modules/storage/tests/dialect-pins.integration.test.js +266 -0
  65. package/dist/modules/storage/tests/storage-commands.test.d.ts +8 -0
  66. package/dist/modules/storage/tests/storage-commands.test.js +324 -0
  67. package/dist/modules/storage/tests/storage-document-paths.test.d.ts +8 -0
  68. package/dist/modules/storage/tests/storage-document-paths.test.js +214 -0
  69. package/dist/modules/storage/tests/storage-document-tree.test.d.ts +8 -0
  70. package/dist/modules/storage/tests/storage-document-tree.test.js +361 -0
  71. package/dist/modules/storage/tests/storage-queries.test.d.ts +8 -0
  72. package/dist/modules/storage/tests/storage-queries.test.js +685 -0
  73. package/dist/modules/storage/tests/storage-status-and-lifecycle.test.d.ts +8 -0
  74. package/dist/modules/storage/tests/storage-status-and-lifecycle.test.js +268 -0
  75. 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
+ }