@byline/db-postgres 3.9.0 → 3.10.1
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/database/schema/index.d.ts +183 -0
- package/dist/database/schema/index.js +33 -0
- package/dist/index.js +11 -1
- package/dist/modules/audit/audit-commands.d.ts +27 -0
- package/dist/modules/audit/audit-commands.js +40 -0
- package/dist/modules/audit/audit-queries.d.ts +29 -0
- package/dist/modules/audit/audit-queries.js +55 -0
- package/dist/modules/audit/tests/audit-log.test.d.ts +8 -0
- package/dist/modules/audit/tests/audit-log.test.js +117 -0
- package/package.json +4 -4
|
@@ -3312,4 +3312,187 @@ export declare const jsonStoreRelations: import("drizzle-orm").Relations<"byline
|
|
|
3312
3312
|
document: import("drizzle-orm").One<"byline_document_versions", true>;
|
|
3313
3313
|
collection: import("drizzle-orm").One<"byline_collections", true>;
|
|
3314
3314
|
}>;
|
|
3315
|
+
export declare const auditLog: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
3316
|
+
name: "byline_audit_log";
|
|
3317
|
+
schema: undefined;
|
|
3318
|
+
columns: {
|
|
3319
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
3320
|
+
name: "id";
|
|
3321
|
+
tableName: "byline_audit_log";
|
|
3322
|
+
dataType: "string";
|
|
3323
|
+
columnType: "PgUUID";
|
|
3324
|
+
data: string;
|
|
3325
|
+
driverParam: string;
|
|
3326
|
+
notNull: true;
|
|
3327
|
+
hasDefault: false;
|
|
3328
|
+
isPrimaryKey: true;
|
|
3329
|
+
isAutoincrement: false;
|
|
3330
|
+
hasRuntimeDefault: false;
|
|
3331
|
+
enumValues: undefined;
|
|
3332
|
+
baseColumn: never;
|
|
3333
|
+
identity: undefined;
|
|
3334
|
+
generated: undefined;
|
|
3335
|
+
}, {}, {}>;
|
|
3336
|
+
document_id: import("drizzle-orm/pg-core").PgColumn<{
|
|
3337
|
+
name: "document_id";
|
|
3338
|
+
tableName: "byline_audit_log";
|
|
3339
|
+
dataType: "string";
|
|
3340
|
+
columnType: "PgUUID";
|
|
3341
|
+
data: string;
|
|
3342
|
+
driverParam: string;
|
|
3343
|
+
notNull: false;
|
|
3344
|
+
hasDefault: false;
|
|
3345
|
+
isPrimaryKey: false;
|
|
3346
|
+
isAutoincrement: false;
|
|
3347
|
+
hasRuntimeDefault: false;
|
|
3348
|
+
enumValues: undefined;
|
|
3349
|
+
baseColumn: never;
|
|
3350
|
+
identity: undefined;
|
|
3351
|
+
generated: undefined;
|
|
3352
|
+
}, {}, {}>;
|
|
3353
|
+
collection_id: import("drizzle-orm/pg-core").PgColumn<{
|
|
3354
|
+
name: "collection_id";
|
|
3355
|
+
tableName: "byline_audit_log";
|
|
3356
|
+
dataType: "string";
|
|
3357
|
+
columnType: "PgUUID";
|
|
3358
|
+
data: string;
|
|
3359
|
+
driverParam: string;
|
|
3360
|
+
notNull: false;
|
|
3361
|
+
hasDefault: false;
|
|
3362
|
+
isPrimaryKey: false;
|
|
3363
|
+
isAutoincrement: false;
|
|
3364
|
+
hasRuntimeDefault: false;
|
|
3365
|
+
enumValues: undefined;
|
|
3366
|
+
baseColumn: never;
|
|
3367
|
+
identity: undefined;
|
|
3368
|
+
generated: undefined;
|
|
3369
|
+
}, {}, {}>;
|
|
3370
|
+
actor_id: import("drizzle-orm/pg-core").PgColumn<{
|
|
3371
|
+
name: "actor_id";
|
|
3372
|
+
tableName: "byline_audit_log";
|
|
3373
|
+
dataType: "string";
|
|
3374
|
+
columnType: "PgUUID";
|
|
3375
|
+
data: string;
|
|
3376
|
+
driverParam: string;
|
|
3377
|
+
notNull: false;
|
|
3378
|
+
hasDefault: false;
|
|
3379
|
+
isPrimaryKey: false;
|
|
3380
|
+
isAutoincrement: false;
|
|
3381
|
+
hasRuntimeDefault: false;
|
|
3382
|
+
enumValues: undefined;
|
|
3383
|
+
baseColumn: never;
|
|
3384
|
+
identity: undefined;
|
|
3385
|
+
generated: undefined;
|
|
3386
|
+
}, {}, {}>;
|
|
3387
|
+
actor_realm: import("drizzle-orm/pg-core").PgColumn<{
|
|
3388
|
+
name: "actor_realm";
|
|
3389
|
+
tableName: "byline_audit_log";
|
|
3390
|
+
dataType: "string";
|
|
3391
|
+
columnType: "PgVarchar";
|
|
3392
|
+
data: string;
|
|
3393
|
+
driverParam: string;
|
|
3394
|
+
notNull: true;
|
|
3395
|
+
hasDefault: false;
|
|
3396
|
+
isPrimaryKey: false;
|
|
3397
|
+
isAutoincrement: false;
|
|
3398
|
+
hasRuntimeDefault: false;
|
|
3399
|
+
enumValues: [string, ...string[]];
|
|
3400
|
+
baseColumn: never;
|
|
3401
|
+
identity: undefined;
|
|
3402
|
+
generated: undefined;
|
|
3403
|
+
}, {}, {
|
|
3404
|
+
length: 16;
|
|
3405
|
+
}>;
|
|
3406
|
+
action: import("drizzle-orm/pg-core").PgColumn<{
|
|
3407
|
+
name: "action";
|
|
3408
|
+
tableName: "byline_audit_log";
|
|
3409
|
+
dataType: "string";
|
|
3410
|
+
columnType: "PgVarchar";
|
|
3411
|
+
data: string;
|
|
3412
|
+
driverParam: string;
|
|
3413
|
+
notNull: true;
|
|
3414
|
+
hasDefault: false;
|
|
3415
|
+
isPrimaryKey: false;
|
|
3416
|
+
isAutoincrement: false;
|
|
3417
|
+
hasRuntimeDefault: false;
|
|
3418
|
+
enumValues: [string, ...string[]];
|
|
3419
|
+
baseColumn: never;
|
|
3420
|
+
identity: undefined;
|
|
3421
|
+
generated: undefined;
|
|
3422
|
+
}, {}, {
|
|
3423
|
+
length: 64;
|
|
3424
|
+
}>;
|
|
3425
|
+
field: import("drizzle-orm/pg-core").PgColumn<{
|
|
3426
|
+
name: "field";
|
|
3427
|
+
tableName: "byline_audit_log";
|
|
3428
|
+
dataType: "string";
|
|
3429
|
+
columnType: "PgVarchar";
|
|
3430
|
+
data: string;
|
|
3431
|
+
driverParam: string;
|
|
3432
|
+
notNull: false;
|
|
3433
|
+
hasDefault: false;
|
|
3434
|
+
isPrimaryKey: false;
|
|
3435
|
+
isAutoincrement: false;
|
|
3436
|
+
hasRuntimeDefault: false;
|
|
3437
|
+
enumValues: [string, ...string[]];
|
|
3438
|
+
baseColumn: never;
|
|
3439
|
+
identity: undefined;
|
|
3440
|
+
generated: undefined;
|
|
3441
|
+
}, {}, {
|
|
3442
|
+
length: 128;
|
|
3443
|
+
}>;
|
|
3444
|
+
before: import("drizzle-orm/pg-core").PgColumn<{
|
|
3445
|
+
name: "before";
|
|
3446
|
+
tableName: "byline_audit_log";
|
|
3447
|
+
dataType: "json";
|
|
3448
|
+
columnType: "PgJsonb";
|
|
3449
|
+
data: unknown;
|
|
3450
|
+
driverParam: unknown;
|
|
3451
|
+
notNull: false;
|
|
3452
|
+
hasDefault: false;
|
|
3453
|
+
isPrimaryKey: false;
|
|
3454
|
+
isAutoincrement: false;
|
|
3455
|
+
hasRuntimeDefault: false;
|
|
3456
|
+
enumValues: undefined;
|
|
3457
|
+
baseColumn: never;
|
|
3458
|
+
identity: undefined;
|
|
3459
|
+
generated: undefined;
|
|
3460
|
+
}, {}, {}>;
|
|
3461
|
+
after: import("drizzle-orm/pg-core").PgColumn<{
|
|
3462
|
+
name: "after";
|
|
3463
|
+
tableName: "byline_audit_log";
|
|
3464
|
+
dataType: "json";
|
|
3465
|
+
columnType: "PgJsonb";
|
|
3466
|
+
data: unknown;
|
|
3467
|
+
driverParam: unknown;
|
|
3468
|
+
notNull: false;
|
|
3469
|
+
hasDefault: false;
|
|
3470
|
+
isPrimaryKey: false;
|
|
3471
|
+
isAutoincrement: false;
|
|
3472
|
+
hasRuntimeDefault: false;
|
|
3473
|
+
enumValues: undefined;
|
|
3474
|
+
baseColumn: never;
|
|
3475
|
+
identity: undefined;
|
|
3476
|
+
generated: undefined;
|
|
3477
|
+
}, {}, {}>;
|
|
3478
|
+
occurred_at: import("drizzle-orm/pg-core").PgColumn<{
|
|
3479
|
+
name: "occurred_at";
|
|
3480
|
+
tableName: "byline_audit_log";
|
|
3481
|
+
dataType: "date";
|
|
3482
|
+
columnType: "PgTimestamp";
|
|
3483
|
+
data: Date;
|
|
3484
|
+
driverParam: string;
|
|
3485
|
+
notNull: true;
|
|
3486
|
+
hasDefault: true;
|
|
3487
|
+
isPrimaryKey: false;
|
|
3488
|
+
isAutoincrement: false;
|
|
3489
|
+
hasRuntimeDefault: false;
|
|
3490
|
+
enumValues: undefined;
|
|
3491
|
+
baseColumn: never;
|
|
3492
|
+
identity: undefined;
|
|
3493
|
+
generated: undefined;
|
|
3494
|
+
}, {}, {}>;
|
|
3495
|
+
};
|
|
3496
|
+
dialect: "pg";
|
|
3497
|
+
}>;
|
|
3315
3498
|
export { adminPermissions, adminPermissionsRelations, adminRefreshTokens, adminRefreshTokensRelations, adminRoleAdminUser, adminRoleAdminUserRelations, adminRoles, adminRolesRelations, adminUsers, adminUsersRelations, } from './auth.js';
|
|
@@ -632,6 +632,39 @@ export const jsonStoreRelations = relations(jsonStore, ({ one }) => ({
|
|
|
632
632
|
}),
|
|
633
633
|
}));
|
|
634
634
|
// ---------------------------------------------------------------------------
|
|
635
|
+
// Audit log — byline_audit_log
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
// Document-grain audit log. Records the changes the immutable version stream
|
|
638
|
+
// does NOT capture an actor for: non-versioned system-field writes (path,
|
|
639
|
+
// availableLocales), in-place status transitions, and deletions — plus, later,
|
|
640
|
+
// admin-module events. One generic table (nullable `document_id`, namespaced
|
|
641
|
+
// `action`) so the system-wide activity report and future admin-realm auditing
|
|
642
|
+
// fit without a second migration. Append-only and deliberately **FK-free**: an
|
|
643
|
+
// audit row is an immutable historical fact that must outlive the document,
|
|
644
|
+
// collection, or actor it references — a `document.deleted` row cannot be
|
|
645
|
+
// allowed to cascade-delete itself. See docs/AUDIT.md — Workstream 2.
|
|
646
|
+
export const auditLog = pgTable('byline_audit_log', {
|
|
647
|
+
id: uuid('id').primaryKey(), // UUIDv7 — time-ordered, so id ordering ≈ time ordering
|
|
648
|
+
document_id: uuid('document_id'), // NULL for admin-realm (non-document) events; no FK
|
|
649
|
+
collection_id: uuid('collection_id'), // no FK — outlives the collection
|
|
650
|
+
actor_id: uuid('actor_id'), // NULL = system / internal tooling / non-UUID synthetic actor
|
|
651
|
+
actor_realm: varchar('actor_realm', { length: 16 }).notNull(), // 'admin' | 'user' | 'system'
|
|
652
|
+
action: varchar('action', { length: 64 }).notNull(), // namespaced, e.g. 'document.path.changed'
|
|
653
|
+
field: varchar('field', { length: 128 }), // the changed field where meaningful (e.g. 'path'), else NULL
|
|
654
|
+
before: jsonb('before'),
|
|
655
|
+
after: jsonb('after'),
|
|
656
|
+
occurred_at: timestamp('occurred_at', { precision: 6, withTimezone: true })
|
|
657
|
+
.notNull()
|
|
658
|
+
.defaultNow(),
|
|
659
|
+
}, (table) => [
|
|
660
|
+
// Per-document history, time-ordered (id is UUIDv7).
|
|
661
|
+
index('idx_audit_log_document_id').on(table.document_id, table.id),
|
|
662
|
+
// Per-actor activity — the system-wide report's actor filter.
|
|
663
|
+
index('idx_audit_log_actor_id').on(table.actor_id, table.id),
|
|
664
|
+
// Action-type filter for the activity report.
|
|
665
|
+
index('idx_audit_log_action').on(table.action, table.id),
|
|
666
|
+
]);
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
635
668
|
// Auth schema — byline_admin_users, byline_admin_roles, etc.
|
|
636
669
|
// See ./auth.ts for definitions and rationale.
|
|
637
670
|
// ---------------------------------------------------------------------------
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,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
11
|
import { DBManagerImpl, TXManagerImpl } from './lib/db-manager.js';
|
|
12
|
+
import { createAuditCommands } from './modules/audit/audit-commands.js';
|
|
13
|
+
import { createAuditQueries } from './modules/audit/audit-queries.js';
|
|
12
14
|
import { createCounterCommands } from './modules/counters/counters-commands.js';
|
|
13
15
|
import { createCommandBuilders, } from './modules/storage/storage-commands.js';
|
|
14
16
|
import { createQueryBuilders } from './modules/storage/storage-queries.js';
|
|
@@ -30,12 +32,20 @@ export const pgAdapter = ({ connectionString, collections, defaultContentLocale,
|
|
|
30
32
|
const commandBuilders = createCommandBuilders(dbManager, defaultContentLocale);
|
|
31
33
|
const queryBuilders = createQueryBuilders(db, collections, defaultContentLocale);
|
|
32
34
|
const counterCommands = createCounterCommands(db);
|
|
35
|
+
// Audit writes run on the DBManager so they join an ambient `withTransaction`
|
|
36
|
+
// (atomic with the mutation they record); audit reads run on the pool.
|
|
37
|
+
const auditCommands = createAuditCommands(dbManager);
|
|
38
|
+
const auditQueries = createAuditQueries(db);
|
|
33
39
|
return {
|
|
34
40
|
commands: {
|
|
35
41
|
...commandBuilders,
|
|
36
42
|
counters: counterCommands,
|
|
43
|
+
audit: auditCommands,
|
|
44
|
+
},
|
|
45
|
+
queries: {
|
|
46
|
+
...queryBuilders,
|
|
47
|
+
audit: auditQueries,
|
|
37
48
|
},
|
|
38
|
-
queries: queryBuilders,
|
|
39
49
|
withTransaction: (fn) => txManager.withTransaction(fn),
|
|
40
50
|
drizzle: db,
|
|
41
51
|
pool,
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
* Append-only audit-log writes (docs/AUDIT.md — Workstream 2). A deliberately
|
|
10
|
+
* dumb command: it inserts one row and knows nothing about *which* changes
|
|
11
|
+
* warrant an audit entry — that policy lives in the lifecycle services, which
|
|
12
|
+
* wrap the mutation + this append in `withTransaction` so they commit
|
|
13
|
+
* atomically. Runs on the `DBManager` executor, so an `append` issued inside a
|
|
14
|
+
* `withTransaction` boundary joins the ambient transaction.
|
|
15
|
+
*/
|
|
16
|
+
import type { AuditLogAppendInput, IAuditCommands } from '@byline/core';
|
|
17
|
+
import type { DBManager } from '../../lib/db-manager.js';
|
|
18
|
+
export declare class AuditCommands implements IAuditCommands {
|
|
19
|
+
private dbManager;
|
|
20
|
+
constructor(dbManager: DBManager);
|
|
21
|
+
/** Ambient transaction when a `withTransaction` boundary is open, else the pool. */
|
|
22
|
+
private get db();
|
|
23
|
+
append(input: AuditLogAppendInput): Promise<{
|
|
24
|
+
id: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createAuditCommands(dbManager: DBManager): AuditCommands;
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { v7 as uuidv7 } from 'uuid';
|
|
9
|
+
import { auditLog } from '../../database/schema/index.js';
|
|
10
|
+
export class AuditCommands {
|
|
11
|
+
dbManager;
|
|
12
|
+
constructor(dbManager) {
|
|
13
|
+
this.dbManager = dbManager;
|
|
14
|
+
}
|
|
15
|
+
/** Ambient transaction when a `withTransaction` boundary is open, else the pool. */
|
|
16
|
+
get db() {
|
|
17
|
+
return this.dbManager.get();
|
|
18
|
+
}
|
|
19
|
+
async append(input) {
|
|
20
|
+
const id = uuidv7(); // time-ordered, so id ordering ≈ occurred_at ordering
|
|
21
|
+
await this.db.insert(auditLog).values({
|
|
22
|
+
id,
|
|
23
|
+
document_id: input.documentId ?? null,
|
|
24
|
+
collection_id: input.collectionId ?? null,
|
|
25
|
+
actor_id: input.actorId ?? null,
|
|
26
|
+
actor_realm: input.actorRealm,
|
|
27
|
+
action: input.action,
|
|
28
|
+
field: input.field ?? null,
|
|
29
|
+
// `?? null` only coerces undefined → SQL NULL; a real `false`/`0`/`''`
|
|
30
|
+
// before/after value is preserved.
|
|
31
|
+
before: input.before ?? null,
|
|
32
|
+
after: input.after ?? null,
|
|
33
|
+
// occurred_at defaults to now() at the DB.
|
|
34
|
+
});
|
|
35
|
+
return { id };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createAuditCommands(dbManager) {
|
|
39
|
+
return new AuditCommands(dbManager);
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
* Audit-log reads (docs/AUDIT.md — Workstreams 3 & 4). Reads run on the pool
|
|
10
|
+
* directly — they never need to join an audit write's transaction — so this
|
|
11
|
+
* takes the raw connection rather than the `DBManager`. Access scoping is the
|
|
12
|
+
* caller's responsibility (the document's own read gate); these queries do no
|
|
13
|
+
* scoping of their own.
|
|
14
|
+
*/
|
|
15
|
+
import type { AuditLogPage, IAuditQueries } from '@byline/core';
|
|
16
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
17
|
+
import type * as schema from '../../database/schema/index.js';
|
|
18
|
+
type DatabaseConnection = NodePgDatabase<typeof schema>;
|
|
19
|
+
export declare class AuditQueries implements IAuditQueries {
|
|
20
|
+
private db;
|
|
21
|
+
constructor(db: DatabaseConnection);
|
|
22
|
+
getDocumentAuditLog(params: {
|
|
23
|
+
document_id: string;
|
|
24
|
+
page?: number;
|
|
25
|
+
page_size?: number;
|
|
26
|
+
}): Promise<AuditLogPage>;
|
|
27
|
+
}
|
|
28
|
+
export declare function createAuditQueries(db: DatabaseConnection): AuditQueries;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { desc, eq, sql } from 'drizzle-orm';
|
|
9
|
+
import { auditLog } from '../../database/schema/index.js';
|
|
10
|
+
function toEntry(row) {
|
|
11
|
+
return {
|
|
12
|
+
id: row.id,
|
|
13
|
+
documentId: row.document_id,
|
|
14
|
+
collectionId: row.collection_id,
|
|
15
|
+
actorId: row.actor_id,
|
|
16
|
+
actorRealm: row.actor_realm,
|
|
17
|
+
action: row.action,
|
|
18
|
+
field: row.field,
|
|
19
|
+
before: row.before,
|
|
20
|
+
after: row.after,
|
|
21
|
+
occurredAt: row.occurred_at,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export class AuditQueries {
|
|
25
|
+
db;
|
|
26
|
+
constructor(db) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
}
|
|
29
|
+
async getDocumentAuditLog(params) {
|
|
30
|
+
const page = params.page ?? 1;
|
|
31
|
+
const pageSize = params.page_size ?? 20;
|
|
32
|
+
const offset = (page - 1) * pageSize;
|
|
33
|
+
const totalResult = await this.db
|
|
34
|
+
.select({ count: sql `count(*)` })
|
|
35
|
+
.from(auditLog)
|
|
36
|
+
.where(eq(auditLog.document_id, params.document_id));
|
|
37
|
+
const total = Number(totalResult[0]?.count) || 0;
|
|
38
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
39
|
+
const rows = await this.db
|
|
40
|
+
.select()
|
|
41
|
+
.from(auditLog)
|
|
42
|
+
.where(eq(auditLog.document_id, params.document_id))
|
|
43
|
+
// id is UUIDv7 — DESC is newest-first without a separate sort column.
|
|
44
|
+
.orderBy(desc(auditLog.id))
|
|
45
|
+
.limit(pageSize)
|
|
46
|
+
.offset(offset);
|
|
47
|
+
return {
|
|
48
|
+
entries: rows.map(toEntry),
|
|
49
|
+
meta: { total, page, pageSize, totalPages },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function createAuditQueries(db) {
|
|
54
|
+
return new AuditQueries(db);
|
|
55
|
+
}
|
|
@@ -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,117 @@
|
|
|
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 the audit-log storage layer (docs/AUDIT.md — W2):
|
|
10
|
+
* `audit.append` (write) + `audit.getDocumentAuditLog` (read), and the
|
|
11
|
+
* load-bearing property that an `append` issued inside `withTransaction`
|
|
12
|
+
* commits or rolls back **with** the transaction it is part of.
|
|
13
|
+
*/
|
|
14
|
+
import { v7 as uuidv7 } from 'uuid';
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
16
|
+
import { setupTestDB, teardownTestDB } from '../../../lib/test-helper.js';
|
|
17
|
+
import { createAuditCommands } from '../audit-commands.js';
|
|
18
|
+
import { createAuditQueries } from '../audit-queries.js';
|
|
19
|
+
let auditCommands;
|
|
20
|
+
let auditQueries;
|
|
21
|
+
let txManager;
|
|
22
|
+
describe('audit-log storage', () => {
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
const testDB = setupTestDB([]);
|
|
25
|
+
txManager = testDB.txManager;
|
|
26
|
+
auditCommands = createAuditCommands(testDB.dbManager);
|
|
27
|
+
auditQueries = createAuditQueries(testDB.db);
|
|
28
|
+
});
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await teardownTestDB();
|
|
31
|
+
});
|
|
32
|
+
it('appends an entry and reads it back with full field fidelity', async () => {
|
|
33
|
+
const documentId = uuidv7();
|
|
34
|
+
const collectionId = uuidv7();
|
|
35
|
+
const actorId = uuidv7();
|
|
36
|
+
await auditCommands.append({
|
|
37
|
+
documentId,
|
|
38
|
+
collectionId,
|
|
39
|
+
actorId,
|
|
40
|
+
actorRealm: 'admin',
|
|
41
|
+
action: 'document.path.changed',
|
|
42
|
+
field: 'path',
|
|
43
|
+
before: 'old-slug',
|
|
44
|
+
after: 'new-slug',
|
|
45
|
+
});
|
|
46
|
+
const page = await auditQueries.getDocumentAuditLog({ document_id: documentId });
|
|
47
|
+
expect(page.meta.total).toBe(1);
|
|
48
|
+
const entry = page.entries[0];
|
|
49
|
+
expect(entry.documentId).toBe(documentId);
|
|
50
|
+
expect(entry.collectionId).toBe(collectionId);
|
|
51
|
+
expect(entry.actorId).toBe(actorId);
|
|
52
|
+
expect(entry.actorRealm).toBe('admin');
|
|
53
|
+
expect(entry.action).toBe('document.path.changed');
|
|
54
|
+
expect(entry.field).toBe('path');
|
|
55
|
+
expect(entry.before).toBe('old-slug');
|
|
56
|
+
expect(entry.after).toBe('new-slug');
|
|
57
|
+
expect(entry.occurredAt).toBeInstanceOf(Date);
|
|
58
|
+
});
|
|
59
|
+
it('persists NULL actor + structured before/after, and returns newest-first', async () => {
|
|
60
|
+
const documentId = uuidv7();
|
|
61
|
+
// A system write (no actor) with object-shaped before/after.
|
|
62
|
+
await auditCommands.append({
|
|
63
|
+
documentId,
|
|
64
|
+
actorId: null,
|
|
65
|
+
actorRealm: 'system',
|
|
66
|
+
action: 'document.locales.changed',
|
|
67
|
+
field: 'availableLocales',
|
|
68
|
+
before: ['en'],
|
|
69
|
+
after: ['en', 'fr'],
|
|
70
|
+
});
|
|
71
|
+
await auditCommands.append({
|
|
72
|
+
documentId,
|
|
73
|
+
actorRealm: 'admin',
|
|
74
|
+
action: 'document.status.changed',
|
|
75
|
+
field: 'status',
|
|
76
|
+
before: 'draft',
|
|
77
|
+
after: 'published',
|
|
78
|
+
});
|
|
79
|
+
const page = await auditQueries.getDocumentAuditLog({ document_id: documentId });
|
|
80
|
+
expect(page.meta.total).toBe(2);
|
|
81
|
+
// UUIDv7 ids → newest first: the status change was appended last.
|
|
82
|
+
expect(page.entries[0]?.action).toBe('document.status.changed');
|
|
83
|
+
expect(page.entries[1]?.action).toBe('document.locales.changed');
|
|
84
|
+
const localesEntry = page.entries[1];
|
|
85
|
+
expect(localesEntry.actorId).toBeNull();
|
|
86
|
+
expect(localesEntry.before).toEqual(['en']);
|
|
87
|
+
expect(localesEntry.after).toEqual(['en', 'fr']);
|
|
88
|
+
});
|
|
89
|
+
it('rolls back the audit row when its enclosing transaction throws', async () => {
|
|
90
|
+
const documentId = uuidv7();
|
|
91
|
+
await expect(txManager.withTransaction(async () => {
|
|
92
|
+
await auditCommands.append({
|
|
93
|
+
documentId,
|
|
94
|
+
actorRealm: 'admin',
|
|
95
|
+
action: 'document.deleted',
|
|
96
|
+
});
|
|
97
|
+
// The unit of work fails after the audit append. If the append had run
|
|
98
|
+
// on the pool rather than the ambient tx, the row would survive.
|
|
99
|
+
throw new Error('boom');
|
|
100
|
+
})).rejects.toThrow('boom');
|
|
101
|
+
const page = await auditQueries.getDocumentAuditLog({ document_id: documentId });
|
|
102
|
+
expect(page.meta.total).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
it('commits the audit row with its enclosing transaction', async () => {
|
|
105
|
+
const documentId = uuidv7();
|
|
106
|
+
await txManager.withTransaction(async () => {
|
|
107
|
+
await auditCommands.append({
|
|
108
|
+
documentId,
|
|
109
|
+
actorRealm: 'admin',
|
|
110
|
+
action: 'document.deleted',
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
const page = await auditQueries.getDocumentAuditLog({ document_id: documentId });
|
|
114
|
+
expect(page.meta.total).toBe(1);
|
|
115
|
+
expect(page.entries[0]?.action).toBe('document.deleted');
|
|
116
|
+
});
|
|
117
|
+
});
|
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.10.1",
|
|
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/admin": "3.10.1",
|
|
61
|
+
"@byline/auth": "3.10.1",
|
|
62
|
+
"@byline/core": "3.10.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@biomejs/biome": "2.4.15",
|