@proteinjs/db 1.55.0 → 1.56.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 +13 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/Columns.d.ts +20 -2
- package/dist/src/Columns.d.ts.map +1 -1
- package/dist/src/Columns.js +46 -45
- package/dist/src/Columns.js.map +1 -1
- package/dist/src/Db.d.ts +17 -0
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +28 -9
- 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/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/CascadeDeleteTests.d.ts.map +1 -1
- package/dist/test/reusable/CascadeDeleteTests.js +132 -0
- package/dist/test/reusable/CascadeDeleteTests.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/cascadeDeleteTestTables.d.ts +38 -0
- package/dist/test/util/tables/cascadeDeleteTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/cascadeDeleteTestTables.js +119 -1
- package/dist/test/util/tables/cascadeDeleteTestTables.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 +13 -1
- package/package.json +2 -2
- package/src/Columns.ts +50 -27
- package/src/Db.ts +42 -15
- package/src/TableWatcher.ts +17 -6
- package/src/TableWatcherRunner.ts +25 -12
- package/test/index.ts +2 -0
- package/test/reusable/CascadeDeleteTests.ts +90 -0
- package/test/reusable/TableWatcherAuthorityTests.ts +204 -0
- package/test/util/DbTestEnvironment.ts +2 -0
- package/test/util/tables/cascadeDeleteTestTables.ts +85 -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.56.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": "469b64193ef5d4fc0faa74ebe1f6c2af04dfd6cb"
|
|
70
70
|
}
|
package/src/Columns.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import moment from 'moment';
|
|
2
2
|
import { v1 as uuidv1 } from 'uuid';
|
|
3
|
-
import { Column, ColumnOptions, getColumnPropertyName, Table
|
|
3
|
+
import { Column, ColumnOptions, getColumnPropertyName, Table } from './Table';
|
|
4
4
|
import { Record } from './Record';
|
|
5
5
|
import { ReferenceArray } from './reference/ReferenceArray';
|
|
6
6
|
import { Db } from './Db';
|
|
@@ -263,17 +263,29 @@ export class ReferenceArrayColumn<T extends Record> extends ObjectColumn<Referen
|
|
|
263
263
|
return new ReferenceArray(this.referenceTable, ids);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Cascade: the referenced records go with the holder. Runs through the Db the firing write
|
|
268
|
+
* hands it — its driver, its transaction context, its authority — and off the ids the column
|
|
269
|
+
* stores; nothing here opens a Db of its own or re-reads the referenced rows, either of which
|
|
270
|
+
* would run as the session's user rather than as the write (a system delete's cascade must stay
|
|
271
|
+
* system: in a background context there is no session user at all).
|
|
272
|
+
*/
|
|
273
|
+
async beforeDelete(
|
|
274
|
+
table: Table<any>,
|
|
275
|
+
columnPropertyName: string,
|
|
276
|
+
records: any[],
|
|
277
|
+
getTable?: (tableName: string) => Table<any>,
|
|
278
|
+
db?: Db
|
|
279
|
+
): Promise<void> {
|
|
267
280
|
if (!this.cascadeDelete) {
|
|
268
281
|
return;
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
const recordIdsToDelete: string[] = [];
|
|
272
285
|
for (const record of records) {
|
|
273
|
-
const referenceArray = record[columnPropertyName] as ReferenceArray<Record
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
recordIdsToDelete.push(referenceRecord.id);
|
|
286
|
+
const referenceArray = record[columnPropertyName] as ReferenceArray<Record> | null | undefined;
|
|
287
|
+
if (referenceArray) {
|
|
288
|
+
recordIdsToDelete.push(...referenceArray._ids);
|
|
277
289
|
}
|
|
278
290
|
}
|
|
279
291
|
|
|
@@ -281,11 +293,12 @@ export class ReferenceArrayColumn<T extends Record> extends ObjectColumn<Referen
|
|
|
281
293
|
return;
|
|
282
294
|
}
|
|
283
295
|
|
|
284
|
-
|
|
296
|
+
// Optional in the hook contract for implementers that need neither; the Db hands both to every hook.
|
|
297
|
+
const referenceTable = getTable!(this.referenceTable);
|
|
285
298
|
const qb = new QueryBuilderFactory()
|
|
286
299
|
.getQueryBuilder(referenceTable)
|
|
287
300
|
.condition({ field: 'id', operator: 'IN', value: recordIdsToDelete });
|
|
288
|
-
await
|
|
301
|
+
await db!.delete(referenceTable, qb);
|
|
289
302
|
}
|
|
290
303
|
}
|
|
291
304
|
|
|
@@ -331,7 +344,18 @@ export class ReferenceColumn<T extends Record> extends StringColumn<Reference<T>
|
|
|
331
344
|
return reference as Reference<T>;
|
|
332
345
|
}
|
|
333
346
|
|
|
334
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Cascade: the referenced record goes with the holder. Runs through the Db the firing write
|
|
349
|
+
* hands it (its driver, transaction context and authority) off the stored id — see
|
|
350
|
+
* `ReferenceArrayColumn.beforeDelete` for why the hook never opens a Db of its own.
|
|
351
|
+
*/
|
|
352
|
+
async beforeDelete(
|
|
353
|
+
table: Table<any>,
|
|
354
|
+
columnPropertyName: string,
|
|
355
|
+
records: any[],
|
|
356
|
+
getTable?: (tableName: string) => Table<any>,
|
|
357
|
+
db?: Db
|
|
358
|
+
): Promise<void> {
|
|
335
359
|
if (!this.cascadeDelete) {
|
|
336
360
|
return;
|
|
337
361
|
}
|
|
@@ -348,11 +372,12 @@ export class ReferenceColumn<T extends Record> extends StringColumn<Reference<T>
|
|
|
348
372
|
return;
|
|
349
373
|
}
|
|
350
374
|
|
|
351
|
-
|
|
375
|
+
// Optional in the hook contract for implementers that need neither; the Db hands both to every hook.
|
|
376
|
+
const referenceTable = getTable!(this.referenceTable);
|
|
352
377
|
const qb = new QueryBuilderFactory()
|
|
353
378
|
.getQueryBuilder(referenceTable)
|
|
354
379
|
.condition({ field: 'id', operator: 'IN', value: recordIdsToDelete });
|
|
355
|
-
await
|
|
380
|
+
await db!.delete(referenceTable, qb);
|
|
356
381
|
}
|
|
357
382
|
}
|
|
358
383
|
|
|
@@ -508,6 +533,12 @@ export class DynamicReferenceColumn<T extends Record> extends StringColumn<Refer
|
|
|
508
533
|
return new Reference<T>(tableName, serializedFieldValue);
|
|
509
534
|
}
|
|
510
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Cascade: the referenced record goes with the holder, whichever table the row names. Runs
|
|
538
|
+
* through the Db the firing write hands it (its driver, transaction context and authority) off
|
|
539
|
+
* the stored ids — see `ReferenceArrayColumn.beforeDelete` for why the hook never opens a Db of
|
|
540
|
+
* its own.
|
|
541
|
+
*/
|
|
511
542
|
async beforeDelete(
|
|
512
543
|
table: Table<any>,
|
|
513
544
|
columnPropertyName: string,
|
|
@@ -519,12 +550,8 @@ export class DynamicReferenceColumn<T extends Record> extends StringColumn<Refer
|
|
|
519
550
|
return;
|
|
520
551
|
}
|
|
521
552
|
|
|
522
|
-
|
|
523
|
-
const dbInstance = db ? db : new Db();
|
|
524
|
-
|
|
525
|
-
// Get all referenced record IDs grouped by table
|
|
553
|
+
// The referenced ids, grouped by the table each row names
|
|
526
554
|
const recordsToDelete = new Map<string, string[]>();
|
|
527
|
-
|
|
528
555
|
for (const record of records) {
|
|
529
556
|
const reference = record[columnPropertyName] as Reference<Record>;
|
|
530
557
|
if (!reference?._id || !reference._table) {
|
|
@@ -537,17 +564,13 @@ export class DynamicReferenceColumn<T extends Record> extends StringColumn<Refer
|
|
|
537
564
|
recordsToDelete.get(reference._table)!.push(reference._id);
|
|
538
565
|
}
|
|
539
566
|
|
|
540
|
-
//
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
.getQueryBuilder(referenceTable)
|
|
548
|
-
.condition({ field: 'id', operator: 'IN', value: ids });
|
|
549
|
-
await dbInstance.delete(referenceTable, qb);
|
|
550
|
-
}
|
|
567
|
+
// Optional in the hook contract for implementers that need neither; the Db hands both to every hook.
|
|
568
|
+
for (const [tableName, ids] of Array.from(recordsToDelete.entries())) {
|
|
569
|
+
const referenceTable = getTable!(tableName);
|
|
570
|
+
const qb = new QueryBuilderFactory()
|
|
571
|
+
.getQueryBuilder(referenceTable)
|
|
572
|
+
.condition({ field: 'id', operator: 'IN', value: ids });
|
|
573
|
+
await db!.delete(referenceTable, qb);
|
|
551
574
|
}
|
|
552
575
|
}
|
|
553
576
|
}
|
package/src/Db.ts
CHANGED
|
@@ -241,7 +241,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
241
241
|
|
|
242
242
|
let recordCopy = Object.assign({}, record);
|
|
243
243
|
await addDefaultFieldValues(table, recordCopy, this.runAsSystem);
|
|
244
|
-
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy);
|
|
244
|
+
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy, this.newHookDb());
|
|
245
245
|
await this.addColumnInsertHooks(table, recordCopy);
|
|
246
246
|
const encryptionContext = await this.encryptionWriteContext(table, recordCopy);
|
|
247
247
|
const recordSerializer = new RecordSerializer(table, encryptionContext);
|
|
@@ -269,7 +269,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
269
269
|
);
|
|
270
270
|
}
|
|
271
271
|
await this.runColumnAfterInsertHooks(table, recordCopy);
|
|
272
|
-
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
272
|
+
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T, this.newHookDb());
|
|
273
273
|
return recordCopy as T;
|
|
274
274
|
}
|
|
275
275
|
|
|
@@ -311,7 +311,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
311
311
|
for (const record of records) {
|
|
312
312
|
let recordCopy = Object.assign({}, record);
|
|
313
313
|
await addDefaultFieldValues(table, recordCopy, this.runAsSystem);
|
|
314
|
-
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy);
|
|
314
|
+
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy, this.newHookDb());
|
|
315
315
|
await this.addColumnInsertHooks(table, recordCopy);
|
|
316
316
|
const encryptionContext = await this.encryptionWriteContext(table, recordCopy);
|
|
317
317
|
const serializedRecord = await new RecordSerializer(table, encryptionContext).serialize(recordCopy);
|
|
@@ -343,7 +343,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
343
343
|
);
|
|
344
344
|
}
|
|
345
345
|
await this.runColumnAfterInsertHooks(table, recordCopy);
|
|
346
|
-
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
346
|
+
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T, this.newHookDb());
|
|
347
347
|
}
|
|
348
348
|
return prepared.map((row) => row.recordCopy as T);
|
|
349
349
|
}
|
|
@@ -389,7 +389,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
389
389
|
qb.condition({ field: 'id', operator: '=', value: recordCopy.id as T[keyof T] });
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
recordCopy = await this.tableWatcherRunner.runBeforeUpdateTableWatchers(table, recordCopy, qb);
|
|
392
|
+
recordCopy = await this.tableWatcherRunner.runBeforeUpdateTableWatchers(table, recordCopy, qb, this.newHookDb());
|
|
393
393
|
let recordUpdateCount: number;
|
|
394
394
|
if (await this.updateTouchesEncryptedColumns(table, recordCopy)) {
|
|
395
395
|
recordUpdateCount = await this.updateEncrypted(table, recordCopy, qb, driverOptions);
|
|
@@ -412,7 +412,13 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
412
412
|
await this.runZeroRowFilteredWriteHooks(table, id, 'write');
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
-
await this.tableWatcherRunner.runAfterUpdateTableWatchers(
|
|
415
|
+
await this.tableWatcherRunner.runAfterUpdateTableWatchers(
|
|
416
|
+
table,
|
|
417
|
+
recordUpdateCount,
|
|
418
|
+
recordCopy,
|
|
419
|
+
qb,
|
|
420
|
+
this.newHookDb()
|
|
421
|
+
);
|
|
416
422
|
return recordUpdateCount;
|
|
417
423
|
}
|
|
418
424
|
|
|
@@ -542,26 +548,31 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
542
548
|
const generateDelete = (config: DbDriverDmlStatementConfig) =>
|
|
543
549
|
new StatementFactory<T>().delete(table.name, deleteQb, this.statementConfigFactory.getStatementConfig(config));
|
|
544
550
|
await this.runColumnBeforeDeletes(table, recordsToDelete);
|
|
545
|
-
await this.tableWatcherRunner.runBeforeDeleteTableWatchers(table, recordsToDelete, qb, deleteQb);
|
|
551
|
+
await this.tableWatcherRunner.runBeforeDeleteTableWatchers(table, recordsToDelete, qb, deleteQb, this.newHookDb());
|
|
546
552
|
const recordDeleteCount = await this.dbDriver.runDml(generateDelete, this.transactionForDriver());
|
|
547
553
|
await this.runCascadeDeletions(table, recordsToDelete);
|
|
548
554
|
await this.runColumnReverseCascadeDeletions(table, recordsToDelete);
|
|
549
555
|
await this.deleteEncryptionTokenRows(table, recordsToDeleteIds as string[]);
|
|
550
|
-
await this.tableWatcherRunner.runAfterDeleteTableWatchers(
|
|
556
|
+
await this.tableWatcherRunner.runAfterDeleteTableWatchers(
|
|
557
|
+
table,
|
|
558
|
+
recordDeleteCount,
|
|
559
|
+
recordsToDelete,
|
|
560
|
+
qb,
|
|
561
|
+
deleteQb,
|
|
562
|
+
this.newHookDb()
|
|
563
|
+
);
|
|
551
564
|
return recordDeleteCount;
|
|
552
565
|
}
|
|
553
566
|
|
|
567
|
+
/**
|
|
568
|
+
* A column's `beforeDelete` hook cascades the delete through reference columns, with the
|
|
569
|
+
* authority of the write that fired it ({@link newHookDb}).
|
|
570
|
+
*/
|
|
554
571
|
private async runColumnBeforeDeletes(table: Table<any>, recordsToDelete: Record[]) {
|
|
555
572
|
for (const columnPropertyName in table.columns) {
|
|
556
573
|
const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
|
|
557
574
|
if (typeof column.beforeDelete !== 'undefined') {
|
|
558
|
-
await column.beforeDelete(
|
|
559
|
-
table,
|
|
560
|
-
columnPropertyName,
|
|
561
|
-
recordsToDelete,
|
|
562
|
-
this.getTable,
|
|
563
|
-
new Db(this.dbDriver, this.getTable, this.transactionContextFactory)
|
|
564
|
-
);
|
|
575
|
+
await column.beforeDelete(table, columnPropertyName, recordsToDelete, this.getTable, this.newHookDb());
|
|
565
576
|
}
|
|
566
577
|
}
|
|
567
578
|
}
|
|
@@ -1163,6 +1174,22 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
1163
1174
|
return new Db<any>(this.dbDriver, this.getTable, this.transactionContextFactory, true);
|
|
1164
1175
|
}
|
|
1165
1176
|
|
|
1177
|
+
/**
|
|
1178
|
+
* The Db a hook is handed — a column's `beforeDelete`, a table watcher's six hooks: an instance
|
|
1179
|
+
* riding THIS instance's driver, tables and transaction context AND its system mode, so the hook
|
|
1180
|
+
* runs with the authority of the write that fired it. A hook that opened a Db of its own
|
|
1181
|
+
* (`getDb()`, a session-scoped Db) ran as whoever the session held: a system write's follow-on
|
|
1182
|
+
* narrowed to that user's rows inside a request, and refused outright in a background context
|
|
1183
|
+
* (an account purge, a sweep, a deploy gate), where a scoped table finds no user to scope the
|
|
1184
|
+
* query to — the cascade left orphans, the watcher left its rows (a message's topic links, a
|
|
1185
|
+
* media row's file) and either swallowed or raised the "No user in scope context" error.
|
|
1186
|
+
* Transaction state is instance state ({@link newSelfWrapDb}), so the hook rides the ambient
|
|
1187
|
+
* transaction through the shared context and never this instance's own.
|
|
1188
|
+
*/
|
|
1189
|
+
private newHookDb(): Db<any> {
|
|
1190
|
+
return new Db<any>(this.dbDriver, this.getTable, this.transactionContextFactory, this.runAsSystem);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1166
1193
|
/**
|
|
1167
1194
|
* A fresh instance for self-wrapping RMW verbs (`updateArrayMembership`, `updatePreserving`)
|
|
1168
1195
|
* 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` });
|
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';
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
GroupArr,
|
|
7
7
|
GroupDyn,
|
|
8
8
|
GroupRef,
|
|
9
|
+
GuardedHolderArr,
|
|
10
|
+
GuardedHolderDyn,
|
|
11
|
+
GuardedHolderRef,
|
|
12
|
+
GuardedMember,
|
|
9
13
|
MemberArr,
|
|
10
14
|
MemberArrRev,
|
|
11
15
|
MemberDyn,
|
|
@@ -224,5 +228,91 @@ export const cascadeDeleteTests = (
|
|
|
224
228
|
expect(missionsAfterPilot.length).toBe(0);
|
|
225
229
|
});
|
|
226
230
|
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* -------------------- Cascade Delete as system (holder → guarded referenced) --------------------
|
|
234
|
+
* A system delete's cascade runs with the authority of the write that fired it. The children's
|
|
235
|
+
* table refuses every non-system query (a scoped table's shape), and the delete runs in a fresh
|
|
236
|
+
* async context with no session — a background sweep's shape. Before the fix the cascade hooks
|
|
237
|
+
* ran as a non-system Db, and every case below failed with the guarded table's refusal.
|
|
238
|
+
*/
|
|
239
|
+
describe('Cascade Delete as system', () => {
|
|
240
|
+
const systemDb = new Db(driver, undefined, transactionContextFactory, true);
|
|
241
|
+
const memberTable = cascadeDeleteTestTables.GuardedMember;
|
|
242
|
+
|
|
243
|
+
/** A fresh async context with nothing inherited from the test's own — a background sweep's shape. */
|
|
244
|
+
const inFreshContext = <T>(fn: () => Promise<T>): Promise<T> =>
|
|
245
|
+
new Promise<T>((resolve, reject) => {
|
|
246
|
+
setImmediate(() => {
|
|
247
|
+
fn().then(resolve, reject);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('ReferenceColumn: a system delete of the holder deletes the guarded referenced record', async () => {
|
|
252
|
+
const holderTable = cascadeDeleteTestTables.GuardedHolderRef;
|
|
253
|
+
|
|
254
|
+
const m = await systemDb.insert(memberTable, { name: 'Gail' });
|
|
255
|
+
const h = await systemDb.insert(holderTable, {
|
|
256
|
+
holderName: 'H-Ref',
|
|
257
|
+
memberRef: new Reference<GuardedMember>(memberTable.name, m.id),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const delQb = new QueryBuilder<GuardedHolderRef>(holderTable.name).condition({
|
|
261
|
+
field: 'id',
|
|
262
|
+
operator: '=',
|
|
263
|
+
value: h.id,
|
|
264
|
+
});
|
|
265
|
+
const deleted = await inFreshContext(() => systemDb.delete(holderTable, delQb));
|
|
266
|
+
expect(deleted).toBe(1);
|
|
267
|
+
|
|
268
|
+
expect(await systemDb.query(memberTable, { id: m.id })).toHaveLength(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('ReferenceArrayColumn: a system delete of the holder deletes every guarded referenced record', async () => {
|
|
272
|
+
const holderTable = cascadeDeleteTestTables.GuardedHolderArr;
|
|
273
|
+
|
|
274
|
+
const m1 = await systemDb.insert(memberTable, { name: 'Gus' });
|
|
275
|
+
const m2 = await systemDb.insert(memberTable, { name: 'Greta' });
|
|
276
|
+
const m3 = await systemDb.insert(memberTable, { name: 'Glen' });
|
|
277
|
+
const h = await systemDb.insert(holderTable, {
|
|
278
|
+
holderName: 'H-Arr',
|
|
279
|
+
memberRefs: new ReferenceArray<GuardedMember>(memberTable.name, [m1.id, m2.id]),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const delQb = new QueryBuilder<GuardedHolderArr>(holderTable.name).condition({
|
|
283
|
+
field: 'id',
|
|
284
|
+
operator: '=',
|
|
285
|
+
value: h.id,
|
|
286
|
+
});
|
|
287
|
+
const deleted = await inFreshContext(() => systemDb.delete(holderTable, delQb));
|
|
288
|
+
expect(deleted).toBe(1);
|
|
289
|
+
|
|
290
|
+
expect(await systemDb.query(memberTable, { id: m1.id })).toHaveLength(0);
|
|
291
|
+
expect(await systemDb.query(memberTable, { id: m2.id })).toHaveLength(0);
|
|
292
|
+
// The unreferenced member stands: the cascade is the stored ids, nothing wider.
|
|
293
|
+
expect(await systemDb.query(memberTable, { id: m3.id })).toHaveLength(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('DynamicReferenceColumn: a system delete of the holder deletes the guarded dynamically referenced record', async () => {
|
|
297
|
+
const holderTable = cascadeDeleteTestTables.GuardedHolderDyn;
|
|
298
|
+
|
|
299
|
+
const m = await systemDb.insert(memberTable, { name: 'Gwen' });
|
|
300
|
+
const h = await systemDb.insert(holderTable, {
|
|
301
|
+
holderName: 'H-Dyn',
|
|
302
|
+
memberDynTableName: memberTable.name,
|
|
303
|
+
memberDynRef: new Reference<GuardedMember>(memberTable.name, m.id),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const delQb = new QueryBuilder<GuardedHolderDyn>(holderTable.name).condition({
|
|
307
|
+
field: 'id',
|
|
308
|
+
operator: '=',
|
|
309
|
+
value: h.id,
|
|
310
|
+
});
|
|
311
|
+
const deleted = await inFreshContext(() => systemDb.delete(holderTable, delQb));
|
|
312
|
+
expect(deleted).toBe(1);
|
|
313
|
+
|
|
314
|
+
expect(await systemDb.query(memberTable, { id: m.id })).toHaveLength(0);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
227
317
|
};
|
|
228
318
|
};
|