@proteinjs/db 1.54.3 → 1.55.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/.eslintrc.js +15 -1
- package/CHANGELOG.md +22 -0
- package/dist/generated/test/index.d.ts.map +1 -1
- package/dist/generated/test/index.js +15 -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 +8 -0
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +9 -1
- package/dist/src/Db.js.map +1 -1
- package/dist/src/Record.d.ts +15 -0
- package/dist/src/Record.d.ts.map +1 -1
- package/dist/src/Record.js +118 -67
- package/dist/src/Record.js.map +1 -1
- package/dist/src/auth/TableServiceAuth.js.map +1 -1
- package/dist/src/schema/TableManager.d.ts +8 -0
- package/dist/src/schema/TableManager.d.ts.map +1 -1
- package/dist/src/schema/TableManager.js +22 -0
- package/dist/src/schema/TableManager.js.map +1 -1
- package/dist/src/source/SourceRecordSyncRunner.js +1 -1
- package/dist/src/source/SourceRecordSyncRunner.js.map +1 -1
- package/dist/test/SettleThenFail.test.d.ts +2 -0
- package/dist/test/SettleThenFail.test.d.ts.map +1 -0
- package/dist/test/SettleThenFail.test.js +237 -0
- package/dist/test/SettleThenFail.test.js.map +1 -0
- package/dist/test/StoredValueSeating.test.d.ts +2 -0
- package/dist/test/StoredValueSeating.test.d.ts.map +1 -0
- package/dist/test/StoredValueSeating.test.js +243 -0
- package/dist/test/StoredValueSeating.test.js.map +1 -0
- package/dist/test/SyncRowHydration.test.js.map +1 -1
- package/dist/test/UndeclaredColumnCensus.test.d.ts +2 -0
- package/dist/test/UndeclaredColumnCensus.test.d.ts.map +1 -0
- package/dist/test/UndeclaredColumnCensus.test.js +139 -0
- package/dist/test/UndeclaredColumnCensus.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/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/StoredValueContractTests.d.ts +3 -0
- package/dist/test/reusable/StoredValueContractTests.d.ts.map +1 -0
- package/dist/test/reusable/StoredValueContractTests.js +666 -0
- package/dist/test/reusable/StoredValueContractTests.js.map +1 -0
- package/dist/test/reusable/TransactionTests.js +8 -8
- package/dist/test/util/DbTestEnvironment.d.ts.map +1 -1
- package/dist/test/util/DbTestEnvironment.js +5 -1
- 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/storedValueContractTestTables.d.ts +66 -0
- package/dist/test/util/tables/storedValueContractTestTables.d.ts.map +1 -0
- package/dist/test/util/tables/storedValueContractTestTables.js +85 -0
- package/dist/test/util/tables/storedValueContractTestTables.js.map +1 -0
- package/generated/test/index.ts +15 -1
- package/package.json +3 -3
- package/src/Columns.ts +50 -27
- package/src/Db.ts +9 -1
- package/src/Record.ts +125 -66
- package/src/auth/TableServiceAuth.ts +1 -1
- package/src/schema/TableManager.ts +25 -0
- package/src/source/SourceRecordSyncRunner.ts +1 -1
- package/test/SettleThenFail.test.ts +167 -0
- package/test/StoredValueSeating.test.ts +169 -0
- package/test/SyncRowHydration.test.ts +1 -1
- package/test/UndeclaredColumnCensus.test.ts +87 -0
- package/test/index.ts +2 -0
- package/test/reusable/CascadeDeleteTests.ts +91 -15
- package/test/reusable/StoredValueContractTests.ts +419 -0
- package/test/reusable/TransactionTests.ts +1 -1
- package/test/util/DbTestEnvironment.ts +3 -0
- package/test/util/tables/cascadeDeleteTestTables.ts +85 -0
- package/test/util/tables/storedValueContractTestTables.ts +62 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Column, Table } from '../src/Table';
|
|
2
|
+
import {
|
|
3
|
+
DuplicateColumnNameError,
|
|
4
|
+
FieldSerializer,
|
|
5
|
+
MissingFieldError,
|
|
6
|
+
Record,
|
|
7
|
+
RecordSerializer,
|
|
8
|
+
SerializedRecord,
|
|
9
|
+
withRecordColumns,
|
|
10
|
+
} from '../src/Record';
|
|
11
|
+
import { ArrayColumn, StringColumn } from '../src/Columns';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where a stored cell is seated: by the declared COLUMN name alone. A stored key that is only a
|
|
15
|
+
* PROPERTY's name — the text column `tags` a cutover retired beside the array column `tag_list`
|
|
16
|
+
* that replaced it, property `tags` both times — names no seat: it never reaches the array
|
|
17
|
+
* column's parser, never displaces the array, whichever side of the real cell it lands on, and
|
|
18
|
+
* whatever it holds (a bare word, an empty string, NULL). The row reader used to seat by property
|
|
19
|
+
* name as a second pass, which is the only way an undeclared cell could reach a declared column's
|
|
20
|
+
* parser — and, iterated after the real cell, a NULL in it silently replaced the array.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface Member extends Record {
|
|
24
|
+
name: string;
|
|
25
|
+
tags: string[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class MemberTable extends Table<Member> {
|
|
29
|
+
public name = 'seating_member';
|
|
30
|
+
public columns = withRecordColumns<Member>({
|
|
31
|
+
name: new StringColumn('name'),
|
|
32
|
+
tags: new ArrayColumn<string>('tag_list'),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const memberTable = new MemberTable() as Table<Member>;
|
|
37
|
+
|
|
38
|
+
type SerializerInternals = { logger: { warn: (...args: unknown[]) => void } };
|
|
39
|
+
type SerializerStatics = { legacyCellsWarned: Set<string> };
|
|
40
|
+
|
|
41
|
+
const LEGACY_WORD = 'legacyword7';
|
|
42
|
+
|
|
43
|
+
/** Every warning a serializer logs, as the logger received it. */
|
|
44
|
+
const warningsOf = (serializer: RecordSerializer<any>): unknown[][] => {
|
|
45
|
+
const calls: unknown[][] = [];
|
|
46
|
+
(serializer as unknown as SerializerInternals).logger.warn = (...args: unknown[]) => {
|
|
47
|
+
calls.push(args);
|
|
48
|
+
};
|
|
49
|
+
return calls;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** A stored row in the given key order — the order a driver's row carries its cells in. */
|
|
53
|
+
const rowOf = (cells: { [columnName: string]: unknown }): SerializedRecord => ({
|
|
54
|
+
id: 'm1',
|
|
55
|
+
created: new Date(1_700_000_000_000),
|
|
56
|
+
updated: new Date(1_700_000_000_000),
|
|
57
|
+
...cells,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
(RecordSerializer as unknown as SerializerStatics).legacyCellsWarned.clear();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('a stored key that is a property name, not a column name', () => {
|
|
65
|
+
test.each([
|
|
66
|
+
['a bare word', LEGACY_WORD],
|
|
67
|
+
['an empty string', ''],
|
|
68
|
+
['NULL', null],
|
|
69
|
+
])('iterated AFTER the declared cell, a legacy cell holding %s leaves the array in place', async (_, legacy) => {
|
|
70
|
+
const serializer = new RecordSerializer<Member>(memberTable);
|
|
71
|
+
const warnings = warningsOf(serializer);
|
|
72
|
+
|
|
73
|
+
const [record] = await serializer.deserializeAll([rowOf({ name: 'm', tag_list: '["a","b"]', tags: legacy })]);
|
|
74
|
+
|
|
75
|
+
expect(record.tags).toEqual(['a', 'b']);
|
|
76
|
+
expect(record).toMatchObject({ id: 'm1', name: 'm' });
|
|
77
|
+
// The legacy cell never reached the array column's parser: no cell-omitted warning.
|
|
78
|
+
expect(warnings).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test.each([
|
|
82
|
+
['a bare word', LEGACY_WORD],
|
|
83
|
+
['an empty string', ''],
|
|
84
|
+
['NULL', null],
|
|
85
|
+
])('iterated BEFORE the declared cell, a legacy cell holding %s leaves the array in place', async (_, legacy) => {
|
|
86
|
+
const serializer = new RecordSerializer<Member>(memberTable);
|
|
87
|
+
const warnings = warningsOf(serializer);
|
|
88
|
+
|
|
89
|
+
const [record] = await serializer.deserializeAll([rowOf({ name: 'm', tags: legacy, tag_list: '["a","b"]' })]);
|
|
90
|
+
|
|
91
|
+
expect(record.tags).toEqual(['a', 'b']);
|
|
92
|
+
expect(warnings).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a stored key equal to a column name is that column; the property name alone is nobody', async () => {
|
|
96
|
+
const fieldSerializer = new FieldSerializer<Member>(memberTable);
|
|
97
|
+
|
|
98
|
+
const seated = await fieldSerializer.deserialize('tag_list', '["a"]', {});
|
|
99
|
+
expect(seated).toEqual({ fieldPropertyName: 'tags', fieldValue: ['a'] });
|
|
100
|
+
|
|
101
|
+
await expect(fieldSerializer.deserialize('tags', LEGACY_WORD, {})).rejects.toBeInstanceOf(MissingFieldError);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('two declared columns sharing one stored name', () => {
|
|
106
|
+
test('is refused at the first hydration, naming the table and both properties', () => {
|
|
107
|
+
interface Twice extends Record {
|
|
108
|
+
first: string;
|
|
109
|
+
second: string;
|
|
110
|
+
}
|
|
111
|
+
class TwiceTable extends Table<Twice> {
|
|
112
|
+
public name = 'seating_twice';
|
|
113
|
+
public columns = withRecordColumns<Twice>({
|
|
114
|
+
first: new StringColumn('shared'),
|
|
115
|
+
second: new StringColumn('shared'),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const serializer = new RecordSerializer<Twice>(new TwiceTable() as Table<Twice>);
|
|
119
|
+
|
|
120
|
+
expect(() => serializer.deserializeAll([rowOf({ shared: 'x' })])).toThrow(DuplicateColumnNameError);
|
|
121
|
+
expect(() => serializer.deserializeAll([rowOf({ shared: 'x' })])).toThrow(
|
|
122
|
+
`(seating_twice) Columns first and second both declare the column name 'shared'`
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('the write side', () => {
|
|
128
|
+
class RefusingColumn implements Column<string, string> {
|
|
129
|
+
constructor(public name: string) {}
|
|
130
|
+
|
|
131
|
+
async serialize(): Promise<string> {
|
|
132
|
+
throw new RangeError('cannot be stored');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
interface Note extends Record {
|
|
136
|
+
title: string;
|
|
137
|
+
body: string;
|
|
138
|
+
}
|
|
139
|
+
class NoteTable extends Table<Note> {
|
|
140
|
+
public name = 'seating_note';
|
|
141
|
+
public columns = withRecordColumns<Note>({
|
|
142
|
+
title: new StringColumn('title'),
|
|
143
|
+
body: new RefusingColumn('body'),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const noteTable = new NoteTable() as Table<Note>;
|
|
147
|
+
|
|
148
|
+
test('a column that cannot serialize its value fails the write with that error', async () => {
|
|
149
|
+
const serializer = new RecordSerializer<Note>(noteTable);
|
|
150
|
+
const warnings = warningsOf(serializer);
|
|
151
|
+
|
|
152
|
+
await expect(serializer.serialize({ id: 'n1', title: 't', body: 'b' })).rejects.toThrow(
|
|
153
|
+
new RangeError('cannot be stored')
|
|
154
|
+
);
|
|
155
|
+
expect(warnings).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('a field no column owns is omitted from the write, and warned', async () => {
|
|
159
|
+
const serializer = new RecordSerializer<Member>(memberTable);
|
|
160
|
+
const warnings = warningsOf(serializer);
|
|
161
|
+
|
|
162
|
+
const serialized = await serializer.serialize({ id: 'm1', name: 'm', tags: ['a'], retired: 'gone' });
|
|
163
|
+
|
|
164
|
+
expect(Object.keys(serialized)).toEqual(['id', 'name', 'tag_list']);
|
|
165
|
+
expect(JSON.parse(serialized.tag_list)).toEqual(['a']);
|
|
166
|
+
expect(warnings).toHaveLength(1);
|
|
167
|
+
expect(warnings[0][0]).toMatchObject({ obj: { omittedFields: ['retired'] } });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -4,7 +4,7 @@ import { Db, DbDriver } from '../src/Db';
|
|
|
4
4
|
import { TableWatcherRunner } from '../src/TableWatcherRunner';
|
|
5
5
|
import { Column, Table } from '../src/Table';
|
|
6
6
|
import { Record, RecordSerializer, SerializedRecord, withRecordColumns } from '../src/Record';
|
|
7
|
-
import {
|
|
7
|
+
import { StringColumn } from '../src/Columns';
|
|
8
8
|
import { DefaultTransactionContextFactory, TransactionContextData } from '../src/transaction/TransactionContextFactory';
|
|
9
9
|
import { DataKeyMaterial, DataKeyStore } from '../src/encryption/DataKeyStore';
|
|
10
10
|
import { EncryptionEnvelope } from '../src/encryption/EncryptionEnvelope';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { inspect } from 'util';
|
|
2
|
+
import { Logger } from '@proteinjs/logger';
|
|
3
|
+
import { TableManager } from '../src/schema/TableManager';
|
|
4
|
+
import { Table } from '../src/Table';
|
|
5
|
+
import { ArrayColumn, StringColumn } from '../src/Columns';
|
|
6
|
+
import { Record, withRecordColumns } from '../src/Record';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The census (`TableManager.warnUndeclaredColumns`): at every schema sync of an existing table,
|
|
10
|
+
* the physical columns no declared column owns are named in ONE warning — a column a rename
|
|
11
|
+
* retired (`tags` beside the `tag_list` that replaced it), a column the table dropped. Names
|
|
12
|
+
* only: the warning carries no stored value (the census reads the schema, never a row).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Member extends Record {
|
|
16
|
+
name: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class MemberTable extends Table<Member> {
|
|
21
|
+
public name = 'census_member';
|
|
22
|
+
public columns = withRecordColumns<Member>({
|
|
23
|
+
name: new StringColumn('name'),
|
|
24
|
+
tags: new ArrayColumn<string>('tag_list'),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type CapturedLog = { logLevel: string; message?: string; obj?: any };
|
|
29
|
+
type ManagerInternals = {
|
|
30
|
+
logger: Logger;
|
|
31
|
+
getTableChanges(table: Table<any>): Promise<unknown>;
|
|
32
|
+
getIndexOperations(table: Table<any>): Promise<{ indexesToCreate: unknown[]; indexesToDrop: unknown[] }>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const printedLine = (log: CapturedLog) => [log.message ?? '', inspect(log.obj, { depth: 10 })].join('\n');
|
|
36
|
+
|
|
37
|
+
describe('the undeclared-column census', () => {
|
|
38
|
+
const table = new MemberTable() as Table<Member>;
|
|
39
|
+
let captured: CapturedLog[];
|
|
40
|
+
|
|
41
|
+
const managerOver = (physicalColumns: string[]) => {
|
|
42
|
+
const columnMetadata: { [name: string]: { type: string; isNullable: boolean } } = {};
|
|
43
|
+
for (const name of physicalColumns) {
|
|
44
|
+
columnMetadata[name] = { type: 'STRING(MAX)', isNullable: true };
|
|
45
|
+
}
|
|
46
|
+
const manager = new TableManager(
|
|
47
|
+
{} as never,
|
|
48
|
+
{ getType: () => 'STRING(MAX)' } as never,
|
|
49
|
+
{} as never,
|
|
50
|
+
{
|
|
51
|
+
getColumnMetadata: async () => columnMetadata,
|
|
52
|
+
getUniqueColumns: async () => [],
|
|
53
|
+
getForeignKeys: async () => ({}),
|
|
54
|
+
} as never
|
|
55
|
+
);
|
|
56
|
+
const internals = manager as unknown as ManagerInternals;
|
|
57
|
+
internals.logger = new Logger({
|
|
58
|
+
name: 'TableManager',
|
|
59
|
+
logWriter: { write: (log: CapturedLog) => captured.push(log) } as never,
|
|
60
|
+
});
|
|
61
|
+
internals.getIndexOperations = async () => ({ indexesToCreate: [], indexesToDrop: [] });
|
|
62
|
+
return internals;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
captured = [];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a physical column no declared column owns is named, once, with the table — names only', async () => {
|
|
70
|
+
const manager = managerOver(['id', 'created', 'updated', 'name', 'tag_list', 'tags']);
|
|
71
|
+
|
|
72
|
+
await manager.getTableChanges(table);
|
|
73
|
+
|
|
74
|
+
const warnings = captured.filter((log) => log.logLevel === 'warn');
|
|
75
|
+
expect(warnings).toHaveLength(1);
|
|
76
|
+
expect(warnings[0].obj).toEqual({ table: 'census_member', columns: ['tags'] });
|
|
77
|
+
expect(printedLine(warnings[0])).toContain('census_member');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a table whose every physical column is declared warns nothing', async () => {
|
|
81
|
+
const manager = managerOver(['id', 'created', 'updated', 'name', 'tag_list']);
|
|
82
|
+
|
|
83
|
+
await manager.getTableChanges(table);
|
|
84
|
+
|
|
85
|
+
expect(captured.filter((log) => log.logLevel === 'warn')).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
});
|
package/test/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './reusable/CascadeDeleteTests';
|
|
|
7
7
|
export * from './reusable/PreloadReferencesTests';
|
|
8
8
|
export * from './reusable/RecordIteratorTests';
|
|
9
9
|
export * from './reusable/SourceRecordSyncTests';
|
|
10
|
+
export * from './reusable/StoredValueContractTests';
|
|
10
11
|
|
|
11
12
|
export * from './util/testUser';
|
|
12
13
|
|
|
@@ -16,5 +17,6 @@ export * from './util/tables/crudTestTables';
|
|
|
16
17
|
export * from './util/tables/dynamicReferenceColumnTestTables';
|
|
17
18
|
export * from './util/tables/recordIteratorTestTables';
|
|
18
19
|
export * from './util/tables/sourceRecordSyncTestTables';
|
|
20
|
+
export * from './util/tables/storedValueContractTestTables';
|
|
19
21
|
export * from './util/tables/tableManagerTestTables';
|
|
20
22
|
export * from './util/tables/transactionTestTables';
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
StringColumn,
|
|
3
|
-
ReferenceColumn,
|
|
4
|
-
ReferenceArrayColumn,
|
|
5
|
-
DynamicReferenceColumn,
|
|
6
|
-
DynamicReferenceTableNameColumn,
|
|
7
|
-
Db,
|
|
8
|
-
DbDriver,
|
|
9
|
-
Table,
|
|
10
|
-
withRecordColumns,
|
|
11
|
-
Record,
|
|
12
|
-
Reference,
|
|
13
|
-
ReferenceArray,
|
|
14
|
-
DefaultTransactionContextFactory,
|
|
15
|
-
} from '@proteinjs/db';
|
|
1
|
+
import { Db, DbDriver, Table, Reference, ReferenceArray, DefaultTransactionContextFactory } from '@proteinjs/db';
|
|
16
2
|
import { QueryBuilder } from '@proteinjs/db-query';
|
|
17
3
|
import { DbTestEnvironment } from '../util/DbTestEnvironment';
|
|
18
4
|
import {
|
|
@@ -20,6 +6,10 @@ import {
|
|
|
20
6
|
GroupArr,
|
|
21
7
|
GroupDyn,
|
|
22
8
|
GroupRef,
|
|
9
|
+
GuardedHolderArr,
|
|
10
|
+
GuardedHolderDyn,
|
|
11
|
+
GuardedHolderRef,
|
|
12
|
+
GuardedMember,
|
|
23
13
|
MemberArr,
|
|
24
14
|
MemberArrRev,
|
|
25
15
|
MemberDyn,
|
|
@@ -238,5 +228,91 @@ export const cascadeDeleteTests = (
|
|
|
238
228
|
expect(missionsAfterPilot.length).toBe(0);
|
|
239
229
|
});
|
|
240
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
|
+
});
|
|
241
317
|
};
|
|
242
318
|
};
|