@proteinjs/db 1.26.0 → 1.27.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 +17 -0
- package/LICENSE +21 -0
- package/dist/generated/index.js +3 -3
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.js +3 -3
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/Db.d.ts +18 -5
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +43 -35
- package/dist/src/Db.js.map +1 -1
- package/dist/src/transaction/TransactionContextFactory.d.ts +9 -2
- package/dist/src/transaction/TransactionContextFactory.d.ts.map +1 -1
- package/generated/index.ts +16 -19
- package/generated/test/index.ts +40 -43
- package/package.json +3 -2
- package/src/Db.ts +47 -28
- package/src/transaction/TransactionContextFactory.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -65,5 +65,6 @@
|
|
|
65
65
|
"jest": "29.7.0",
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
|
-
}
|
|
68
|
+
},
|
|
69
|
+
"gitHead": "020e0d6b909cdd60925952aa05c23cfdecd8353d"
|
|
69
70
|
}
|
package/src/Db.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
DefaultTransactionContextFactory,
|
|
25
25
|
PostCommitHook,
|
|
26
26
|
getDefaultTransactionContextFactory,
|
|
27
|
+
TransactionContextData,
|
|
27
28
|
} from './transaction/TransactionContextFactory';
|
|
28
29
|
import { isInstanceOf } from '@proteinjs/util';
|
|
29
30
|
import { Reference } from './reference/Reference';
|
|
@@ -76,7 +77,6 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
76
77
|
private statementConfigFactory: StatementConfigFactory;
|
|
77
78
|
private auth = new TableAuth();
|
|
78
79
|
private tableWatcherRunner = new TableWatcherRunner<R>();
|
|
79
|
-
private currentTransaction?: any;
|
|
80
80
|
private transactionContextFactory: DefaultTransactionContextFactory;
|
|
81
81
|
public serviceMetadata: Service['serviceMetadata'] = {
|
|
82
82
|
auth: {
|
|
@@ -96,10 +96,6 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
96
96
|
this.transactionContextFactory = transactionContextFactory
|
|
97
97
|
? transactionContextFactory
|
|
98
98
|
: this.getDefaultTransactionContextFactory();
|
|
99
|
-
const transactionContext = this.transactionContextFactory.getTransactionContext();
|
|
100
|
-
if (transactionContext.currentTransaction) {
|
|
101
|
-
this.currentTransaction = transactionContext.currentTransaction;
|
|
102
|
-
}
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
static getDefaultDbDriver(): DbDriver {
|
|
@@ -159,7 +155,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
159
155
|
serializedRecord as Partial<T>,
|
|
160
156
|
this.statementConfigFactory.getStatementConfig(config)
|
|
161
157
|
);
|
|
162
|
-
await this.dbDriver.runDml(generateInsert, this.
|
|
158
|
+
await this.dbDriver.runDml(generateInsert, this.transactionForDriver());
|
|
163
159
|
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
164
160
|
return recordCopy as T;
|
|
165
161
|
}
|
|
@@ -202,7 +198,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
202
198
|
qb,
|
|
203
199
|
this.statementConfigFactory.getStatementConfig(config)
|
|
204
200
|
);
|
|
205
|
-
const recordUpdateCount = await this.dbDriver.runDml(generateUpdate, this.
|
|
201
|
+
const recordUpdateCount = await this.dbDriver.runDml(generateUpdate, this.transactionForDriver());
|
|
206
202
|
await this.tableWatcherRunner.runAfterUpdateTableWatchers(table, recordUpdateCount, recordCopy, qb);
|
|
207
203
|
return recordUpdateCount;
|
|
208
204
|
}
|
|
@@ -219,7 +215,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
219
215
|
* or the record no longer exists — a concurrently deleted record wins).
|
|
220
216
|
*/
|
|
221
217
|
async updateArrayMembership<T extends R>(table: Table<T>, update: ArrayMembershipUpdate): Promise<number> {
|
|
222
|
-
if (!this.currentTransaction) {
|
|
218
|
+
if (!this.transactionContextFactory.getTransactionContext().currentTransaction) {
|
|
223
219
|
const db = this.newSelfWrapDb();
|
|
224
220
|
return await db.runTransaction(async () => await db.updateArrayMembership(table, update));
|
|
225
221
|
}
|
|
@@ -261,7 +257,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
261
257
|
* Self-wraps in a transaction when called outside one. Plain-JSON columns only.
|
|
262
258
|
*/
|
|
263
259
|
async updatePreserving<T extends R>(table: Table<T>, record: Partial<T>, preserve: PreservedPath[]): Promise<number> {
|
|
264
|
-
if (!this.currentTransaction) {
|
|
260
|
+
if (!this.transactionContextFactory.getTransactionContext().currentTransaction) {
|
|
265
261
|
const db = this.newSelfWrapDb();
|
|
266
262
|
return await db.runTransaction(async () => await db.updatePreserving(table, record, preserve));
|
|
267
263
|
}
|
|
@@ -316,7 +312,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
316
312
|
new StatementFactory<T>().delete(table.name, deleteQb, this.statementConfigFactory.getStatementConfig(config));
|
|
317
313
|
await this.runColumnBeforeDeletes(table, recordsToDelete);
|
|
318
314
|
await this.tableWatcherRunner.runBeforeDeleteTableWatchers(table, recordsToDelete, qb, deleteQb);
|
|
319
|
-
const recordDeleteCount = await this.dbDriver.runDml(generateDelete, this.
|
|
315
|
+
const recordDeleteCount = await this.dbDriver.runDml(generateDelete, this.transactionForDriver());
|
|
320
316
|
await this.runCascadeDeletions(table, recordsToDelete);
|
|
321
317
|
await this.runColumnReverseCascadeDeletions(table, recordsToDelete);
|
|
322
318
|
await this.tableWatcherRunner.runAfterDeleteTableWatchers(table, recordDeleteCount, recordsToDelete, qb, deleteQb);
|
|
@@ -536,7 +532,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
536
532
|
|
|
537
533
|
const generateQuery = (config: DbDriverQueryStatementConfig) =>
|
|
538
534
|
qb.toSql(this.statementConfigFactory.getStatementConfig(config));
|
|
539
|
-
const serializedRecords = await this.dbDriver.runQuery(generateQuery, this.
|
|
535
|
+
const serializedRecords = await this.dbDriver.runQuery(generateQuery, this.transactionForDriver());
|
|
540
536
|
const recordSerializer = new RecordSerializer(table);
|
|
541
537
|
const records = await Promise.all(
|
|
542
538
|
serializedRecords.map(async (serializedRecord) => recordSerializer.deserialize(serializedRecord))
|
|
@@ -579,7 +575,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
579
575
|
await this.addColumnQueries(table, qb);
|
|
580
576
|
const generateQuery = (config: DbDriverQueryStatementConfig) =>
|
|
581
577
|
qb.toSql(this.statementConfigFactory.getStatementConfig(config));
|
|
582
|
-
const result = await this.dbDriver.runQuery(generateQuery, this.
|
|
578
|
+
const result = await this.dbDriver.runQuery(generateQuery, this.transactionForDriver());
|
|
583
579
|
return result[0]['count'];
|
|
584
580
|
}
|
|
585
581
|
|
|
@@ -608,13 +604,19 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
608
604
|
/**
|
|
609
605
|
* Run a transaction.
|
|
610
606
|
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
607
|
+
* Db instances are STATELESS with respect to transactions: every operation resolves the
|
|
608
|
+
* ambient transaction (AsyncLocalStorage) at call time, so any Db instance used inside the
|
|
609
|
+
* transaction body — whenever it was constructed — rides the transaction. There is no way
|
|
610
|
+
* to issue an operation outside a transaction from inside its body, and no second session
|
|
611
|
+
* is ever acquired inside one (the historical pool-wedge class is unrepresentable —
|
|
612
|
+
* plans/DB_PERF_PLAN.md P2, in the consumer repo).
|
|
615
613
|
*
|
|
616
614
|
* Note: Nested transactions are not supported; will throw.
|
|
617
615
|
*
|
|
616
|
+
* Note: work spawned inside the body but NOT awaited by it escapes the transaction's
|
|
617
|
+
* lifetime while still holding its context — such work fails loudly on its next db
|
|
618
|
+
* operation (see TransactionContextData.ended). Await everything inside the body, or run
|
|
619
|
+
* it outside the transaction.
|
|
618
620
|
*
|
|
619
621
|
* Example:
|
|
620
622
|
*
|
|
@@ -630,8 +632,8 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
630
632
|
* ```
|
|
631
633
|
*/
|
|
632
634
|
async runTransaction<T>(fn: () => Promise<T>): Promise<T> {
|
|
633
|
-
if (this.currentTransaction) {
|
|
634
|
-
throw new Error(`Nested transactions are not supported. A transaction is already running
|
|
635
|
+
if (this.transactionContextFactory.getTransactionContext().currentTransaction) {
|
|
636
|
+
throw new Error(`Nested transactions are not supported. A transaction is already running in this context.`);
|
|
635
637
|
}
|
|
636
638
|
|
|
637
639
|
// Reassigned fresh per driver attempt: drivers may retry `fn` on transient aborts (Spanner's
|
|
@@ -639,19 +641,18 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
639
641
|
// the attempt that actually commits. Only the committed attempt's queue is drained below.
|
|
640
642
|
let postCommitHooks: PostCommitHook[] = [];
|
|
641
643
|
const result = await this.dbDriver.runTransaction(async (transaction) => {
|
|
642
|
-
|
|
644
|
+
// A fresh ambient store per attempt: the transaction, the attempt's post-commit hook
|
|
645
|
+
// queue (shared by every Db instance created inside — see runAfterCommit), and the
|
|
646
|
+
// ended-tombstone below.
|
|
643
647
|
postCommitHooks = [];
|
|
644
|
-
|
|
648
|
+
const contextData: TransactionContextData = { currentTransaction: transaction, postCommitHooks };
|
|
645
649
|
try {
|
|
646
|
-
return await this.transactionContextFactory.runInContext(
|
|
647
|
-
// Seed the post-commit hook queue on the ambient context so every Db instance created
|
|
648
|
-
// inside this transaction shares it (see runAfterCommit).
|
|
649
|
-
this.transactionContextFactory.getTransactionContext().postCommitHooks = postCommitHooks;
|
|
650
|
-
const result = await fn();
|
|
651
|
-
return result;
|
|
652
|
-
});
|
|
650
|
+
return await this.transactionContextFactory.runInContext(contextData, fn);
|
|
653
651
|
} finally {
|
|
654
|
-
|
|
652
|
+
// Tombstone the store: detached work spawned inside the body still holds it by
|
|
653
|
+
// reference — the flag turns its next db operation into a loud error instead of a
|
|
654
|
+
// silent op on a finished transaction (see transactionForDriver).
|
|
655
|
+
contextData.ended = true;
|
|
655
656
|
}
|
|
656
657
|
});
|
|
657
658
|
|
|
@@ -714,6 +715,24 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
714
715
|
return new Db<R>(this.dbDriver, this.getTable, this.transactionContextFactory, this.runAsSystem);
|
|
715
716
|
}
|
|
716
717
|
|
|
718
|
+
/**
|
|
719
|
+
* The transaction every driver call rides, resolved from the ambient context AT CALL TIME
|
|
720
|
+
* (statelessness is the safety property: operations inside a transaction body always ride
|
|
721
|
+
* it; operations outside always use the pool). The one remaining failure shape — work that
|
|
722
|
+
* escaped a finished transaction's body while holding its context — throws here by name;
|
|
723
|
+
* handing the driver an ended transaction would fail anyway, with a far worse error.
|
|
724
|
+
*/
|
|
725
|
+
private transactionForDriver(): any {
|
|
726
|
+
const context = this.transactionContextFactory.getTransactionContext();
|
|
727
|
+
if (context.ended) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
`Db operation issued in the context of a transaction that already ended: work spawned inside a runTransaction body (and not awaited by it) survived the transaction. Await the work inside the body, or run it outside the transaction.`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return context.currentTransaction;
|
|
734
|
+
}
|
|
735
|
+
|
|
717
736
|
// Utility: simple chunker
|
|
718
737
|
private chunk<T>(arr: T[], size: number): T[][] {
|
|
719
738
|
if (size <= 0) {
|
|
@@ -16,9 +16,16 @@ export interface TransactionContextData {
|
|
|
16
16
|
* `Db.runTransaction` once the driver reports the commit durable.
|
|
17
17
|
*/
|
|
18
18
|
postCommitHooks?: PostCommitHook[];
|
|
19
|
+
/**
|
|
20
|
+
* Set by `Db.runTransaction` when the transaction completes. Work spawned inside the
|
|
21
|
+
* transaction body but not awaited by it still holds this store by reference (the async
|
|
22
|
+
* context propagates) — the flag turns any later db operation from that escaped context
|
|
23
|
+
* into a loud, named error instead of silently handing the driver a finished transaction.
|
|
24
|
+
*/
|
|
25
|
+
ended?: boolean;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export interface DefaultTransactionContextFactory extends Loadable {
|
|
22
29
|
getTransactionContext(): TransactionContextData;
|
|
23
|
-
runInContext<T>(
|
|
30
|
+
runInContext<T>(context: TransactionContextData, fn: () => Promise<T>): Promise<T>;
|
|
24
31
|
}
|