@proteinjs/db 1.55.1 → 1.57.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/CHANGELOG.md +22 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.d.ts.map +1 -1
- package/dist/generated/test/index.js +5 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/Db.d.ts +22 -6
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +35 -15
- package/dist/src/Db.js.map +1 -1
- package/dist/src/TableWatcher.d.ts +15 -6
- package/dist/src/TableWatcher.d.ts.map +1 -1
- package/dist/src/TableWatcher.js.map +1 -1
- package/dist/src/TableWatcherRunner.d.ts +14 -6
- package/dist/src/TableWatcherRunner.d.ts.map +1 -1
- package/dist/src/TableWatcherRunner.js +19 -12
- package/dist/src/TableWatcherRunner.js.map +1 -1
- package/dist/test/DbIsSystem.test.d.ts +2 -0
- package/dist/test/DbIsSystem.test.d.ts.map +1 -0
- package/dist/test/DbIsSystem.test.js +280 -0
- package/dist/test/DbIsSystem.test.js.map +1 -0
- package/dist/test/index.d.ts +2 -0
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +2 -0
- package/dist/test/index.js.map +1 -1
- package/dist/test/reusable/TableWatcherAuthorityTests.d.ts +13 -0
- package/dist/test/reusable/TableWatcherAuthorityTests.d.ts.map +1 -0
- package/dist/test/reusable/TableWatcherAuthorityTests.js +392 -0
- package/dist/test/reusable/TableWatcherAuthorityTests.js.map +1 -0
- package/dist/test/util/DbTestEnvironment.d.ts.map +1 -1
- package/dist/test/util/DbTestEnvironment.js +3 -2
- package/dist/test/util/DbTestEnvironment.js.map +1 -1
- package/dist/test/util/tables/tableWatcherAuthorityTestTables.d.ts +35 -0
- package/dist/test/util/tables/tableWatcherAuthorityTestTables.d.ts.map +1 -0
- package/dist/test/util/tables/tableWatcherAuthorityTestTables.js +113 -0
- package/dist/test/util/tables/tableWatcherAuthorityTestTables.js.map +1 -0
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +5 -1
- package/package.json +2 -2
- package/src/Db.ts +50 -21
- package/src/TableWatcher.ts +17 -6
- package/src/TableWatcherRunner.ts +25 -12
- package/test/DbIsSystem.test.ts +173 -0
- package/test/index.ts +2 -0
- package/test/reusable/TableWatcherAuthorityTests.ts +204 -0
- package/test/util/DbTestEnvironment.ts +2 -0
- package/test/util/tables/tableWatcherAuthorityTestTables.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "c33ab303e81e366f73bb2cfac13f818567c37306"
|
|
70
70
|
}
|
package/src/Db.ts
CHANGED
|
@@ -167,6 +167,16 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
167
167
|
: this.getDefaultTransactionContextFactory();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Whether this Db writes as the SYSTEM (`getDbAsSystem()`: no table-auth gate, no scope) rather
|
|
172
|
+
* than as the session's user. The Db a write hands its hooks ({@link newHookDb}) answers for the
|
|
173
|
+
* invoking write, so a watcher that must tell a user's write from a system one (archive on a
|
|
174
|
+
* user's delete; stand aside on a purge) reads the write's mode here instead of the session.
|
|
175
|
+
*/
|
|
176
|
+
isSystem(): boolean {
|
|
177
|
+
return this.runAsSystem;
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
static getDefaultDbDriver(): DbDriver {
|
|
171
181
|
if (!getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY]) {
|
|
172
182
|
const defaultDbDriverFactory = SourceRepository.get().object<DefaultDbDriverFactory>(
|
|
@@ -241,7 +251,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
241
251
|
|
|
242
252
|
let recordCopy = Object.assign({}, record);
|
|
243
253
|
await addDefaultFieldValues(table, recordCopy, this.runAsSystem);
|
|
244
|
-
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy);
|
|
254
|
+
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy, this.newHookDb());
|
|
245
255
|
await this.addColumnInsertHooks(table, recordCopy);
|
|
246
256
|
const encryptionContext = await this.encryptionWriteContext(table, recordCopy);
|
|
247
257
|
const recordSerializer = new RecordSerializer(table, encryptionContext);
|
|
@@ -269,7 +279,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
269
279
|
);
|
|
270
280
|
}
|
|
271
281
|
await this.runColumnAfterInsertHooks(table, recordCopy);
|
|
272
|
-
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
282
|
+
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T, this.newHookDb());
|
|
273
283
|
return recordCopy as T;
|
|
274
284
|
}
|
|
275
285
|
|
|
@@ -311,7 +321,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
311
321
|
for (const record of records) {
|
|
312
322
|
let recordCopy = Object.assign({}, record);
|
|
313
323
|
await addDefaultFieldValues(table, recordCopy, this.runAsSystem);
|
|
314
|
-
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy);
|
|
324
|
+
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy, this.newHookDb());
|
|
315
325
|
await this.addColumnInsertHooks(table, recordCopy);
|
|
316
326
|
const encryptionContext = await this.encryptionWriteContext(table, recordCopy);
|
|
317
327
|
const serializedRecord = await new RecordSerializer(table, encryptionContext).serialize(recordCopy);
|
|
@@ -343,7 +353,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
343
353
|
);
|
|
344
354
|
}
|
|
345
355
|
await this.runColumnAfterInsertHooks(table, recordCopy);
|
|
346
|
-
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
356
|
+
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T, this.newHookDb());
|
|
347
357
|
}
|
|
348
358
|
return prepared.map((row) => row.recordCopy as T);
|
|
349
359
|
}
|
|
@@ -389,7 +399,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
389
399
|
qb.condition({ field: 'id', operator: '=', value: recordCopy.id as T[keyof T] });
|
|
390
400
|
}
|
|
391
401
|
|
|
392
|
-
recordCopy = await this.tableWatcherRunner.runBeforeUpdateTableWatchers(table, recordCopy, qb);
|
|
402
|
+
recordCopy = await this.tableWatcherRunner.runBeforeUpdateTableWatchers(table, recordCopy, qb, this.newHookDb());
|
|
393
403
|
let recordUpdateCount: number;
|
|
394
404
|
if (await this.updateTouchesEncryptedColumns(table, recordCopy)) {
|
|
395
405
|
recordUpdateCount = await this.updateEncrypted(table, recordCopy, qb, driverOptions);
|
|
@@ -412,7 +422,13 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
412
422
|
await this.runZeroRowFilteredWriteHooks(table, id, 'write');
|
|
413
423
|
}
|
|
414
424
|
}
|
|
415
|
-
await this.tableWatcherRunner.runAfterUpdateTableWatchers(
|
|
425
|
+
await this.tableWatcherRunner.runAfterUpdateTableWatchers(
|
|
426
|
+
table,
|
|
427
|
+
recordUpdateCount,
|
|
428
|
+
recordCopy,
|
|
429
|
+
qb,
|
|
430
|
+
this.newHookDb()
|
|
431
|
+
);
|
|
416
432
|
return recordUpdateCount;
|
|
417
433
|
}
|
|
418
434
|
|
|
@@ -542,34 +558,31 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
542
558
|
const generateDelete = (config: DbDriverDmlStatementConfig) =>
|
|
543
559
|
new StatementFactory<T>().delete(table.name, deleteQb, this.statementConfigFactory.getStatementConfig(config));
|
|
544
560
|
await this.runColumnBeforeDeletes(table, recordsToDelete);
|
|
545
|
-
await this.tableWatcherRunner.runBeforeDeleteTableWatchers(table, recordsToDelete, qb, deleteQb);
|
|
561
|
+
await this.tableWatcherRunner.runBeforeDeleteTableWatchers(table, recordsToDelete, qb, deleteQb, this.newHookDb());
|
|
546
562
|
const recordDeleteCount = await this.dbDriver.runDml(generateDelete, this.transactionForDriver());
|
|
547
563
|
await this.runCascadeDeletions(table, recordsToDelete);
|
|
548
564
|
await this.runColumnReverseCascadeDeletions(table, recordsToDelete);
|
|
549
565
|
await this.deleteEncryptionTokenRows(table, recordsToDeleteIds as string[]);
|
|
550
|
-
await this.tableWatcherRunner.runAfterDeleteTableWatchers(
|
|
566
|
+
await this.tableWatcherRunner.runAfterDeleteTableWatchers(
|
|
567
|
+
table,
|
|
568
|
+
recordDeleteCount,
|
|
569
|
+
recordsToDelete,
|
|
570
|
+
qb,
|
|
571
|
+
deleteQb,
|
|
572
|
+
this.newHookDb()
|
|
573
|
+
);
|
|
551
574
|
return recordDeleteCount;
|
|
552
575
|
}
|
|
553
576
|
|
|
554
577
|
/**
|
|
555
|
-
* A column's `beforeDelete` hook
|
|
556
|
-
*
|
|
557
|
-
* The hooks cascade a delete through reference columns, and a system delete's cascade must stay
|
|
558
|
-
* system — handed a default (non-system) Db, the cascade ran as whoever the session held, which
|
|
559
|
-
* narrowed it to that user's rows inside a request and refused it outright in a background
|
|
560
|
-
* context, where a scoped table finds no user to scope the query to.
|
|
578
|
+
* A column's `beforeDelete` hook cascades the delete through reference columns, with the
|
|
579
|
+
* authority of the write that fired it ({@link newHookDb}).
|
|
561
580
|
*/
|
|
562
581
|
private async runColumnBeforeDeletes(table: Table<any>, recordsToDelete: Record[]) {
|
|
563
582
|
for (const columnPropertyName in table.columns) {
|
|
564
583
|
const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
|
|
565
584
|
if (typeof column.beforeDelete !== 'undefined') {
|
|
566
|
-
await column.beforeDelete(
|
|
567
|
-
table,
|
|
568
|
-
columnPropertyName,
|
|
569
|
-
recordsToDelete,
|
|
570
|
-
this.getTable,
|
|
571
|
-
new Db(this.dbDriver, this.getTable, this.transactionContextFactory, this.runAsSystem)
|
|
572
|
-
);
|
|
585
|
+
await column.beforeDelete(table, columnPropertyName, recordsToDelete, this.getTable, this.newHookDb());
|
|
573
586
|
}
|
|
574
587
|
}
|
|
575
588
|
}
|
|
@@ -1171,6 +1184,22 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
1171
1184
|
return new Db<any>(this.dbDriver, this.getTable, this.transactionContextFactory, true);
|
|
1172
1185
|
}
|
|
1173
1186
|
|
|
1187
|
+
/**
|
|
1188
|
+
* The Db a hook is handed — a column's `beforeDelete`, a table watcher's six hooks: an instance
|
|
1189
|
+
* riding THIS instance's driver, tables and transaction context AND its system mode, so the hook
|
|
1190
|
+
* runs with the authority of the write that fired it. A hook that opened a Db of its own
|
|
1191
|
+
* (`getDb()`, a session-scoped Db) ran as whoever the session held: a system write's follow-on
|
|
1192
|
+
* narrowed to that user's rows inside a request, and refused outright in a background context
|
|
1193
|
+
* (an account purge, a sweep, a deploy gate), where a scoped table finds no user to scope the
|
|
1194
|
+
* query to — the cascade left orphans, the watcher left its rows (a message's topic links, a
|
|
1195
|
+
* media row's file) and either swallowed or raised the "No user in scope context" error.
|
|
1196
|
+
* Transaction state is instance state ({@link newSelfWrapDb}), so the hook rides the ambient
|
|
1197
|
+
* transaction through the shared context and never this instance's own.
|
|
1198
|
+
*/
|
|
1199
|
+
private newHookDb(): Db<any> {
|
|
1200
|
+
return new Db<any>(this.dbDriver, this.getTable, this.transactionContextFactory, this.runAsSystem);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1174
1203
|
/**
|
|
1175
1204
|
* A fresh instance for self-wrapping RMW verbs (`updateArrayMembership`, `updatePreserving`)
|
|
1176
1205
|
* in a transaction. `runTransaction` carries the open transaction as INSTANCE state, and these
|
package/src/TableWatcher.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Loadable, SourceRepository } from '@proteinjs/reflection';
|
|
|
2
2
|
import { QueryBuilder } from '@proteinjs/db-query';
|
|
3
3
|
import { Record } from './Record';
|
|
4
4
|
import { Table } from './Table';
|
|
5
|
+
import { Db } from './Db';
|
|
5
6
|
|
|
6
7
|
type RequiredProps<T, K extends keyof T> = Omit<T, K> & {
|
|
7
8
|
[P in K]-?: T[P];
|
|
@@ -65,22 +66,32 @@ export const getTableWatcherMap = <R extends Record = any>(): TableWatcherMap<R>
|
|
|
65
66
|
export const getTableWatchers = <R extends Record = any>() =>
|
|
66
67
|
SourceRepository.get().objects<TableWatcher<R>>('@proteinjs/db/TableWatcher');
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Hooks around a table's writes. Every hook's trailing `db` is the Db the invoking write made
|
|
71
|
+
* for its hooks — the write's driver, tables and transaction context AND its system mode — so a
|
|
72
|
+
* watcher's own reads and writes carry the authority of the write that fired it: a system write's
|
|
73
|
+
* follow-on runs as system (a background purge with no session user), a request's as its session.
|
|
74
|
+
* The Db hands it to every hook; a watcher never opens a Db of its own for the write's follow-on.
|
|
75
|
+
* Optional in the type so an implementer that ignores it still conforms.
|
|
76
|
+
*/
|
|
68
77
|
export interface TableWatcher<R extends Record = Record> extends Loadable {
|
|
69
78
|
name(): string;
|
|
70
79
|
table(): Table<R>;
|
|
71
|
-
beforeInsert?<T extends R>(record: Omit<T, keyof R
|
|
72
|
-
afterInsert?<T extends R>(record: T): Promise<void>;
|
|
73
|
-
beforeUpdate?<T extends R>(record: Partial<T>, qb: QueryBuilder<T
|
|
74
|
-
afterUpdate?<T extends R>(recordUpdateCount: number, record: Partial<T>, qb: QueryBuilder<T
|
|
80
|
+
beforeInsert?<T extends R>(record: Omit<T, keyof R>, db?: Db): Promise<Omit<T, keyof R>>;
|
|
81
|
+
afterInsert?<T extends R>(record: T, db?: Db): Promise<void>;
|
|
82
|
+
beforeUpdate?<T extends R>(record: Partial<T>, qb: QueryBuilder<T>, db?: Db): Promise<Partial<T>>;
|
|
83
|
+
afterUpdate?<T extends R>(recordUpdateCount: number, record: Partial<T>, qb: QueryBuilder<T>, db?: Db): Promise<void>;
|
|
75
84
|
beforeDelete?<T extends R>(
|
|
76
85
|
recordsToDelete: T[],
|
|
77
86
|
initialQb: QueryBuilder<T>,
|
|
78
|
-
deleteQb: QueryBuilder<T
|
|
87
|
+
deleteQb: QueryBuilder<T>,
|
|
88
|
+
db?: Db
|
|
79
89
|
): Promise<void>;
|
|
80
90
|
afterDelete?<T extends R>(
|
|
81
91
|
recordDeleteCount: number,
|
|
82
92
|
deletedRecords: T[],
|
|
83
93
|
initialQb: QueryBuilder<T>,
|
|
84
|
-
deleteQb: QueryBuilder<T
|
|
94
|
+
deleteQb: QueryBuilder<T>,
|
|
95
|
+
db?: Db
|
|
85
96
|
): Promise<void>;
|
|
86
97
|
}
|
|
@@ -2,11 +2,19 @@ import { Logger } from '@proteinjs/logger';
|
|
|
2
2
|
import { TableWatcherMap, getTableWatcherMap } from './TableWatcher';
|
|
3
3
|
import { Record } from './Record';
|
|
4
4
|
import { Table } from './Table';
|
|
5
|
+
import { Db } from './Db';
|
|
5
6
|
import { QueryBuilder } from '@proteinjs/db-query';
|
|
6
7
|
|
|
7
8
|
const getEnvVar = (key: string): string | undefined =>
|
|
8
9
|
typeof process !== 'undefined' && process.env ? process.env[key] : undefined;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Runs a table's watchers around a write. Every hook is handed `db`, the Db the invoking write
|
|
13
|
+
* made for its hooks (`Db.newHookDb`): the write's driver, tables and transaction context AND its
|
|
14
|
+
* system mode, so a watcher's own reads and writes carry the authority of the write that fired
|
|
15
|
+
* it. A watcher never opens a Db of its own for the write's follow-on — a session-scoped Db ran
|
|
16
|
+
* as whoever the session held, which refused a system write's follow-on in a background context.
|
|
17
|
+
*/
|
|
10
18
|
export class TableWatcherRunner<R extends Record = Record> {
|
|
11
19
|
private static tableWatcherMap: TableWatcherMap;
|
|
12
20
|
private logger = new Logger({ name: this.constructor.name, logLevel: getEnvVar('DB_LOG_LEVEL') as any });
|
|
@@ -19,7 +27,8 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
19
27
|
|
|
20
28
|
async runBeforeInsertTableWatchers<T extends R>(
|
|
21
29
|
table: Table<T>,
|
|
22
|
-
record: Omit<T, keyof R
|
|
30
|
+
record: Omit<T, keyof R>,
|
|
31
|
+
db: Db
|
|
23
32
|
): Promise<Omit<T, keyof R>> {
|
|
24
33
|
const tableWatcherMap = TableWatcherRunner.tableWatcherMap[table.name];
|
|
25
34
|
if (!tableWatcherMap) {
|
|
@@ -35,7 +44,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
35
44
|
let updatedRecord: any = record;
|
|
36
45
|
for (const tableWatcher of tableWatchers) {
|
|
37
46
|
this.logger.info({ message: `(${table.name}) Running ${tableWatcher.name()}.beforeInsert` });
|
|
38
|
-
updatedRecord = await tableWatcher.beforeInsert(updatedRecord);
|
|
47
|
+
updatedRecord = await tableWatcher.beforeInsert(updatedRecord, db);
|
|
39
48
|
this.logger.info({ message: `(${table.name}) Finished running ${tableWatcher.name()}.beforeInsert` });
|
|
40
49
|
}
|
|
41
50
|
this.logger.info({ message: `(${table.name}) Finished running before-insert table watchers` });
|
|
@@ -43,7 +52,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
43
52
|
return updatedRecord;
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
async runAfterInsertTableWatchers<T extends R>(table: Table<T>, record: T): Promise<void> {
|
|
55
|
+
async runAfterInsertTableWatchers<T extends R>(table: Table<T>, record: T, db: Db): Promise<void> {
|
|
47
56
|
const tableWatcherMap = TableWatcherRunner.tableWatcherMap[table.name];
|
|
48
57
|
if (!tableWatcherMap) {
|
|
49
58
|
return;
|
|
@@ -57,7 +66,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
57
66
|
this.logger.info({ message: `(${table.name}) Running after-insert table watchers` });
|
|
58
67
|
for (const tableWatcher of tableWatchers) {
|
|
59
68
|
this.logger.info({ message: `(${table.name}) Running ${tableWatcher.name()}.afterInsert` });
|
|
60
|
-
await tableWatcher.afterInsert(record);
|
|
69
|
+
await tableWatcher.afterInsert(record, db);
|
|
61
70
|
this.logger.info({ message: `(${table.name}) Finished running ${tableWatcher.name()}.afterInsert` });
|
|
62
71
|
}
|
|
63
72
|
this.logger.info({ message: `(${table.name}) Finished running after-insert table watchers` });
|
|
@@ -66,7 +75,8 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
66
75
|
async runBeforeUpdateTableWatchers<T extends R>(
|
|
67
76
|
table: Table<T>,
|
|
68
77
|
record: Partial<T>,
|
|
69
|
-
qb: QueryBuilder<T
|
|
78
|
+
qb: QueryBuilder<T>,
|
|
79
|
+
db: Db
|
|
70
80
|
): Promise<Partial<T>> {
|
|
71
81
|
const tableWatcherMap = TableWatcherRunner.tableWatcherMap[table.name];
|
|
72
82
|
if (!tableWatcherMap) {
|
|
@@ -83,7 +93,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
83
93
|
let updatedRecord: any = record;
|
|
84
94
|
for (const tableWatcher of tableWatchers) {
|
|
85
95
|
this.logger.info({ message: `(${table.name}) Running ${tableWatcher.name()}.beforeUpdate` });
|
|
86
|
-
updatedRecord = await tableWatcher.beforeUpdate(updatedRecord, qbCopy);
|
|
96
|
+
updatedRecord = await tableWatcher.beforeUpdate(updatedRecord, qbCopy, db);
|
|
87
97
|
this.logger.info({ message: `(${table.name}) Finished running ${tableWatcher.name()}.beforeUpdate` });
|
|
88
98
|
}
|
|
89
99
|
this.logger.info({ message: `(${table.name}) Finished running before-update table watchers` });
|
|
@@ -95,7 +105,8 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
95
105
|
table: Table<T>,
|
|
96
106
|
recordUpdateCount: number,
|
|
97
107
|
record: Partial<T>,
|
|
98
|
-
qb: QueryBuilder<T
|
|
108
|
+
qb: QueryBuilder<T>,
|
|
109
|
+
db: Db
|
|
99
110
|
): Promise<void> {
|
|
100
111
|
const tableWatcherMap = TableWatcherRunner.tableWatcherMap[table.name];
|
|
101
112
|
if (!tableWatcherMap) {
|
|
@@ -111,7 +122,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
111
122
|
const qbCopy = QueryBuilder.fromQueryBuilder(qb, table.name);
|
|
112
123
|
for (const tableWatcher of tableWatchers) {
|
|
113
124
|
this.logger.info({ message: `(${table.name}) Running ${tableWatcher.name()}.afterUpdate` });
|
|
114
|
-
await tableWatcher.afterUpdate(recordUpdateCount, record, qbCopy);
|
|
125
|
+
await tableWatcher.afterUpdate(recordUpdateCount, record, qbCopy, db);
|
|
115
126
|
this.logger.info({ message: `(${table.name}) Finished running ${tableWatcher.name()}.afterUpdate` });
|
|
116
127
|
}
|
|
117
128
|
this.logger.info({ message: `(${table.name}) Finished running after-update table watchers` });
|
|
@@ -121,7 +132,8 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
121
132
|
table: Table<T>,
|
|
122
133
|
recordsToDelete: T[],
|
|
123
134
|
initialQb: QueryBuilder<T>,
|
|
124
|
-
deleteQb: QueryBuilder<T
|
|
135
|
+
deleteQb: QueryBuilder<T>,
|
|
136
|
+
db: Db
|
|
125
137
|
): Promise<void> {
|
|
126
138
|
const tableWatcherMap = TableWatcherRunner.tableWatcherMap[table.name];
|
|
127
139
|
if (!tableWatcherMap) {
|
|
@@ -137,7 +149,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
137
149
|
const initialQbCopy = QueryBuilder.fromQueryBuilder(initialQb, table.name);
|
|
138
150
|
for (const tableWatcher of tableWatchers) {
|
|
139
151
|
this.logger.info({ message: `(${table.name}) Running ${tableWatcher.name()}.beforeDelete` });
|
|
140
|
-
await tableWatcher.beforeDelete(recordsToDelete, initialQbCopy, deleteQb);
|
|
152
|
+
await tableWatcher.beforeDelete(recordsToDelete, initialQbCopy, deleteQb, db);
|
|
141
153
|
this.logger.info({ message: `(${table.name}) Finished running ${tableWatcher.name()}.beforeDelete` });
|
|
142
154
|
}
|
|
143
155
|
this.logger.info({ message: `(${table.name}) Finished running before-delete table watchers` });
|
|
@@ -148,7 +160,8 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
148
160
|
recordDeleteCount: number,
|
|
149
161
|
deletedRecords: T[],
|
|
150
162
|
initialQb: QueryBuilder<T>,
|
|
151
|
-
deleteQb: QueryBuilder<T
|
|
163
|
+
deleteQb: QueryBuilder<T>,
|
|
164
|
+
db: Db
|
|
152
165
|
): Promise<void> {
|
|
153
166
|
const tableWatcherMap = TableWatcherRunner.tableWatcherMap[table.name];
|
|
154
167
|
if (!tableWatcherMap) {
|
|
@@ -164,7 +177,7 @@ export class TableWatcherRunner<R extends Record = Record> {
|
|
|
164
177
|
const initialQbCopy = QueryBuilder.fromQueryBuilder(initialQb, table.name);
|
|
165
178
|
for (const tableWatcher of tableWatchers) {
|
|
166
179
|
this.logger.info({ message: `(${table.name}) Running ${tableWatcher.name()}.afterDelete` });
|
|
167
|
-
await tableWatcher.afterDelete(recordDeleteCount, deletedRecords, initialQbCopy, deleteQb);
|
|
180
|
+
await tableWatcher.afterDelete(recordDeleteCount, deletedRecords, initialQbCopy, deleteQb, db);
|
|
168
181
|
this.logger.info({ message: `(${table.name}) Finished running ${tableWatcher.name()}.afterDelete` });
|
|
169
182
|
}
|
|
170
183
|
this.logger.info({ message: `(${table.name}) Finished running after-delete table watchers` });
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { SourceRepository } from '@proteinjs/reflection';
|
|
2
|
+
import { Statement } from '@proteinjs/db-query';
|
|
3
|
+
import { Db, DbDriver, DbDriverDmlStatementConfig, getDb, getDbAsSystem } from '../src/Db';
|
|
4
|
+
import { Table } from '../src/Table';
|
|
5
|
+
import { Record, withRecordColumns } from '../src/Record';
|
|
6
|
+
import { StringColumn } from '../src/Columns';
|
|
7
|
+
import { TableWatcher } from '../src/TableWatcher';
|
|
8
|
+
import { TableWatcherRunner } from '../src/TableWatcherRunner';
|
|
9
|
+
import { DefaultTransactionContextFactory, TransactionContextData } from '../src/transaction/TransactionContextFactory';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `Db.isSystem()` — the public read of a write's MODE. A hook that must tell a user's write from
|
|
13
|
+
* the system's (a watcher that archives on a user's delete and stands aside on a purge) reads it
|
|
14
|
+
* off the Db the write hands it, instead of guessing from the session. Pinned both ways on the
|
|
15
|
+
* public factories, and — the pin that matters — on the Db the hook runner hands each watcher:
|
|
16
|
+
* that Db answers for the INVOKING write, whatever the session holds.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface Doc extends Record {
|
|
20
|
+
title: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class DocTable extends Table<Doc> {
|
|
24
|
+
public name = 'is_system_doc';
|
|
25
|
+
/** Every door open to everyone: a session Db reaches the write with no user to resolve. */
|
|
26
|
+
public auth: Table<Doc>['auth'] = { db: { all: 'public' } };
|
|
27
|
+
public columns = withRecordColumns<Doc>({
|
|
28
|
+
title: new StringColumn('title'),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const docTable = new DocTable() as Table<Doc>;
|
|
33
|
+
|
|
34
|
+
/** What each hook was handed: the mode of its trailing Db. */
|
|
35
|
+
const handed: { phase: string; mode: boolean | undefined }[] = [];
|
|
36
|
+
|
|
37
|
+
class ModeRecordingWatcher implements TableWatcher<Doc> {
|
|
38
|
+
name(): string {
|
|
39
|
+
return 'ModeRecordingWatcher';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
table(): Table<Doc> {
|
|
43
|
+
return docTable;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async beforeInsert<T extends Doc>(record: Omit<T, keyof Doc>, db?: Db): Promise<Omit<T, keyof Doc>> {
|
|
47
|
+
handed.push({ phase: 'beforeInsert', mode: db?.isSystem() });
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async afterInsert<T extends Doc>(_record: T, db?: Db): Promise<void> {
|
|
52
|
+
handed.push({ phase: 'afterInsert', mode: db?.isSystem() });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class TestTransactionContextFactory implements DefaultTransactionContextFactory {
|
|
57
|
+
private context: TransactionContextData = {};
|
|
58
|
+
|
|
59
|
+
getTransactionContext(): TransactionContextData {
|
|
60
|
+
return this.context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async runInContext<T>(context: TransactionContextData, fn: () => Promise<T>): Promise<T> {
|
|
64
|
+
const previous = this.context;
|
|
65
|
+
this.context = context;
|
|
66
|
+
try {
|
|
67
|
+
return await fn();
|
|
68
|
+
} finally {
|
|
69
|
+
this.context = previous;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Accepts every statement; no database behind it. */
|
|
75
|
+
class AcceptingDriver implements DbDriver {
|
|
76
|
+
getDbName(): string {
|
|
77
|
+
return 'test';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async createDbIfNotExists(): Promise<void> {}
|
|
81
|
+
|
|
82
|
+
async createDb(): Promise<void> {}
|
|
83
|
+
|
|
84
|
+
async dropDb(): Promise<void> {}
|
|
85
|
+
|
|
86
|
+
getTableManager(): never {
|
|
87
|
+
throw new Error('not used by these tests');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async runQuery(): Promise<never[]> {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async runDml(generateStatement: (config: DbDriverDmlStatementConfig) => Statement): Promise<number> {
|
|
95
|
+
generateStatement({
|
|
96
|
+
useParams: true,
|
|
97
|
+
useNamedParams: true,
|
|
98
|
+
prefixTablesWithDb: false,
|
|
99
|
+
getDriverColumnType: () => 'STRING(MAX)',
|
|
100
|
+
});
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async runTransaction<T>(fn: (transaction: unknown) => Promise<T>): Promise<T> {
|
|
105
|
+
return await fn({ id: 'transaction' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getOperationDeadlineMs(): number {
|
|
109
|
+
return 1000;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type SourceRepositoryInternals = { objectCache: { [qualifiedName: string]: unknown[] } };
|
|
114
|
+
type TableWatcherRunnerStatics = { tableWatcherMap?: unknown };
|
|
115
|
+
const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
|
|
116
|
+
const DEFAULT_DRIVER_GLOBAL = '__proteinjs_db_defaultDbDriver';
|
|
117
|
+
|
|
118
|
+
const driver = new AcceptingDriver();
|
|
119
|
+
const transactions = new TestTransactionContextFactory();
|
|
120
|
+
const modesHandedBy = async (db: Db<Doc>): Promise<(boolean | undefined)[]> => {
|
|
121
|
+
handed.length = 0;
|
|
122
|
+
await db.insert(docTable, { title: 'row' });
|
|
123
|
+
expect(handed.map(({ phase }) => phase)).toEqual(['beforeInsert', 'afterInsert']);
|
|
124
|
+
return handed.map(({ mode }) => mode);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
describe('Db.isSystem', () => {
|
|
128
|
+
let originalWatchers: unknown[] | undefined;
|
|
129
|
+
let originalFactories: unknown[] | undefined;
|
|
130
|
+
let originalTransactionFactories: unknown[] | undefined;
|
|
131
|
+
|
|
132
|
+
beforeAll(() => {
|
|
133
|
+
originalWatchers = objectCache()['@proteinjs/db/TableWatcher'];
|
|
134
|
+
originalFactories = objectCache()['@proteinjs/db/DefaultDbDriverFactory'];
|
|
135
|
+
originalTransactionFactories = objectCache()['@proteinjs/db/DefaultTransactionContextFactory'];
|
|
136
|
+
objectCache()['@proteinjs/db/TableWatcher'] = [new ModeRecordingWatcher()];
|
|
137
|
+
// The public factories resolve their driver and transaction context through reflection.
|
|
138
|
+
objectCache()['@proteinjs/db/DefaultDbDriverFactory'] = [{ getDbDriver: () => driver }];
|
|
139
|
+
objectCache()['@proteinjs/db/DefaultTransactionContextFactory'] = [transactions];
|
|
140
|
+
delete (globalThis as { [key: string]: unknown })[DEFAULT_DRIVER_GLOBAL];
|
|
141
|
+
(TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterAll(() => {
|
|
145
|
+
const restore = (name: string, original: unknown[] | undefined) => {
|
|
146
|
+
if (original) {
|
|
147
|
+
objectCache()[name] = original;
|
|
148
|
+
} else {
|
|
149
|
+
delete objectCache()[name];
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
restore('@proteinjs/db/TableWatcher', originalWatchers);
|
|
153
|
+
restore('@proteinjs/db/DefaultDbDriverFactory', originalFactories);
|
|
154
|
+
restore('@proteinjs/db/DefaultTransactionContextFactory', originalTransactionFactories);
|
|
155
|
+
delete (globalThis as { [key: string]: unknown })[DEFAULT_DRIVER_GLOBAL];
|
|
156
|
+
(TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('a system Db answers true, a session Db false — on the public factories and on the constructor', () => {
|
|
160
|
+
expect(getDbAsSystem().isSystem()).toBe(true);
|
|
161
|
+
expect(getDb().isSystem()).toBe(false);
|
|
162
|
+
expect(new Db<Doc>(driver, () => docTable, transactions, true).isSystem()).toBe(true);
|
|
163
|
+
expect(new Db<Doc>(driver, () => docTable, transactions).isSystem()).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("the Db a write hands its watchers answers for THAT write: a system write's hooks read true", async () => {
|
|
167
|
+
expect(await modesHandedBy(getDbAsSystem<Doc>())).toEqual([true, true]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("the Db a write hands its watchers answers for THAT write: a session write's hooks read false", async () => {
|
|
171
|
+
expect(await modesHandedBy(getDb<Doc>())).toEqual([false, false]);
|
|
172
|
+
});
|
|
173
|
+
});
|
package/test/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './reusable/PreloadReferencesTests';
|
|
|
8
8
|
export * from './reusable/RecordIteratorTests';
|
|
9
9
|
export * from './reusable/SourceRecordSyncTests';
|
|
10
10
|
export * from './reusable/StoredValueContractTests';
|
|
11
|
+
export * from './reusable/TableWatcherAuthorityTests';
|
|
11
12
|
|
|
12
13
|
export * from './util/testUser';
|
|
13
14
|
|
|
@@ -19,4 +20,5 @@ export * from './util/tables/recordIteratorTestTables';
|
|
|
19
20
|
export * from './util/tables/sourceRecordSyncTestTables';
|
|
20
21
|
export * from './util/tables/storedValueContractTestTables';
|
|
21
22
|
export * from './util/tables/tableManagerTestTables';
|
|
23
|
+
export * from './util/tables/tableWatcherAuthorityTestTables';
|
|
22
24
|
export * from './util/tables/transactionTestTables';
|