@proteinjs/db 1.52.0 → 1.54.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/generated/test/index.js +1 -1
  5. package/dist/generated/test/index.js.map +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/src/Db.d.ts +58 -5
  11. package/dist/src/Db.d.ts.map +1 -1
  12. package/dist/src/Db.js +83 -24
  13. package/dist/src/Db.js.map +1 -1
  14. package/dist/src/DuplicateKeyError.d.ts +20 -0
  15. package/dist/src/DuplicateKeyError.d.ts.map +1 -0
  16. package/dist/src/DuplicateKeyError.js +47 -0
  17. package/dist/src/DuplicateKeyError.js.map +1 -0
  18. package/dist/src/TransactionRetryExhaustedError.d.ts +25 -0
  19. package/dist/src/TransactionRetryExhaustedError.d.ts.map +1 -0
  20. package/dist/src/TransactionRetryExhaustedError.js +54 -0
  21. package/dist/src/TransactionRetryExhaustedError.js.map +1 -0
  22. package/dist/src/transaction/TransactionRetryPolicy.d.ts +34 -0
  23. package/dist/src/transaction/TransactionRetryPolicy.d.ts.map +1 -0
  24. package/dist/src/transaction/TransactionRetryPolicy.js +3 -0
  25. package/dist/src/transaction/TransactionRetryPolicy.js.map +1 -0
  26. package/dist/test/DuplicateKeyError.test.d.ts +2 -0
  27. package/dist/test/DuplicateKeyError.test.d.ts.map +1 -0
  28. package/dist/test/DuplicateKeyError.test.js +287 -0
  29. package/dist/test/DuplicateKeyError.test.js.map +1 -0
  30. package/dist/test/TransactionRetryOptions.test.d.ts +2 -0
  31. package/dist/test/TransactionRetryOptions.test.d.ts.map +1 -0
  32. package/dist/test/TransactionRetryOptions.test.js +293 -0
  33. package/dist/test/TransactionRetryOptions.test.js.map +1 -0
  34. package/generated/index.ts +1 -1
  35. package/generated/test/index.ts +1 -1
  36. package/index.ts +3 -0
  37. package/package.json +2 -2
  38. package/src/Db.ts +98 -12
  39. package/src/DuplicateKeyError.ts +26 -0
  40. package/src/TransactionRetryExhaustedError.ts +37 -0
  41. package/src/transaction/TransactionRetryPolicy.ts +31 -0
  42. package/test/DuplicateKeyError.test.ts +179 -0
  43. package/test/TransactionRetryOptions.test.ts +195 -0
