@byline/db-postgres 3.8.0 → 3.9.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/dist/index.js +10 -1
- package/dist/lib/db-manager.d.ts +61 -0
- package/dist/lib/db-manager.js +43 -0
- package/dist/lib/test-helper.d.ts +3 -0
- package/dist/lib/test-helper.js +11 -2
- package/dist/modules/storage/storage-commands.d.ts +20 -9
- package/dist/modules/storage/storage-commands.js +27 -9
- package/dist/modules/storage/tests/storage-transactions.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-transactions.test.js +108 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
9
9
|
import pg from 'pg';
|
|
10
10
|
import * as schema from './database/schema/index.js';
|
|
11
|
+
import { DBManagerImpl, TXManagerImpl } from './lib/db-manager.js';
|
|
11
12
|
import { createCounterCommands } from './modules/counters/counters-commands.js';
|
|
12
13
|
import { createCommandBuilders, } from './modules/storage/storage-commands.js';
|
|
13
14
|
import { createQueryBuilders } from './modules/storage/storage-queries.js';
|
|
@@ -19,7 +20,14 @@ export const pgAdapter = ({ connectionString, collections, defaultContentLocale,
|
|
|
19
20
|
connectionTimeoutMillis,
|
|
20
21
|
});
|
|
21
22
|
const db = drizzle(pool, { schema });
|
|
22
|
-
|
|
23
|
+
// Request-scoped transaction propagation (docs/TRANSACTIONS.md). The command
|
|
24
|
+
// builders run on the DBManager — each `this.db` access resolves to the
|
|
25
|
+
// ambient transaction when a `withTransaction` boundary is open, else the
|
|
26
|
+
// pool. Queries and counters stay on the raw `db` for now (reads don't need
|
|
27
|
+
// to join the audit transaction); they migrate opportunistically.
|
|
28
|
+
const dbManager = new DBManagerImpl({ dbPool: db });
|
|
29
|
+
const txManager = new TXManagerImpl({ db: dbManager });
|
|
30
|
+
const commandBuilders = createCommandBuilders(dbManager, defaultContentLocale);
|
|
23
31
|
const queryBuilders = createQueryBuilders(db, collections, defaultContentLocale);
|
|
24
32
|
const counterCommands = createCounterCommands(db);
|
|
25
33
|
return {
|
|
@@ -28,6 +36,7 @@ export const pgAdapter = ({ connectionString, collections, defaultContentLocale,
|
|
|
28
36
|
counters: counterCommands,
|
|
29
37
|
},
|
|
30
38
|
queries: queryBuilders,
|
|
39
|
+
withTransaction: (fn) => txManager.withTransaction(fn),
|
|
31
40
|
drizzle: db,
|
|
32
41
|
pool,
|
|
33
42
|
backfillVersionLocales: () => commandBuilders.documents.backfillVersionLocales(),
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
* Ported from the Modulus project (`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/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 { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
20
|
+
import type * as schema from '../database/schema/index.js';
|
|
21
|
+
/**
|
|
22
|
+
* The executor every storage command runs on: either the connection pool
|
|
23
|
+
* (autonomous, statement-at-a-time) or — when a `withTransaction` boundary is
|
|
24
|
+
* open in the current async context — that transaction. Commands obtain it via
|
|
25
|
+
* `DBManager.get()` and never thread a transaction handle through their
|
|
26
|
+
* signatures.
|
|
27
|
+
*/
|
|
28
|
+
export type DBExecutor = NodePgDatabase<typeof schema>;
|
|
29
|
+
export interface DBManager {
|
|
30
|
+
/**
|
|
31
|
+
* The current executor: the ambient transaction when a `withTransaction`
|
|
32
|
+
* boundary is open in this async context, otherwise the pool.
|
|
33
|
+
*/
|
|
34
|
+
get(): DBExecutor;
|
|
35
|
+
}
|
|
36
|
+
export declare class DBManagerImpl implements DBManager {
|
|
37
|
+
private readonly dbPool;
|
|
38
|
+
constructor(deps: {
|
|
39
|
+
dbPool: DBExecutor;
|
|
40
|
+
});
|
|
41
|
+
get(): DBExecutor;
|
|
42
|
+
}
|
|
43
|
+
export interface TXManager {
|
|
44
|
+
/**
|
|
45
|
+
* Run `fn` inside a single database transaction. Every `DBManager.get()`
|
|
46
|
+
* call made during `fn` (transitively, across `await`s) returns that
|
|
47
|
+
* transaction, so the commands `fn` invokes commit or roll back together.
|
|
48
|
+
*
|
|
49
|
+
* Nesting: when already inside a `withTransaction`, the inner call opens a
|
|
50
|
+
* SAVEPOINT (Drizzle nested transaction) — an inner throw rolls back to the
|
|
51
|
+
* savepoint, an outer throw rolls back everything.
|
|
52
|
+
*/
|
|
53
|
+
withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
54
|
+
}
|
|
55
|
+
export declare class TXManagerImpl implements TXManager {
|
|
56
|
+
private readonly db;
|
|
57
|
+
constructor(deps: {
|
|
58
|
+
db: DBManager;
|
|
59
|
+
});
|
|
60
|
+
withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
* Ported from the Modulus project (`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/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 PgTransaction; it carries the full query-builder
|
|
38
|
+
// surface every command uses. The cast bridges the one structural gap
|
|
39
|
+
// to NodePgDatabase — the transaction lacks `$client`, which no command
|
|
40
|
+
// touches. See docs/TRANSACTIONS.md.
|
|
41
|
+
transactionALS.run(tx, fn));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { CollectionDefinition } from '@byline/core';
|
|
2
2
|
import { type NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
3
3
|
import * as schema from '../database/schema/index.js';
|
|
4
|
+
import { DBManagerImpl, TXManagerImpl } from './db-manager.js';
|
|
4
5
|
export declare function setupTestDB(collections?: CollectionDefinition[]): {
|
|
5
6
|
pool: import("pg").Pool;
|
|
6
7
|
db: NodePgDatabase<typeof schema>;
|
|
8
|
+
dbManager: DBManagerImpl;
|
|
9
|
+
txManager: TXManagerImpl;
|
|
7
10
|
commandBuilders: {
|
|
8
11
|
collections: import("../modules/storage/storage-commands.js").CollectionCommands;
|
|
9
12
|
documents: import("../modules/storage/storage-commands.js").DocumentCommands;
|
package/dist/lib/test-helper.js
CHANGED
|
@@ -3,9 +3,12 @@ import pg from 'pg';
|
|
|
3
3
|
import * as schema from '../database/schema/index.js';
|
|
4
4
|
import { createCommandBuilders } from '../modules/storage/storage-commands.js';
|
|
5
5
|
import { createQueryBuilders } from '../modules/storage/storage-queries.js';
|
|
6
|
+
import { DBManagerImpl, TXManagerImpl } from './db-manager.js';
|
|
6
7
|
import { assertTestDatabase } from './test-db.js';
|
|
7
8
|
let pool;
|
|
8
9
|
let db;
|
|
10
|
+
let dbManager;
|
|
11
|
+
let txManager;
|
|
9
12
|
let commandBuilders;
|
|
10
13
|
let queryBuilders;
|
|
11
14
|
export function setupTestDB(collections = []) {
|
|
@@ -26,19 +29,25 @@ export function setupTestDB(collections = []) {
|
|
|
26
29
|
if (!db) {
|
|
27
30
|
db = drizzle(pool, { schema });
|
|
28
31
|
}
|
|
32
|
+
if (!dbManager) {
|
|
33
|
+
dbManager = new DBManagerImpl({ dbPool: db });
|
|
34
|
+
txManager = new TXManagerImpl({ db: dbManager });
|
|
35
|
+
}
|
|
29
36
|
if (!commandBuilders) {
|
|
30
|
-
commandBuilders = createCommandBuilders(
|
|
37
|
+
commandBuilders = createCommandBuilders(dbManager, 'en');
|
|
31
38
|
}
|
|
32
39
|
// Recreate queryBuilders when collections are provided so that
|
|
33
40
|
// DocumentQueries can resolve collection definitions by path.
|
|
34
41
|
queryBuilders = createQueryBuilders(db, collections, 'en');
|
|
35
|
-
return { pool, db, commandBuilders, queryBuilders };
|
|
42
|
+
return { pool, db, dbManager, txManager, commandBuilders, queryBuilders };
|
|
36
43
|
}
|
|
37
44
|
export async function teardownTestDB() {
|
|
38
45
|
if (pool) {
|
|
39
46
|
await pool.end();
|
|
40
47
|
pool = undefined;
|
|
41
48
|
db = undefined;
|
|
49
|
+
dbManager = undefined;
|
|
50
|
+
txManager = undefined;
|
|
42
51
|
commandBuilders = undefined;
|
|
43
52
|
queryBuilders = undefined;
|
|
44
53
|
}
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { CollectionDefinition, ICollectionCommands, IDocumentCommands } from '@byline/core';
|
|
9
|
-
import type {
|
|
10
|
-
import type * as schema from '../../database/schema/index.js';
|
|
11
|
-
type DatabaseConnection = NodePgDatabase<typeof schema>;
|
|
9
|
+
import type { DBManager } from '../../lib/db-manager.js';
|
|
12
10
|
/** Outcome of re-anchoring a single document's content source locale. */
|
|
13
11
|
export type ReAnchorStatus = 'reanchored' | 'skipped-incomplete' | 'already-anchored' | 'not-found';
|
|
14
12
|
export interface ReAnchorResult {
|
|
@@ -36,8 +34,15 @@ export interface ReAnchorReport {
|
|
|
36
34
|
* CollectionCommands
|
|
37
35
|
*/
|
|
38
36
|
export declare class CollectionCommands implements ICollectionCommands {
|
|
39
|
-
private
|
|
40
|
-
constructor(
|
|
37
|
+
private dbManager;
|
|
38
|
+
constructor(dbManager: DBManager);
|
|
39
|
+
/**
|
|
40
|
+
* The executor for this call — the ambient transaction when a
|
|
41
|
+
* `withTransaction` boundary is open, otherwise the pool. Resolved per
|
|
42
|
+
* access so every `this.db.*` below transparently joins an enclosing
|
|
43
|
+
* transaction with no call-site change. See docs/TRANSACTIONS.md.
|
|
44
|
+
*/
|
|
45
|
+
private get db();
|
|
41
46
|
create(path: string, config: CollectionDefinition, opts?: {
|
|
42
47
|
version?: number;
|
|
43
48
|
schemaHash?: string;
|
|
@@ -73,9 +78,16 @@ export declare class CollectionCommands implements ICollectionCommands {
|
|
|
73
78
|
* DocumentCommands
|
|
74
79
|
*/
|
|
75
80
|
export declare class DocumentCommands implements IDocumentCommands {
|
|
76
|
-
private
|
|
81
|
+
private dbManager;
|
|
77
82
|
private defaultContentLocale;
|
|
78
|
-
constructor(
|
|
83
|
+
constructor(dbManager: DBManager, defaultContentLocale: string);
|
|
84
|
+
/**
|
|
85
|
+
* The executor for this call — the ambient transaction when a
|
|
86
|
+
* `withTransaction` boundary is open, otherwise the pool. Resolved per
|
|
87
|
+
* access so every `this.db.*` below transparently joins an enclosing
|
|
88
|
+
* transaction with no call-site change. See docs/TRANSACTIONS.md.
|
|
89
|
+
*/
|
|
90
|
+
private get db();
|
|
79
91
|
/**
|
|
80
92
|
* createDocumentVersion
|
|
81
93
|
*
|
|
@@ -379,8 +391,7 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
379
391
|
order_key: string;
|
|
380
392
|
}): Promise<void>;
|
|
381
393
|
}
|
|
382
|
-
export declare function createCommandBuilders(
|
|
394
|
+
export declare function createCommandBuilders(dbManager: DBManager, defaultContentLocale: string): {
|
|
383
395
|
collections: CollectionCommands;
|
|
384
396
|
documents: DocumentCommands;
|
|
385
397
|
};
|
|
386
|
-
export {};
|
|
@@ -15,9 +15,18 @@ import { getFirstOrThrow } from './storage-utils.js';
|
|
|
15
15
|
* CollectionCommands
|
|
16
16
|
*/
|
|
17
17
|
export class CollectionCommands {
|
|
18
|
-
|
|
19
|
-
constructor(
|
|
20
|
-
this.
|
|
18
|
+
dbManager;
|
|
19
|
+
constructor(dbManager) {
|
|
20
|
+
this.dbManager = dbManager;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The executor for this call — the ambient transaction when a
|
|
24
|
+
* `withTransaction` boundary is open, otherwise the pool. Resolved per
|
|
25
|
+
* access so every `this.db.*` below transparently joins an enclosing
|
|
26
|
+
* transaction with no call-site change. See docs/TRANSACTIONS.md.
|
|
27
|
+
*/
|
|
28
|
+
get db() {
|
|
29
|
+
return this.dbManager.get();
|
|
21
30
|
}
|
|
22
31
|
async create(path, config, opts) {
|
|
23
32
|
return await this.db
|
|
@@ -51,12 +60,21 @@ export class CollectionCommands {
|
|
|
51
60
|
* DocumentCommands
|
|
52
61
|
*/
|
|
53
62
|
export class DocumentCommands {
|
|
54
|
-
|
|
63
|
+
dbManager;
|
|
55
64
|
defaultContentLocale;
|
|
56
|
-
constructor(
|
|
57
|
-
this.
|
|
65
|
+
constructor(dbManager, defaultContentLocale) {
|
|
66
|
+
this.dbManager = dbManager;
|
|
58
67
|
this.defaultContentLocale = defaultContentLocale;
|
|
59
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The executor for this call — the ambient transaction when a
|
|
71
|
+
* `withTransaction` boundary is open, otherwise the pool. Resolved per
|
|
72
|
+
* access so every `this.db.*` below transparently joins an enclosing
|
|
73
|
+
* transaction with no call-site change. See docs/TRANSACTIONS.md.
|
|
74
|
+
*/
|
|
75
|
+
get db() {
|
|
76
|
+
return this.dbManager.get();
|
|
77
|
+
}
|
|
60
78
|
/**
|
|
61
79
|
* createDocumentVersion
|
|
62
80
|
*
|
|
@@ -836,9 +854,9 @@ export class DocumentCommands {
|
|
|
836
854
|
.where(eq(documents.id, params.document_id));
|
|
837
855
|
}
|
|
838
856
|
}
|
|
839
|
-
export function createCommandBuilders(
|
|
857
|
+
export function createCommandBuilders(dbManager, defaultContentLocale) {
|
|
840
858
|
return {
|
|
841
|
-
collections: new CollectionCommands(
|
|
842
|
-
documents: new DocumentCommands(
|
|
859
|
+
collections: new CollectionCommands(dbManager),
|
|
860
|
+
documents: new DocumentCommands(dbManager, defaultContentLocale),
|
|
843
861
|
};
|
|
844
862
|
}
|
|
@@ -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,108 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Integration tests for request-scoped transaction propagation
|
|
10
|
+
* (`withTransaction` / DBManager / TXManager — see docs/TRANSACTIONS.md).
|
|
11
|
+
*
|
|
12
|
+
* Proves the load-bearing guarantee the audit log depends on: multiple
|
|
13
|
+
* `commands.*` calls wrapped in one `withTransaction` commit or roll back
|
|
14
|
+
* **together**, with no transaction handle threaded through their signatures.
|
|
15
|
+
* The commands join the ambient transaction purely via the AsyncLocalStorage
|
|
16
|
+
* propagation in DBManager.get().
|
|
17
|
+
*/
|
|
18
|
+
import { eq } from 'drizzle-orm';
|
|
19
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
20
|
+
import { collections } from '../../../database/schema/index.js';
|
|
21
|
+
import { setupTestDB, teardownTestDB } from '../../../lib/test-helper.js';
|
|
22
|
+
let db;
|
|
23
|
+
let dbManager;
|
|
24
|
+
let txManager;
|
|
25
|
+
let commandBuilders;
|
|
26
|
+
const ts = Date.now();
|
|
27
|
+
const createdPaths = [];
|
|
28
|
+
async function collectionExists(path) {
|
|
29
|
+
const rows = await db
|
|
30
|
+
.select({ id: collections.id })
|
|
31
|
+
.from(collections)
|
|
32
|
+
.where(eq(collections.path, path));
|
|
33
|
+
return rows.length > 0;
|
|
34
|
+
}
|
|
35
|
+
describe('withTransaction propagation + atomicity', () => {
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
const testDB = setupTestDB([]);
|
|
38
|
+
db = testDB.db;
|
|
39
|
+
dbManager = testDB.dbManager;
|
|
40
|
+
txManager = testDB.txManager;
|
|
41
|
+
commandBuilders = testDB.commandBuilders;
|
|
42
|
+
});
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
for (const path of createdPaths) {
|
|
45
|
+
try {
|
|
46
|
+
const rows = await db
|
|
47
|
+
.select({ id: collections.id })
|
|
48
|
+
.from(collections)
|
|
49
|
+
.where(eq(collections.path, path));
|
|
50
|
+
if (rows[0])
|
|
51
|
+
await commandBuilders.collections.delete(rows[0].id);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error('cleanup failed for', path, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await teardownTestDB();
|
|
58
|
+
});
|
|
59
|
+
it('get() returns the pool outside a transaction and the tx inside it', async () => {
|
|
60
|
+
// Outside any boundary, the executor is the pool itself.
|
|
61
|
+
expect(dbManager.get()).toBe(db);
|
|
62
|
+
let insideExecutor;
|
|
63
|
+
await txManager.withTransaction(async () => {
|
|
64
|
+
insideExecutor = dbManager.get();
|
|
65
|
+
});
|
|
66
|
+
// Inside the boundary it is the ambient transaction — a different handle.
|
|
67
|
+
expect(insideExecutor).not.toBe(db);
|
|
68
|
+
});
|
|
69
|
+
it('commits every command in the boundary together', async () => {
|
|
70
|
+
const a = `tx-commit-a-${ts}`;
|
|
71
|
+
const b = `tx-commit-b-${ts}`;
|
|
72
|
+
createdPaths.push(a, b);
|
|
73
|
+
await txManager.withTransaction(async () => {
|
|
74
|
+
await commandBuilders.collections.create(a, {
|
|
75
|
+
path: a,
|
|
76
|
+
labels: { singular: 'A', plural: 'As' },
|
|
77
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
78
|
+
});
|
|
79
|
+
await commandBuilders.collections.create(b, {
|
|
80
|
+
path: b,
|
|
81
|
+
labels: { singular: 'B', plural: 'Bs' },
|
|
82
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
expect(await collectionExists(a)).toBe(true);
|
|
86
|
+
expect(await collectionExists(b)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('rolls back every command in the boundary when it throws', async () => {
|
|
89
|
+
const a = `tx-rollback-a-${ts}`;
|
|
90
|
+
const b = `tx-rollback-b-${ts}`;
|
|
91
|
+
const boom = new Error('boom');
|
|
92
|
+
await expect(txManager.withTransaction(async () => {
|
|
93
|
+
// First write succeeds...
|
|
94
|
+
await commandBuilders.collections.create(a, {
|
|
95
|
+
path: a,
|
|
96
|
+
labels: { singular: 'A', plural: 'As' },
|
|
97
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
98
|
+
});
|
|
99
|
+
// ...then the unit of work fails after it. If the first write ran on
|
|
100
|
+
// the pool rather than the ambient tx, it would survive this throw.
|
|
101
|
+
throw boom;
|
|
102
|
+
})).rejects.toThrow('boom');
|
|
103
|
+
// Atomicity: the successful-looking first write was rolled back with the
|
|
104
|
+
// failing transaction. Neither collection exists.
|
|
105
|
+
expect(await collectionExists(a)).toBe(false);
|
|
106
|
+
expect(await collectionExists(b)).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/db-postgres",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.9.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"pg": "^8.21.0",
|
|
58
58
|
"uuid": "^14.0.0",
|
|
59
59
|
"zod": "^4.4.3",
|
|
60
|
-
"@byline/
|
|
61
|
-
"@byline/
|
|
62
|
-
"@byline/core": "3.
|
|
60
|
+
"@byline/auth": "3.9.0",
|
|
61
|
+
"@byline/admin": "3.9.0",
|
|
62
|
+
"@byline/core": "3.9.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@biomejs/biome": "2.4.15",
|