@proteinjs/db 1.51.0 → 1.53.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.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/Db.d.ts +48 -0
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +206 -31
- package/dist/src/Db.js.map +1 -1
- package/dist/src/DuplicateKeyError.d.ts +20 -0
- package/dist/src/DuplicateKeyError.d.ts.map +1 -0
- package/dist/src/DuplicateKeyError.js +47 -0
- package/dist/src/DuplicateKeyError.js.map +1 -0
- package/dist/test/DuplicateKeyError.test.d.ts +2 -0
- package/dist/test/DuplicateKeyError.test.d.ts.map +1 -0
- package/dist/test/DuplicateKeyError.test.js +287 -0
- package/dist/test/DuplicateKeyError.test.js.map +1 -0
- package/dist/test/InsertMany.test.d.ts +2 -0
- package/dist/test/InsertMany.test.d.ts.map +1 -0
- package/dist/test/InsertMany.test.js +475 -0
- package/dist/test/InsertMany.test.js.map +1 -0
- package/dist/test/TableServiceAuth.test.js +12 -0
- package/dist/test/TableServiceAuth.test.js.map +1 -1
- package/dist/test/reusable/CrudTests.d.ts.map +1 -1
- package/dist/test/reusable/CrudTests.js +44 -0
- package/dist/test/reusable/CrudTests.js.map +1 -1
- package/dist/test/reusable/TransactionTests.d.ts.map +1 -1
- package/dist/test/reusable/TransactionTests.js +90 -0
- package/dist/test/reusable/TransactionTests.js.map +1 -1
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +1 -1
- package/index.ts +1 -0
- package/package.json +3 -3
- package/src/Db.ts +128 -1
- package/src/DuplicateKeyError.ts +26 -0
- package/test/DuplicateKeyError.test.ts +179 -0
- package/test/InsertMany.test.ts +298 -0
- package/test/TableServiceAuth.test.ts +12 -0
- package/test/reusable/CrudTests.ts +30 -0
- package/test/reusable/TransactionTests.ts +46 -0
package/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './src/Db';
|
|
|
2
2
|
export * from './src/DetachedDbOps';
|
|
3
3
|
export * from './src/Table';
|
|
4
4
|
export * from './src/RecordAccessError';
|
|
5
|
+
export * from './src/DuplicateKeyError';
|
|
5
6
|
export * from './src/auth/TableAuth';
|
|
6
7
|
export * from './src/auth/TableServiceAuth';
|
|
7
8
|
export * from './src/Columns';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.0",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test": "jest --passWithNoTests"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@proteinjs/db-query": "^1.
|
|
44
|
+
"@proteinjs/db-query": "^1.11.0",
|
|
45
45
|
"@proteinjs/logger": "^1.2.0",
|
|
46
46
|
"@proteinjs/reflection": "^1.2.0",
|
|
47
47
|
"@proteinjs/serializer": "^1.1.10",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "480320d415088dd0d378bd0e9863dc70353fa92f"
|
|
70
70
|
}
|
package/src/Db.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { ReferenceArray } from './reference/ReferenceArray';
|
|
|
32
32
|
import { ReferenceCache } from './reference/ReferenceCache';
|
|
33
33
|
import { ArrayMembershipUpdate, applyArrayMembershipOps } from './reference/ArrayMembershipOps';
|
|
34
34
|
import { PreservedPath, overlayPreservedPaths } from './UpdatePreserving';
|
|
35
|
+
import { DuplicateKeyError } from './DuplicateKeyError';
|
|
35
36
|
|
|
36
37
|
/** get `Db` if on server, and `DbService` if on browser */
|
|
37
38
|
export const getDb = <R extends Record = Record>() =>
|
|
@@ -104,6 +105,21 @@ export interface DbDriver {
|
|
|
104
105
|
* configured its database to tolerate rather than a number of its own.
|
|
105
106
|
*/
|
|
106
107
|
getOperationDeadlineMs(): number;
|
|
108
|
+
/**
|
|
109
|
+
* The most bound parameters one statement may carry on this driver's backend, when the backend
|
|
110
|
+
* bounds it — `Db.insertMany` sizes its multi-row statements by it (`columns × rows` parameters
|
|
111
|
+
* each), so a batch wider than the bound goes as the fewest statements that fit instead of one
|
|
112
|
+
* the backend refuses. A driver whose backend declares no such bound leaves this undeclared,
|
|
113
|
+
* and a batch on it is always one statement.
|
|
114
|
+
*/
|
|
115
|
+
getStatementParameterLimit?(): number;
|
|
116
|
+
/**
|
|
117
|
+
* Whether `error` — what this driver's `runDml` threw — is the backend refusing a row because a
|
|
118
|
+
* row with the same key is already there (the primary key, or a unique index). `Db.insert` and
|
|
119
|
+
* `Db.insertMany` rethrow such a refusal as the typed `DuplicateKeyError`, so a caller can take
|
|
120
|
+
* it by type. A driver that declares no classifier lets the backend's error through as it came.
|
|
121
|
+
*/
|
|
122
|
+
isDuplicateKeyError?(error: unknown): boolean;
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
export class Db<R extends Record = Record> implements DbService<R> {
|
|
@@ -211,7 +227,11 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
211
227
|
serializedRecord as Partial<T>,
|
|
212
228
|
this.statementConfigFactory.getStatementConfig(config)
|
|
213
229
|
);
|
|
214
|
-
|
|
230
|
+
try {
|
|
231
|
+
await this.dbDriver.runDml(generateInsert, this.transactionForDriver());
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw this.insertFailure(table, error);
|
|
234
|
+
}
|
|
215
235
|
if (encryptionContext) {
|
|
216
236
|
// The same write that stored the ciphertext maintains its search tokens (rides the
|
|
217
237
|
// ambient transaction when there is one).
|
|
@@ -228,6 +248,81 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
228
248
|
return recordCopy as T;
|
|
229
249
|
}
|
|
230
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Insert `records` as ONE statement (a multi-row INSERT) instead of one statement per row,
|
|
253
|
+
* each row keeping every contract {@link insert} gives a single one: the table's default field
|
|
254
|
+
* values, the before-insert table watchers, the columns' before-insert hooks, the encryption
|
|
255
|
+
* context, then — after the statement — the search-token upkeep, the columns' after-insert
|
|
256
|
+
* hooks and the after-insert table watchers. Each phase runs per row, in the order given, and
|
|
257
|
+
* sees the row exactly as `insert` would; what differs from N single inserts is only that every
|
|
258
|
+
* row's before-phases precede the statement and every row's after-phases follow it.
|
|
259
|
+
*
|
|
260
|
+
* Atomic: inside a transaction the statement rides it; outside one this opens a transaction
|
|
261
|
+
* of its own around the batch, so a row the database refuses fails the whole batch and no row
|
|
262
|
+
* of it lands. A batch wider than the driver's statement parameter bound
|
|
263
|
+
* ({@link DbDriver.getStatementParameterLimit}) goes as the fewest statements that fit, in
|
|
264
|
+
* order, inside that same transaction.
|
|
265
|
+
*
|
|
266
|
+
* Returns the rows as inserted (defaults applied, watchers' changes carried), in order. An
|
|
267
|
+
* empty list inserts nothing and returns [].
|
|
268
|
+
*/
|
|
269
|
+
async insertMany<T extends R>(table: Table<T>, records: Omit<T, keyof R>[]): Promise<T[]> {
|
|
270
|
+
if (!this.runAsSystem) {
|
|
271
|
+
this.auth.canInsert(table);
|
|
272
|
+
}
|
|
273
|
+
if (records.length === 0) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
if (!this.transactionContextFactory.getTransactionContext().currentTransaction) {
|
|
277
|
+
const db = this.newSelfWrapDb();
|
|
278
|
+
return await db.runTransaction(async () => await db.insertMany(table, records));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const prepared: {
|
|
282
|
+
recordCopy: any;
|
|
283
|
+
encryptionContext?: { keyOwner: string };
|
|
284
|
+
serializedRecord: SerializedRecord;
|
|
285
|
+
}[] = [];
|
|
286
|
+
for (const record of records) {
|
|
287
|
+
let recordCopy = Object.assign({}, record);
|
|
288
|
+
await addDefaultFieldValues(table, recordCopy, this.runAsSystem);
|
|
289
|
+
recordCopy = await this.tableWatcherRunner.runBeforeInsertTableWatchers(table, recordCopy);
|
|
290
|
+
await this.addColumnInsertHooks(table, recordCopy);
|
|
291
|
+
const encryptionContext = await this.encryptionWriteContext(table, recordCopy);
|
|
292
|
+
const serializedRecord = await new RecordSerializer(table, encryptionContext).serialize(recordCopy);
|
|
293
|
+
prepared.push({ recordCopy, encryptionContext, serializedRecord });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const statementRows of this.chunkByParameterLimit(prepared.map((row) => row.serializedRecord))) {
|
|
297
|
+
const generateInsert = (config: DbDriverDmlStatementConfig) =>
|
|
298
|
+
new StatementFactory<T>().insertMany(
|
|
299
|
+
table.name,
|
|
300
|
+
statementRows as Partial<T>[],
|
|
301
|
+
this.statementConfigFactory.getStatementConfig(config)
|
|
302
|
+
);
|
|
303
|
+
try {
|
|
304
|
+
await this.dbDriver.runDml(generateInsert, this.transactionForDriver());
|
|
305
|
+
} catch (error) {
|
|
306
|
+
throw this.insertFailure(table, error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const { recordCopy, encryptionContext } of prepared) {
|
|
311
|
+
if (encryptionContext) {
|
|
312
|
+
const { EncryptionTokenMaintenance } = await import('./encryption/EncryptionTokenMaintenance');
|
|
313
|
+
await new EncryptionTokenMaintenance().afterInsert(
|
|
314
|
+
table,
|
|
315
|
+
recordCopy,
|
|
316
|
+
encryptionContext.keyOwner,
|
|
317
|
+
this.newSystemDb()
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
await this.runColumnAfterInsertHooks(table, recordCopy);
|
|
321
|
+
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
322
|
+
}
|
|
323
|
+
return prepared.map((row) => row.recordCopy as T);
|
|
324
|
+
}
|
|
325
|
+
|
|
231
326
|
async update<T extends R>(table: Table<T>, record: Partial<T>, query?: Query<T>): Promise<number> {
|
|
232
327
|
if (!this.runAsSystem) {
|
|
233
328
|
this.auth.canUpdate(table);
|
|
@@ -941,6 +1036,15 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
941
1036
|
return { keyOwner: await new EncryptionRecordHooks().resolveKeyOwnerForWrite(table, record) };
|
|
942
1037
|
}
|
|
943
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* What an insert statement's failure becomes for the caller: the typed `DuplicateKeyError` when
|
|
1041
|
+
* the driver classifies the backend's refusal as a row already there; any other failure as it
|
|
1042
|
+
* came.
|
|
1043
|
+
*/
|
|
1044
|
+
private insertFailure(table: Table<any>, error: unknown): unknown {
|
|
1045
|
+
return this.dbDriver.isDuplicateKeyError?.(error) ? new DuplicateKeyError(table.name, error) : error;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
944
1048
|
private async updateTouchesEncryptedColumns(table: Table<any>, record: any): Promise<boolean> {
|
|
945
1049
|
const { EncryptedColumns } = await import('./encryption/EncryptedColumns');
|
|
946
1050
|
const encryptedColumns = new EncryptedColumns();
|
|
@@ -1045,6 +1149,29 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
1045
1149
|
return context.currentTransaction;
|
|
1046
1150
|
}
|
|
1047
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* The rows of one multi-row insert split into the fewest statements the driver's statement
|
|
1154
|
+
* parameter bound admits (`insertMany`): each statement binds `columns × rows` parameters,
|
|
1155
|
+
* the columns being the union over the whole batch (what `StatementFactory.insertMany` lists),
|
|
1156
|
+
* so the split follows the batch's widest shape rather than the first statement's. One
|
|
1157
|
+
* statement when the driver declares no bound.
|
|
1158
|
+
*/
|
|
1159
|
+
private chunkByParameterLimit(rows: SerializedRecord[]): SerializedRecord[][] {
|
|
1160
|
+
const limit = this.dbDriver.getStatementParameterLimit?.();
|
|
1161
|
+
if (limit === undefined) {
|
|
1162
|
+
return [rows];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const columns = new Set<string>();
|
|
1166
|
+
for (const row of rows) {
|
|
1167
|
+
for (const column of Object.keys(row)) {
|
|
1168
|
+
columns.add(column);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const rowsPerStatement = Math.max(1, Math.floor(limit / Math.max(1, columns.size)));
|
|
1172
|
+
return this.chunk(rows, rowsPerStatement);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1048
1175
|
// Utility: simple chunker
|
|
1049
1176
|
private chunk<T>(arr: T[], size: number): T[][] {
|
|
1050
1177
|
if (size <= 0) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An insert the database refused because a row with the same key is already there — the primary
|
|
3
|
+
* key, or any unique index the table declares. `Db.insert` and `Db.insertMany` throw it in place
|
|
4
|
+
* of the driver's own error (which rides as `cause`, never printed: the backend's words quote the
|
|
5
|
+
* colliding key), so a caller for whom "that row already exists" is a branch of its own — an
|
|
6
|
+
* insert-then-update that keeps one row per key under concurrent writers — takes it by type,
|
|
7
|
+
* never by the backend's message text. Which errors are this class is the driver's call
|
|
8
|
+
* (`DbDriver.isDuplicateKeyError`); a driver that declares no classifier lets the backend's error
|
|
9
|
+
* through as it came.
|
|
10
|
+
*
|
|
11
|
+
* Name-tagged rather than relying on `instanceof`: the prototype chain is unreliable across
|
|
12
|
+
* package compile targets (the same reason `RecordAccessError` checks `name`).
|
|
13
|
+
*/
|
|
14
|
+
export class DuplicateKeyError extends Error {
|
|
15
|
+
/** The driver's error — for callers in process; not enumerable, so a serialized error carries no row key. */
|
|
16
|
+
readonly cause: unknown;
|
|
17
|
+
|
|
18
|
+
constructor(tableName: string, cause: unknown) {
|
|
19
|
+
super(`(${tableName}) Insert refused: a row with the same key already exists`);
|
|
20
|
+
this.name = 'DuplicateKeyError';
|
|
21
|
+
Object.defineProperty(this, 'cause', { value: cause, enumerable: false, writable: false });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const isDuplicateKeyError = (error: unknown): error is DuplicateKeyError =>
|
|
26
|
+
!!error && typeof error === 'object' && (error as { name?: string }).name === 'DuplicateKeyError';
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { SourceRepository } from '@proteinjs/reflection';
|
|
2
|
+
import { Statement } from '@proteinjs/db-query';
|
|
3
|
+
import { Db, DbDriver, DbDriverDmlStatementConfig } from '../src/Db';
|
|
4
|
+
import { DuplicateKeyError, isDuplicateKeyError } from '../src/DuplicateKeyError';
|
|
5
|
+
import { Table } from '../src/Table';
|
|
6
|
+
import { Record, withRecordColumns } from '../src/Record';
|
|
7
|
+
import { StringColumn } from '../src/Columns';
|
|
8
|
+
import { TableWatcherRunner } from '../src/TableWatcherRunner';
|
|
9
|
+
import { DefaultTransactionContextFactory, TransactionContextData } from '../src/transaction/TransactionContextFactory';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What an insert's refusal becomes for the caller: a refusal the driver classifies as a duplicate
|
|
13
|
+
* key (a row with the same key already there — the primary key or a unique index) reaches the
|
|
14
|
+
* caller as the typed `DuplicateKeyError`, name-tagged, naming the table and carrying the driver's
|
|
15
|
+
* error as a cause that never prints; every other failure, and every failure on a driver that
|
|
16
|
+
* declares no classifier, reaches the caller exactly as the driver threw it. The same on
|
|
17
|
+
* `insertMany`. What the database itself refuses is the drivers' emulator suites' (the settings
|
|
18
|
+
* package's one-row-per-name suite proves the unique index's refusal end to end).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface Doc extends Record {
|
|
22
|
+
title: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class DocTable extends Table<Doc> {
|
|
26
|
+
public name = 'duplicate_key_doc';
|
|
27
|
+
public columns = withRecordColumns<Doc>({
|
|
28
|
+
title: new StringColumn('title'),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const docTable = new DocTable() as Table<Doc>;
|
|
33
|
+
|
|
34
|
+
class TestTransactionContextFactory implements DefaultTransactionContextFactory {
|
|
35
|
+
private context: TransactionContextData = {};
|
|
36
|
+
|
|
37
|
+
getTransactionContext(): TransactionContextData {
|
|
38
|
+
return this.context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async runInContext<T>(context: TransactionContextData, fn: () => Promise<T>): Promise<T> {
|
|
42
|
+
const previous = this.context;
|
|
43
|
+
this.context = context;
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
} finally {
|
|
47
|
+
this.context = previous;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A driver whose every DML fails with the error it is handed, classifying `code === 6` as a duplicate key. */
|
|
53
|
+
class RefusingDriver implements DbDriver {
|
|
54
|
+
constructor(private readonly refusal: unknown) {}
|
|
55
|
+
|
|
56
|
+
getDbName(): string {
|
|
57
|
+
return 'test';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async createDbIfNotExists(): Promise<void> {}
|
|
61
|
+
|
|
62
|
+
async createDb(): Promise<void> {}
|
|
63
|
+
|
|
64
|
+
async dropDb(): Promise<void> {}
|
|
65
|
+
|
|
66
|
+
getTableManager(): never {
|
|
67
|
+
throw new Error('not used by these tests');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async runQuery(): Promise<never[]> {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async runDml(generateStatement: (config: DbDriverDmlStatementConfig) => Statement): Promise<number> {
|
|
75
|
+
generateStatement({
|
|
76
|
+
useParams: true,
|
|
77
|
+
useNamedParams: true,
|
|
78
|
+
prefixTablesWithDb: false,
|
|
79
|
+
getDriverColumnType: () => 'STRING(MAX)',
|
|
80
|
+
});
|
|
81
|
+
throw this.refusal;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async runTransaction<T>(fn: (transaction: unknown) => Promise<T>): Promise<T> {
|
|
85
|
+
return await fn({ id: 'transaction' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getOperationDeadlineMs(): number {
|
|
89
|
+
return 1000;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isDuplicateKeyError(error: unknown): boolean {
|
|
93
|
+
return (error as { code?: unknown } | null | undefined)?.code === 6;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The same driver with no classifier declared. */
|
|
98
|
+
class UnclassifyingDriver extends RefusingDriver {
|
|
99
|
+
isDuplicateKeyError = undefined as unknown as (error: unknown) => boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dbOn = (driver: DbDriver) => new Db(driver, () => docTable, new TestTransactionContextFactory(), true);
|
|
103
|
+
|
|
104
|
+
const failureOf = async (act: () => Promise<unknown>): Promise<unknown> => {
|
|
105
|
+
try {
|
|
106
|
+
await act();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return error;
|
|
109
|
+
}
|
|
110
|
+
throw new Error('the insert was expected to fail');
|
|
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
|
+
|
|
117
|
+
describe('an insert the database refuses because the row is already there', () => {
|
|
118
|
+
const backendRefusal = Object.assign(new Error('Row {pk#id:"7f3"} in table duplicate_key_doc already exists'), {
|
|
119
|
+
code: 6,
|
|
120
|
+
});
|
|
121
|
+
let originalWatchers: unknown[] | undefined;
|
|
122
|
+
|
|
123
|
+
// No reflection graph in a unit test: the watcher registry is empty by declaration.
|
|
124
|
+
beforeAll(() => {
|
|
125
|
+
originalWatchers = objectCache()['@proteinjs/db/TableWatcher'];
|
|
126
|
+
objectCache()['@proteinjs/db/TableWatcher'] = [];
|
|
127
|
+
(TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterAll(() => {
|
|
131
|
+
if (originalWatchers) {
|
|
132
|
+
objectCache()['@proteinjs/db/TableWatcher'] = originalWatchers;
|
|
133
|
+
} else {
|
|
134
|
+
delete objectCache()['@proteinjs/db/TableWatcher'];
|
|
135
|
+
}
|
|
136
|
+
(TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('reaches the caller as the typed DuplicateKeyError, naming the table, carrying the cause unprinted', async () => {
|
|
140
|
+
const failure = await failureOf(() => dbOn(new RefusingDriver(backendRefusal)).insert(docTable, { title: 'a' }));
|
|
141
|
+
expect(isDuplicateKeyError(failure)).toBe(true);
|
|
142
|
+
const typed = failure as DuplicateKeyError;
|
|
143
|
+
expect(typed.name).toBe('DuplicateKeyError');
|
|
144
|
+
expect(typed.message).toBe('(duplicate_key_doc) Insert refused: a row with the same key already exists');
|
|
145
|
+
expect(typed.cause).toBe(backendRefusal);
|
|
146
|
+
// The backend's words quote the colliding key: they ride for callers in process only.
|
|
147
|
+
expect(Object.keys(typed)).not.toContain('cause');
|
|
148
|
+
expect(JSON.stringify(typed)).not.toContain('pk#id');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('the same on insertMany', async () => {
|
|
152
|
+
const failure = await failureOf(() =>
|
|
153
|
+
dbOn(new RefusingDriver(backendRefusal)).insertMany(docTable, [{ title: 'a' }, { title: 'b' }])
|
|
154
|
+
);
|
|
155
|
+
expect(isDuplicateKeyError(failure)).toBe(true);
|
|
156
|
+
expect((failure as DuplicateKeyError).cause).toBe(backendRefusal);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('any other refusal reaches the caller as the driver threw it', async () => {
|
|
160
|
+
const aborted = Object.assign(new Error('Transaction was aborted'), { code: 10 });
|
|
161
|
+
const failure = await failureOf(() => dbOn(new RefusingDriver(aborted)).insert(docTable, { title: 'a' }));
|
|
162
|
+
expect(failure).toBe(aborted);
|
|
163
|
+
expect(isDuplicateKeyError(failure)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('a driver that declares no classifier lets the refusal through as it came', async () => {
|
|
167
|
+
const failure = await failureOf(() =>
|
|
168
|
+
dbOn(new UnclassifyingDriver(backendRefusal)).insert(docTable, { title: 'a' })
|
|
169
|
+
);
|
|
170
|
+
expect(failure).toBe(backendRefusal);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('the guard reads the name, not the prototype chain', () => {
|
|
174
|
+
expect(isDuplicateKeyError(Object.assign(new Error('x'), { name: 'DuplicateKeyError' }))).toBe(true);
|
|
175
|
+
expect(isDuplicateKeyError(new Error('DuplicateKeyError'))).toBe(false);
|
|
176
|
+
expect(isDuplicateKeyError(undefined)).toBe(false);
|
|
177
|
+
expect(isDuplicateKeyError('DuplicateKeyError')).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
});
|