@byline/db-postgres 3.7.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/admin/admin-users-repository.js +6 -1
- package/dist/modules/storage/storage-commands.d.ts +22 -9
- package/dist/modules/storage/storage-commands.js +32 -11
- package/dist/modules/storage/storage-queries.d.ts +8 -0
- package/dist/modules/storage/storage-queries.js +10 -0
- 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,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { ERR_ADMIN_USER_VERSION_CONFLICT, } from '@byline/admin/admin-users';
|
|
9
|
-
import { and, asc, desc, eq, ilike, or, sql } from 'drizzle-orm';
|
|
9
|
+
import { and, asc, desc, eq, ilike, inArray, or, sql } from 'drizzle-orm';
|
|
10
10
|
import { v7 as uuidv7 } from 'uuid';
|
|
11
11
|
import { adminUsers } from '../../database/schema/auth.js';
|
|
12
12
|
/**
|
|
@@ -78,6 +78,11 @@ export function createAdminUsersRepository(db) {
|
|
|
78
78
|
const [row] = await db.select(PUBLIC_COLUMNS).from(adminUsers).where(eq(adminUsers.id, id));
|
|
79
79
|
return row ?? null;
|
|
80
80
|
},
|
|
81
|
+
async getByIds(ids) {
|
|
82
|
+
if (ids.length === 0)
|
|
83
|
+
return [];
|
|
84
|
+
return db.select(PUBLIC_COLUMNS).from(adminUsers).where(inArray(adminUsers.id, ids));
|
|
85
|
+
},
|
|
81
86
|
async getByEmail(email) {
|
|
82
87
|
const [row] = await db
|
|
83
88
|
.select(PUBLIC_COLUMNS)
|
|
@@ -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
|
*
|
|
@@ -245,6 +257,7 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
245
257
|
documentId: string;
|
|
246
258
|
locale: string;
|
|
247
259
|
status?: string;
|
|
260
|
+
createdBy?: string;
|
|
248
261
|
}): Promise<{
|
|
249
262
|
newVersionId: string;
|
|
250
263
|
previousVersionId: string;
|
|
@@ -268,6 +281,7 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
268
281
|
documentId: string;
|
|
269
282
|
targetLocale: string;
|
|
270
283
|
dryRun?: boolean;
|
|
284
|
+
createdBy?: string;
|
|
271
285
|
}): Promise<ReAnchorResult>;
|
|
272
286
|
/**
|
|
273
287
|
* reAnchorDocuments
|
|
@@ -377,8 +391,7 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
377
391
|
order_key: string;
|
|
378
392
|
}): Promise<void>;
|
|
379
393
|
}
|
|
380
|
-
export declare function createCommandBuilders(
|
|
394
|
+
export declare function createCommandBuilders(dbManager: DBManager, defaultContentLocale: string): {
|
|
381
395
|
collections: CollectionCommands;
|
|
382
396
|
documents: DocumentCommands;
|
|
383
397
|
};
|
|
384
|
-
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
|
*
|
|
@@ -111,6 +129,7 @@ export class DocumentCommands {
|
|
|
111
129
|
collection_version: params.collectionVersion,
|
|
112
130
|
event_type: params.action ?? 'create',
|
|
113
131
|
status: params.status ?? 'draft',
|
|
132
|
+
created_by: params.createdBy ?? null,
|
|
114
133
|
})
|
|
115
134
|
.returning()
|
|
116
135
|
.then(getFirstOrThrow('Failed to create document version'));
|
|
@@ -498,7 +517,7 @@ export class DocumentCommands {
|
|
|
498
517
|
* existence first, so this is a guard).
|
|
499
518
|
*/
|
|
500
519
|
async deleteDocumentLocale(params) {
|
|
501
|
-
const { documentId, locale, status } = params;
|
|
520
|
+
const { documentId, locale, status, createdBy } = params;
|
|
502
521
|
return this.db.transaction(async (tx) => {
|
|
503
522
|
// 1. Current (latest, non-deleted) version + the document's anchor.
|
|
504
523
|
const current = await tx
|
|
@@ -529,6 +548,7 @@ export class DocumentCommands {
|
|
|
529
548
|
event_type: 'delete_locale',
|
|
530
549
|
status: status ?? 'draft',
|
|
531
550
|
change_summary: `deleted content locale ${locale}`,
|
|
551
|
+
created_by: createdBy ?? null,
|
|
532
552
|
});
|
|
533
553
|
await this.copyAllVersionStoreRows(tx, current.versionId, newVersionId, locale);
|
|
534
554
|
// 3. Recompute the new version's availability ledger against the source
|
|
@@ -553,7 +573,7 @@ export class DocumentCommands {
|
|
|
553
573
|
* docs/I18N.md.
|
|
554
574
|
*/
|
|
555
575
|
async reAnchorDocument(params) {
|
|
556
|
-
const { documentId, targetLocale, dryRun = false } = params;
|
|
576
|
+
const { documentId, targetLocale, dryRun = false, createdBy } = params;
|
|
557
577
|
return this.db.transaction(async (tx) => {
|
|
558
578
|
// 1. Current (latest, non-deleted) version + the document's anchor.
|
|
559
579
|
const current = await tx
|
|
@@ -613,6 +633,7 @@ export class DocumentCommands {
|
|
|
613
633
|
event_type: 'update',
|
|
614
634
|
status: current.status ?? 'draft',
|
|
615
635
|
change_summary: `re-anchored content source locale ${fromLocale} → ${targetLocale}`,
|
|
636
|
+
created_by: createdBy ?? null,
|
|
616
637
|
});
|
|
617
638
|
await this.copyAllVersionStoreRows(tx, current.versionId, newVersionId);
|
|
618
639
|
// 6. Ledger for the new version, computed against the new source locale.
|
|
@@ -833,9 +854,9 @@ export class DocumentCommands {
|
|
|
833
854
|
.where(eq(documents.id, params.document_id));
|
|
834
855
|
}
|
|
835
856
|
}
|
|
836
|
-
export function createCommandBuilders(
|
|
857
|
+
export function createCommandBuilders(dbManager, defaultContentLocale) {
|
|
837
858
|
return {
|
|
838
|
-
collections: new CollectionCommands(
|
|
839
|
-
documents: new DocumentCommands(
|
|
859
|
+
collections: new CollectionCommands(dbManager),
|
|
860
|
+
documents: new DocumentCommands(dbManager, defaultContentLocale),
|
|
840
861
|
};
|
|
841
862
|
}
|
|
@@ -256,8 +256,10 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
256
256
|
path: string;
|
|
257
257
|
source_locale: string;
|
|
258
258
|
status: string | null;
|
|
259
|
+
event_type: string;
|
|
259
260
|
created_at: Date;
|
|
260
261
|
updated_at: Date;
|
|
262
|
+
created_by: string | null;
|
|
261
263
|
fields: any;
|
|
262
264
|
availableLocales: string[];
|
|
263
265
|
_availableVersionLocales: string[];
|
|
@@ -268,8 +270,10 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
268
270
|
path: string;
|
|
269
271
|
source_locale: string;
|
|
270
272
|
status: string | null;
|
|
273
|
+
event_type: string;
|
|
271
274
|
created_at: Date;
|
|
272
275
|
updated_at: Date;
|
|
276
|
+
created_by: string | null;
|
|
273
277
|
fields: FlattenedStore[];
|
|
274
278
|
} | null>;
|
|
275
279
|
getDocumentByPath({ collection_id, path, locale, reconstruct, readMode, filters, onMissingLocale, }: {
|
|
@@ -286,8 +290,10 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
286
290
|
path: string;
|
|
287
291
|
source_locale: string;
|
|
288
292
|
status: string | null;
|
|
293
|
+
event_type: string;
|
|
289
294
|
created_at: Date;
|
|
290
295
|
updated_at: Date;
|
|
296
|
+
created_by: string | null;
|
|
291
297
|
fields: any;
|
|
292
298
|
availableLocales: string[];
|
|
293
299
|
_availableVersionLocales: string[];
|
|
@@ -298,8 +304,10 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
298
304
|
path: string;
|
|
299
305
|
source_locale: string;
|
|
300
306
|
status: string | null;
|
|
307
|
+
event_type: string;
|
|
301
308
|
created_at: Date;
|
|
302
309
|
updated_at: Date;
|
|
310
|
+
created_by: string | null;
|
|
303
311
|
fields: FlattenedStore[];
|
|
304
312
|
availableLocales?: undefined;
|
|
305
313
|
_availableVersionLocales?: undefined;
|
|
@@ -531,8 +531,10 @@ export class DocumentQueries {
|
|
|
531
531
|
path: document.path ?? '',
|
|
532
532
|
source_locale: document.source_locale ?? null,
|
|
533
533
|
status: document.status,
|
|
534
|
+
event_type: document.event_type,
|
|
534
535
|
created_at: document.created_at,
|
|
535
536
|
updated_at: document.updated_at,
|
|
537
|
+
created_by: document.created_by ?? null,
|
|
536
538
|
fields,
|
|
537
539
|
availableLocales: advertised ?? [],
|
|
538
540
|
_availableVersionLocales: availability?.availableLocales ?? [],
|
|
@@ -548,8 +550,10 @@ export class DocumentQueries {
|
|
|
548
550
|
path: document.path ?? '',
|
|
549
551
|
source_locale: document.source_locale ?? null,
|
|
550
552
|
status: document.status,
|
|
553
|
+
event_type: document.event_type,
|
|
551
554
|
created_at: document.created_at,
|
|
552
555
|
updated_at: document.updated_at,
|
|
556
|
+
created_by: document.created_by ?? null,
|
|
553
557
|
fields: fieldValues,
|
|
554
558
|
};
|
|
555
559
|
}
|
|
@@ -612,8 +616,10 @@ export class DocumentQueries {
|
|
|
612
616
|
path: document.path ?? '',
|
|
613
617
|
source_locale: document.source_locale ?? null,
|
|
614
618
|
status: document.status,
|
|
619
|
+
event_type: document.event_type,
|
|
615
620
|
created_at: document.created_at,
|
|
616
621
|
updated_at: document.updated_at,
|
|
622
|
+
created_by: document.created_by ?? null,
|
|
617
623
|
fields,
|
|
618
624
|
availableLocales: advertised ?? [],
|
|
619
625
|
_availableVersionLocales: availability?.availableLocales ?? [],
|
|
@@ -628,8 +634,10 @@ export class DocumentQueries {
|
|
|
628
634
|
path: document.path ?? '',
|
|
629
635
|
source_locale: document.source_locale ?? null,
|
|
630
636
|
status: document.status,
|
|
637
|
+
event_type: document.event_type,
|
|
631
638
|
created_at: document.created_at,
|
|
632
639
|
updated_at: document.updated_at,
|
|
640
|
+
created_by: document.created_by ?? null,
|
|
633
641
|
fields: fieldValues,
|
|
634
642
|
};
|
|
635
643
|
}
|
|
@@ -999,8 +1007,10 @@ export class DocumentQueries {
|
|
|
999
1007
|
path: doc.path ?? '',
|
|
1000
1008
|
source_locale: doc.source_locale ?? null,
|
|
1001
1009
|
status: doc.status,
|
|
1010
|
+
event_type: doc.event_type,
|
|
1002
1011
|
created_at: doc.created_at,
|
|
1003
1012
|
updated_at: doc.updated_at,
|
|
1013
|
+
created_by: doc.created_by ?? null,
|
|
1004
1014
|
fields: trimmedFields,
|
|
1005
1015
|
};
|
|
1006
1016
|
result.push(documentWithFields);
|
|
@@ -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",
|