@@ -0,0 +1,195 @@
1
+ import { SourceRepository } from '@proteinjs/reflection';
2
+ import { Statement } from '@proteinjs/db-query';
3
+ import { Db, DbDriver, DbDriverDmlStatementConfig } from '../src/Db';
4
+ import { Table } from '../src/Table';
5
+ import { Record, withRecordColumns } from '../src/Record';
6
+ import { StringColumn } from '../src/Columns';
7
+ import { TableWatcherRunner } from '../src/TableWatcherRunner';
8
+ import { DefaultTransactionContextFactory, TransactionContextData } from '../src/transaction/TransactionContextFactory';
9
+ import { TransactionOptions } from '../src/transaction/TransactionRetryPolicy';
10
+ import {
11
+ TransactionRetryExhaustedError,
12
+ isTransactionRetryExhaustedError,
13
+ } from '../src/TransactionRetryExhaustedError';
14
+
15
+ /**
16
+ * The retry policy a caller names at the call reaches the driver on the write's own transaction —
17
+ * `insert` and `update` outside a transaction hand it to `runDml` beside the (absent) transaction,
18
+ * `runTransaction` hands it to the driver's `runTransaction` — and a write inside a transaction
19
+ * naming a policy of its own is refused by name: the transaction's policy governs every write in
20
+ * it, and a policy nothing would honour is never dropped on the floor. What the driver does with
21
+ * the policy — the bounded attempts, the typed refusal — is the driver suites' (the Spanner
22
+ * driver's BoundedTransactionRetry suite, on the emulator); the typed refusal's shape is held
23
+ * here.
24
+ */
25
+
26
+ interface Doc extends Record {
27
+ title: string;
28
+ }
29
+
30
+ class DocTable extends Table<Doc> {
31
+ public name = 'retry_options_doc';
32
+ public columns = withRecordColumns<Doc>({
33
+ title: new StringColumn('title'),
34
+ });
35
+ }
36
+
37
+ const docTable = new DocTable() as Table<Doc>;
38
+
39
+ class TestTransactionContextFactory implements DefaultTransactionContextFactory {
40
+ private context: TransactionContextData = {};
41
+
42
+ getTransactionContext(): TransactionContextData {
43
+ return this.context;
44
+ }
45
+
46
+ async runInContext<T>(context: TransactionContextData, fn: () => Promise<T>): Promise<T> {
47
+ const previous = this.context;
48
+ this.context = context;
49
+ try {
50
+ return await fn();
51
+ } finally {
52
+ this.context = previous;
53
+ }
54
+ }
55
+ }
56
+
57
+ /** A driver that records the transaction and options every write and transaction arrived with. */
58
+ class RecordingDriver implements DbDriver {
59
+ readonly dmlCalls: { transaction: unknown; options: TransactionOptions | undefined }[] = [];
60
+ readonly transactionCalls: { options: TransactionOptions | undefined }[] = [];
61
+
62
+ getDbName(): string {
63
+ return 'test';
64
+ }
65
+
66
+ async createDbIfNotExists(): Promise<void> {}
67
+
68
+ async createDb(): Promise<void> {}
69
+
70
+ async dropDb(): Promise<void> {}
71
+
72
+ getTableManager(): never {
73
+ throw new Error('not used by these tests');
74
+ }
75
+
76
+ async runQuery(): Promise<never[]> {
77
+ return [];
78
+ }
79
+
80
+ async runDml(
81
+ generateStatement: (config: DbDriverDmlStatementConfig) => Statement,
82
+ transaction?: unknown,
83
+ options?: TransactionOptions
84
+ ): Promise<number> {
85
+ generateStatement({
86
+ useParams: true,
87
+ useNamedParams: true,
88
+ prefixTablesWithDb: false,
89
+ getDriverColumnType: () => 'STRING(MAX)',
90
+ });
91
+ this.dmlCalls.push({ transaction, options });
92
+ return 1;
93
+ }
94
+
95
+ async runTransaction<T>(fn: (transaction: unknown) => Promise<T>, options?: TransactionOptions): Promise<T> {
96
+ this.transactionCalls.push({ options });
97
+ return await fn({ handle: 'the transaction' });
98
+ }
99
+
100
+ getOperationDeadlineMs(): number {
101
+ return 60_000;
102
+ }
103
+ }
104
+
105
+ type SourceRepositoryInternals = { objectCache: { [qualifiedName: string]: unknown[] } };
106
+ type TableWatcherRunnerStatics = { tableWatcherMap?: unknown };
107
+ const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
108
+
109
+ describe('A write names its retry policy at the call; it reaches the driver on the write`s own transaction', () => {
110
+ let driver: RecordingDriver;
111
+ let db: Db<Doc>;
112
+ let originalWatchers: unknown[] | undefined;
113
+
114
+ // No reflection graph in a unit test: the watcher registry is empty by declaration.
115
+ beforeAll(() => {
116
+ originalWatchers = objectCache()['@proteinjs/db/TableWatcher'];
117
+ objectCache()['@proteinjs/db/TableWatcher'] = [];
118
+ (TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
119
+ });
120
+
121
+ afterAll(() => {
122
+ if (originalWatchers) {
123
+ objectCache()['@proteinjs/db/TableWatcher'] = originalWatchers;
124
+ } else {
125
+ delete objectCache()['@proteinjs/db/TableWatcher'];
126
+ }
127
+ (TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
128
+ });
129
+
130
+ beforeEach(() => {
131
+ driver = new RecordingDriver();
132
+ db = new Db<Doc>(driver, () => docTable, new TestTransactionContextFactory(), true);
133
+ });
134
+
135
+ test('an update outside a transaction carries its policy to the driver, with no transaction', async () => {
136
+ await db.update(docTable, { id: 'doc-1', title: 'a title' }, undefined, { retry: { attempts: 2, maxMs: 1500 } });
137
+
138
+ expect(driver.dmlCalls).toEqual([{ transaction: undefined, options: { retry: { attempts: 2, maxMs: 1500 } } }]);
139
+ });
140
+
141
+ test('an insert outside a transaction carries its policy to the driver, with no transaction', async () => {
142
+ await db.insert(docTable, { title: 'a title' }, { retry: 'none' });
143
+
144
+ expect(driver.dmlCalls).toEqual([{ transaction: undefined, options: { retry: 'none' } }]);
145
+ });
146
+
147
+ test('a write with no policy reaches the driver with none — the driver`s own road', async () => {
148
+ await db.update(docTable, { id: 'doc-1', title: 'a title' });
149
+ await db.insert(docTable, { title: 'a title' });
150
+
151
+ expect(driver.dmlCalls.map((call) => call.options)).toEqual([undefined, undefined]);
152
+ });
153
+
154
+ test('a transaction carries its policy to the driver, and the writes inside it ride the transaction with no policy of their own', async () => {
155
+ await db.runTransaction(
156
+ async () => {
157
+ await db.update(docTable, { id: 'doc-1', title: 'a title' });
158
+ await db.insert(docTable, { title: 'a title' });
159
+ },
160
+ { retry: { attempts: 3 } }
161
+ );
162
+
163
+ expect(driver.transactionCalls).toEqual([{ options: { retry: { attempts: 3 } } }]);
164
+ expect(driver.dmlCalls).toEqual([
165
+ { transaction: { handle: 'the transaction' }, options: undefined },
166
+ { transaction: { handle: 'the transaction' }, options: undefined },
167
+ ]);
168
+ });
169
+
170
+ test('a write inside a transaction naming a policy of its own is refused by name, and never reaches the driver', async () => {
171
+ const refusal = db.runTransaction(async () => {
172
+ await db.update(docTable, { id: 'doc-1', title: 'a title' }, undefined, { retry: 'none' });
173
+ });
174
+
175
+ await expect(refusal).rejects.toThrow('A write inside a transaction cannot name a retry policy of its own');
176
+ expect(driver.dmlCalls).toEqual([]);
177
+ });
178
+
179
+ test('the typed refusal: name-tagged, carrying the attempts and the elapsed time, its cause never enumerable', () => {
180
+ const abort = new Error('10 ABORTED: the backend quotes a row value here');
181
+ const refusal = new TransactionRetryExhaustedError('spanner dml transaction', 2, 312.4, abort);
182
+
183
+ expect(isTransactionRetryExhaustedError(refusal)).toBe(true);
184
+ expect(isTransactionRetryExhaustedError(new Error('other'))).toBe(false);
185
+ expect(refusal.name).toBe('TransactionRetryExhaustedError');
186
+ expect(refusal.attempts).toBe(2);
187
+ expect(refusal.elapsedMs).toBe(312.4);
188
+ expect(refusal.cause).toBe(abort);
189
+ expect(Object.keys(refusal)).not.toContain('cause');
190
+ expect(JSON.stringify(refusal)).not.toContain('row value');
191
+ expect(refusal.message).toBe(
192
+ 'Transaction aborted and not retried further: its retry policy allowed 2 attempts, made in 312ms (spanner dml transaction)'
193
+ );
194
+ });
195
+ });