@duraflows/kysely 5.0.0 → 5.2.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/README.md +30 -14
- package/dist/cjs/index.js +20 -5
- package/dist/cjs/kysely-transaction-context.d.ts +43 -2
- package/dist/cjs/kysely-transaction-context.js +73 -14
- package/dist/cjs/kysely-transaction-runner.d.ts +2 -1
- package/dist/cjs/kysely-transaction-runner.js +9 -7
- package/dist/index.js +21 -6
- package/dist/kysely-transaction-context.d.ts +43 -2
- package/dist/kysely-transaction-context.js +70 -13
- package/dist/kysely-transaction-runner.d.ts +2 -1
- package/dist/kysely-transaction-runner.js +10 -8
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -49,27 +49,42 @@ The primary use case for this package is running Duraflows workflow transitions
|
|
|
49
49
|
```ts
|
|
50
50
|
import { KyselyTransactionContext } from "@duraflows/kysely";
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
workflowInstanceUuid: instanceUuid,
|
|
61
|
-
eventName: "PaymentReceived",
|
|
62
|
-
});
|
|
52
|
+
await KyselyTransactionContext.transaction(db, async (trx) => {
|
|
53
|
+
// Your own writes -- uses trx
|
|
54
|
+
await trx.insertInto("orders").values({ id: "ORD-1", status: "paid" }).execute();
|
|
55
|
+
|
|
56
|
+
// Duraflows writes -- also uses trx (same transaction)
|
|
57
|
+
await runtime.triggerEvent({
|
|
58
|
+
workflowInstanceUuid: instanceUuid,
|
|
59
|
+
eventName: "PaymentReceived",
|
|
63
60
|
});
|
|
64
61
|
});
|
|
65
|
-
// Both commit or both
|
|
62
|
+
// Both commit or both roll back. Observers fire after COMMIT, never after a rollback.
|
|
66
63
|
```
|
|
67
64
|
|
|
65
|
+
Each duraflows call inside runs in its own savepoint. If one fails, its writes are rolled back, and your transaction can carry on if you catch the error. Calling `transaction()` inside one that is already active joins it as a savepoint instead of opening a second transaction, so composed service methods stay atomic.
|
|
66
|
+
|
|
67
|
+
"Never after a rollback" has one caveat: if a statement failed and you swallowed its error, PostgreSQL silently turns kysely's `COMMIT` into a rollback, and kysely can't see that. Observers then fire for writes that were never persisted. Don't swallow SQL errors inside a transaction: let them propagate, or run the statement in a nested `KyselyTransactionContext.transaction()` and catch its rejection, which rolls back only that savepoint.
|
|
68
|
+
|
|
69
|
+
The runner's `lockTimeoutMs` / `statementTimeoutMs` don't apply inside `transaction()` or a seeded `run()`: you own those transaction settings, and nested duraflows calls run under them.
|
|
70
|
+
|
|
71
|
+
If you already open the transaction yourself, you can still seed the context:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await db.transaction().execute((trx) =>
|
|
75
|
+
KyselyTransactionContext.run(db, trx, async () => {
|
|
76
|
+
await runtime.triggerEvent({ workflowInstanceUuid: instanceUuid, eventName: "PaymentReceived" });
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Observers then fire when the seeded callback resolves, which is **before** kysely commits, and duraflows calls they make join your still-open transaction. Prefer `transaction()` when observers must never see a rolled-back write. Don't run duraflows calls concurrently (`Promise.all`) on one transaction.
|
|
82
|
+
|
|
68
83
|
### How It Works
|
|
69
84
|
|
|
70
85
|
`KyselyTransactionContext` uses Node.js `AsyncLocalStorage` to propagate the active Kysely transaction through the async call chain. When Duraflows' internal `WorkflowTransactionRunner.runInTransaction()` is called, it checks for an existing transaction in the context:
|
|
71
86
|
|
|
72
|
-
- **Found:**
|
|
87
|
+
- **Found:** Runs the call in a savepoint on it (`SAVEPOINT` … `RELEASE`, or `ROLLBACK TO` on failure)
|
|
73
88
|
- **Not found:** Starts a new Kysely transaction and seeds the context
|
|
74
89
|
|
|
75
90
|
This re-entrancy contract means your application code controls the transaction boundary.
|
|
@@ -127,7 +142,7 @@ Both are applied transaction-locally via `set_config(name, value, true)` — the
|
|
|
127
142
|
|
|
128
143
|
### `kyselyWorkflowProvidersFromTransaction(trx): WorkflowPersistenceProvider`
|
|
129
144
|
|
|
130
|
-
Convenience factory for short-lived runtimes pre-bound to an existing transaction.
|
|
145
|
+
Convenience factory for short-lived runtimes pre-bound to an existing transaction. Each workflow call runs in a savepoint on `trx`, so a failed call leaves no partial writes behind even if you catch its error. Observers fire when the call resolves, before your transaction commits, and duraflows calls they make join `trx`. For strictly post-commit observers, use `KyselyTransactionContext.transaction(db, …)` with the long-lived providers instead.
|
|
131
146
|
|
|
132
147
|
### `KyselyTransactionContext`
|
|
133
148
|
|
|
@@ -135,6 +150,7 @@ The context is scoped per Kysely instance:
|
|
|
135
150
|
|
|
136
151
|
- `getTransaction(db)` -- Returns the active `Transaction<WorkflowDatabase>` for the given `db` instance, or `undefined`
|
|
137
152
|
- `run(db, trx, callback)` -- Executes `callback` with `trx` as the active transaction context for the given `db` instance
|
|
153
|
+
- `transaction(db, callback)` -- Runs `callback(trx)` in a new transaction on `db` with `trx` active for workflow calls; observers fire after it commits. Inside an already-active transaction for `db`, it joins that transaction as a savepoint instead
|
|
138
154
|
|
|
139
155
|
## Documentation
|
|
140
156
|
|
package/dist/cjs/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.KyselyWorkflowDefinitionStore = exports.KyselyWorkflowHistoryStore = exports.KyselyWorkflowInstanceStore = exports.KyselyTransactionRunner = exports.KyselyTransactionContext = void 0;
|
|
4
4
|
exports.kyselyWorkflowProviders = kyselyWorkflowProviders;
|
|
5
5
|
exports.kyselyWorkflowProvidersFromTransaction = kyselyWorkflowProvidersFromTransaction;
|
|
6
|
+
const core_1 = require("@duraflows/core");
|
|
6
7
|
const kysely_instance_store_js_1 = require("./kysely-instance-store.js");
|
|
7
8
|
const kysely_history_store_js_1 = require("./kysely-history-store.js");
|
|
8
9
|
const kysely_definition_store_js_1 = require("./kysely-definition-store.js");
|
|
@@ -41,14 +42,28 @@ function kyselyWorkflowProviders(db, options = {}) {
|
|
|
41
42
|
*/
|
|
42
43
|
function kyselyWorkflowProvidersFromTransaction(trx) {
|
|
43
44
|
const narrowed = trx;
|
|
45
|
+
const boundTrx = trx;
|
|
44
46
|
const transactionRunner = {
|
|
45
47
|
async runInTransaction(callback) {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// Scoped to the bound trx; an unrelated ambient transaction from another
|
|
49
|
+
// provider must never supersede it.
|
|
50
|
+
const scope = kysely_transaction_context_js_1.kyselyTransactionScopes.current(narrowed);
|
|
51
|
+
if (scope) {
|
|
52
|
+
return kysely_transaction_context_js_1.kyselyTransactionScopes.runInSavepoint(narrowed, scope, callback, (sql) => (0, kysely_transaction_context_js_1.executeRawStatement)(scope.connection, sql));
|
|
50
53
|
}
|
|
51
|
-
|
|
54
|
+
// The caller owns `trx`, so even this outermost call is nested in their
|
|
55
|
+
// transaction: run it in a savepoint so a failure leaves no partial
|
|
56
|
+
// writes behind, and deliver its observers once it resolves. `trx` is
|
|
57
|
+
// still open then, so the observers run inside the root scope: a
|
|
58
|
+
// duraflows call they make joins `trx` as a savepoint, and the
|
|
59
|
+
// callbacks it queues drain in the same pass.
|
|
60
|
+
const root = kysely_transaction_context_js_1.kyselyTransactionScopes.createRoot(boundTrx);
|
|
61
|
+
const result = await kysely_transaction_context_js_1.kyselyTransactionScopes.run(narrowed, root, () => kysely_transaction_context_js_1.kyselyTransactionScopes.runInSavepoint(narrowed, root, callback, (sql) => (0, kysely_transaction_context_js_1.executeRawStatement)(boundTrx, sql)));
|
|
62
|
+
await kysely_transaction_context_js_1.kyselyTransactionScopes.run(narrowed, root, () => (0, core_1.runAfterCommitCallbacks)(root.callbacks));
|
|
63
|
+
return result;
|
|
64
|
+
},
|
|
65
|
+
afterCommit(callback) {
|
|
66
|
+
kysely_transaction_context_js_1.kyselyTransactionScopes.afterCommit(narrowed, callback);
|
|
52
67
|
},
|
|
53
68
|
};
|
|
54
69
|
const instanceStore = new kysely_instance_store_js_1.KyselyWorkflowInstanceStore(narrowed);
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import type { Kysely, Transaction } from "kysely";
|
|
2
|
+
import { ScopedTransactionContext } from "@duraflows/core";
|
|
2
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
|
+
type WorkflowTransaction = Transaction<WorkflowDatabase>;
|
|
5
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
6
|
+
export declare const kyselyTransactionScopes: ScopedTransactionContext<Kysely<WorkflowDatabase>, WorkflowTransaction>;
|
|
7
|
+
/**
|
|
8
|
+
* @internal Runs one fixed SQL statement (a savepoint command) on `trx`.
|
|
9
|
+
*
|
|
10
|
+
* `Transaction` has no savepoint API in kysely 0.29 (only `ControlledTransaction`
|
|
11
|
+
* does), and this package imports kysely for types only, since kysely is
|
|
12
|
+
* ESM-only and a runtime import would break the CommonJS build. So the
|
|
13
|
+
* statement is handed to `executeQuery` as a compiled query built by hand,
|
|
14
|
+
* shaped exactly like `CompiledQuery.raw(sql)`. `sql` is always an internally
|
|
15
|
+
* generated `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` statement, never user input.
|
|
16
|
+
*/
|
|
17
|
+
export declare function executeRawStatement(trx: WorkflowTransaction, sql: string): Promise<unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
20
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
21
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
22
|
+
* rolls back and the callbacks are dropped.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runOwnedKyselyTransaction<T>(db: Kysely<WorkflowDatabase>, setup: (trx: WorkflowTransaction) => Promise<void>, callback: (trx: WorkflowTransaction) => Promise<T>): Promise<T>;
|
|
3
25
|
export declare const KyselyTransactionContext: {
|
|
4
|
-
getTransaction(owner: Kysely<WorkflowDatabase>):
|
|
26
|
+
getTransaction(owner: Kysely<WorkflowDatabase>): WorkflowTransaction | undefined;
|
|
5
27
|
/**
|
|
6
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
28
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
29
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
30
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
31
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
32
|
+
* for delivery strictly after COMMIT.
|
|
7
33
|
*
|
|
8
34
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
9
35
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -12,5 +38,20 @@ export declare const KyselyTransactionContext: {
|
|
|
12
38
|
* workflow tables.
|
|
13
39
|
*/
|
|
14
40
|
run<T, DB extends WorkflowDatabase = WorkflowDatabase>(owner: Kysely<WorkflowDatabase>, trx: Transaction<DB>, callback: () => T): T;
|
|
41
|
+
/**
|
|
42
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
43
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
44
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
45
|
+
* rolls back.
|
|
46
|
+
*
|
|
47
|
+
* Called while a transaction for `db` is already active (an outer
|
|
48
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
49
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
50
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
51
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
52
|
+
* dropped).
|
|
53
|
+
*/
|
|
54
|
+
transaction<T, DB extends WorkflowDatabase = WorkflowDatabase>(db: Kysely<DB>, callback: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
|
15
55
|
};
|
|
56
|
+
export {};
|
|
16
57
|
//# sourceMappingURL=kysely-transaction-context.d.ts.map
|
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KyselyTransactionContext = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
exports.KyselyTransactionContext = exports.kyselyTransactionScopes = void 0;
|
|
4
|
+
exports.executeRawStatement = executeRawStatement;
|
|
5
|
+
exports.runOwnedKyselyTransaction = runOwnedKyselyTransaction;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const core_1 = require("@duraflows/core");
|
|
8
|
+
// One context per Kysely instance, so a transaction on db A is invisible to
|
|
9
|
+
// stores and runners bound to db B.
|
|
10
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
11
|
+
exports.kyselyTransactionScopes = new core_1.ScopedTransactionContext();
|
|
12
|
+
/**
|
|
13
|
+
* @internal Runs one fixed SQL statement (a savepoint command) on `trx`.
|
|
14
|
+
*
|
|
15
|
+
* `Transaction` has no savepoint API in kysely 0.29 (only `ControlledTransaction`
|
|
16
|
+
* does), and this package imports kysely for types only, since kysely is
|
|
17
|
+
* ESM-only and a runtime import would break the CommonJS build. So the
|
|
18
|
+
* statement is handed to `executeQuery` as a compiled query built by hand,
|
|
19
|
+
* shaped exactly like `CompiledQuery.raw(sql)`. `sql` is always an internally
|
|
20
|
+
* generated `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` statement, never user input.
|
|
21
|
+
*/
|
|
22
|
+
function executeRawStatement(trx, sql) {
|
|
23
|
+
const query = {
|
|
24
|
+
sql,
|
|
25
|
+
parameters: [],
|
|
26
|
+
query: { kind: "RawNode", sqlFragments: [sql], parameters: [] },
|
|
27
|
+
queryId: { queryId: `duraflows_${(0, node_crypto_1.randomUUID)()}` },
|
|
28
|
+
};
|
|
29
|
+
return trx.executeQuery(query);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
33
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
34
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
35
|
+
* rolls back and the callbacks are dropped.
|
|
36
|
+
*/
|
|
37
|
+
async function runOwnedKyselyTransaction(db, setup, callback) {
|
|
38
|
+
let root;
|
|
39
|
+
const result = await db.transaction().execute(async (trx) => {
|
|
40
|
+
await setup(trx);
|
|
41
|
+
const scope = exports.kyselyTransactionScopes.createRoot(trx);
|
|
42
|
+
root = scope;
|
|
43
|
+
return exports.kyselyTransactionScopes.run(db, scope, () => callback(trx));
|
|
44
|
+
});
|
|
45
|
+
if (root) {
|
|
46
|
+
await (0, core_1.runAfterCommitCallbacks)(root.callbacks);
|
|
13
47
|
}
|
|
14
|
-
return
|
|
48
|
+
return result;
|
|
15
49
|
}
|
|
16
50
|
exports.KyselyTransactionContext = {
|
|
17
51
|
getTransaction(owner) {
|
|
18
|
-
return
|
|
52
|
+
return exports.kyselyTransactionScopes.current(owner)?.connection;
|
|
19
53
|
},
|
|
20
54
|
/**
|
|
21
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
55
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
56
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
57
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
58
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
59
|
+
* for delivery strictly after COMMIT.
|
|
22
60
|
*
|
|
23
61
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
24
62
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -27,7 +65,28 @@ exports.KyselyTransactionContext = {
|
|
|
27
65
|
* workflow tables.
|
|
28
66
|
*/
|
|
29
67
|
run(owner, trx, callback) {
|
|
30
|
-
return
|
|
68
|
+
return exports.kyselyTransactionScopes.runSeeded(owner, trx, callback);
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
72
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
73
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
74
|
+
* rolls back.
|
|
75
|
+
*
|
|
76
|
+
* Called while a transaction for `db` is already active (an outer
|
|
77
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
78
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
79
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
80
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
81
|
+
* dropped).
|
|
82
|
+
*/
|
|
83
|
+
transaction(db, callback) {
|
|
84
|
+
const owner = db;
|
|
85
|
+
const scope = exports.kyselyTransactionScopes.current(owner);
|
|
86
|
+
if (scope) {
|
|
87
|
+
return exports.kyselyTransactionScopes.runInSavepoint(owner, scope, () => callback(scope.connection), (sql) => executeRawStatement(scope.connection, sql));
|
|
88
|
+
}
|
|
89
|
+
return runOwnedKyselyTransaction(owner, async () => { }, (trx) => callback(trx));
|
|
31
90
|
},
|
|
32
91
|
};
|
|
33
92
|
//# sourceMappingURL=kysely-transaction-context.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Kysely } from "kysely";
|
|
2
|
-
import type { WorkflowTransactionRunner } from "@duraflows/core";
|
|
2
|
+
import type { AfterCommitCallback, WorkflowTransactionRunner } from "@duraflows/core";
|
|
3
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
4
|
/**
|
|
5
5
|
* Transaction-scoped PostgreSQL timeouts.
|
|
@@ -31,6 +31,7 @@ export declare class KyselyTransactionRunner implements WorkflowTransactionRunne
|
|
|
31
31
|
private readonly timeoutSettings;
|
|
32
32
|
constructor(db: Kysely<WorkflowDatabase>, options?: KyselyTransactionRunnerOptions);
|
|
33
33
|
runInTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
34
|
+
afterCommit(callback: AfterCommitCallback): void;
|
|
34
35
|
/**
|
|
35
36
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
|
36
37
|
* they are reverted on COMMIT/ROLLBACK and never leak to other users of the
|
|
@@ -36,14 +36,16 @@ class KyselyTransactionRunner {
|
|
|
36
36
|
this.timeoutSettings = buildTimeoutSettings(options);
|
|
37
37
|
}
|
|
38
38
|
async runInTransaction(callback) {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
39
|
+
const scope = kysely_transaction_context_js_1.kyselyTransactionScopes.current(this.db);
|
|
40
|
+
if (scope) {
|
|
41
|
+
// Nested: a savepoint on the outer transaction, so a failure rolls back
|
|
42
|
+
// only this call. The outer transaction's timeouts stay in force.
|
|
43
|
+
return kysely_transaction_context_js_1.kyselyTransactionScopes.runInSavepoint(this.db, scope, callback, (sql) => (0, kysely_transaction_context_js_1.executeRawStatement)(scope.connection, sql));
|
|
42
44
|
}
|
|
43
|
-
return this.db
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
return (0, kysely_transaction_context_js_1.runOwnedKyselyTransaction)(this.db, (trx) => this.applyTimeouts(trx), () => callback());
|
|
46
|
+
}
|
|
47
|
+
afterCommit(callback) {
|
|
48
|
+
kysely_transaction_context_js_1.kyselyTransactionScopes.afterCommit(this.db, callback);
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { runAfterCommitCallbacks } from "@duraflows/core";
|
|
1
2
|
import { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
|
|
2
3
|
import { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
|
|
3
4
|
import { KyselyWorkflowDefinitionStore } from "./kysely-definition-store.js";
|
|
4
5
|
import { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
|
|
5
|
-
import {
|
|
6
|
+
import { executeRawStatement, kyselyTransactionScopes } from "./kysely-transaction-context.js";
|
|
6
7
|
export { KyselyTransactionContext } from "./kysely-transaction-context.js";
|
|
7
8
|
export { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
|
|
8
9
|
export { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
|
|
@@ -31,14 +32,28 @@ export function kyselyWorkflowProviders(db, options = {}) {
|
|
|
31
32
|
*/
|
|
32
33
|
export function kyselyWorkflowProvidersFromTransaction(trx) {
|
|
33
34
|
const narrowed = trx;
|
|
35
|
+
const boundTrx = trx;
|
|
34
36
|
const transactionRunner = {
|
|
35
37
|
async runInTransaction(callback) {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Scoped to the bound trx; an unrelated ambient transaction from another
|
|
39
|
+
// provider must never supersede it.
|
|
40
|
+
const scope = kyselyTransactionScopes.current(narrowed);
|
|
41
|
+
if (scope) {
|
|
42
|
+
return kyselyTransactionScopes.runInSavepoint(narrowed, scope, callback, (sql) => executeRawStatement(scope.connection, sql));
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
// The caller owns `trx`, so even this outermost call is nested in their
|
|
45
|
+
// transaction: run it in a savepoint so a failure leaves no partial
|
|
46
|
+
// writes behind, and deliver its observers once it resolves. `trx` is
|
|
47
|
+
// still open then, so the observers run inside the root scope: a
|
|
48
|
+
// duraflows call they make joins `trx` as a savepoint, and the
|
|
49
|
+
// callbacks it queues drain in the same pass.
|
|
50
|
+
const root = kyselyTransactionScopes.createRoot(boundTrx);
|
|
51
|
+
const result = await kyselyTransactionScopes.run(narrowed, root, () => kyselyTransactionScopes.runInSavepoint(narrowed, root, callback, (sql) => executeRawStatement(boundTrx, sql)));
|
|
52
|
+
await kyselyTransactionScopes.run(narrowed, root, () => runAfterCommitCallbacks(root.callbacks));
|
|
53
|
+
return result;
|
|
54
|
+
},
|
|
55
|
+
afterCommit(callback) {
|
|
56
|
+
kyselyTransactionScopes.afterCommit(narrowed, callback);
|
|
42
57
|
},
|
|
43
58
|
};
|
|
44
59
|
const instanceStore = new KyselyWorkflowInstanceStore(narrowed);
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import type { Kysely, Transaction } from "kysely";
|
|
2
|
+
import { ScopedTransactionContext } from "@duraflows/core";
|
|
2
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
|
+
type WorkflowTransaction = Transaction<WorkflowDatabase>;
|
|
5
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
6
|
+
export declare const kyselyTransactionScopes: ScopedTransactionContext<Kysely<WorkflowDatabase>, WorkflowTransaction>;
|
|
7
|
+
/**
|
|
8
|
+
* @internal Runs one fixed SQL statement (a savepoint command) on `trx`.
|
|
9
|
+
*
|
|
10
|
+
* `Transaction` has no savepoint API in kysely 0.29 (only `ControlledTransaction`
|
|
11
|
+
* does), and this package imports kysely for types only, since kysely is
|
|
12
|
+
* ESM-only and a runtime import would break the CommonJS build. So the
|
|
13
|
+
* statement is handed to `executeQuery` as a compiled query built by hand,
|
|
14
|
+
* shaped exactly like `CompiledQuery.raw(sql)`. `sql` is always an internally
|
|
15
|
+
* generated `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` statement, never user input.
|
|
16
|
+
*/
|
|
17
|
+
export declare function executeRawStatement(trx: WorkflowTransaction, sql: string): Promise<unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
20
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
21
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
22
|
+
* rolls back and the callbacks are dropped.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runOwnedKyselyTransaction<T>(db: Kysely<WorkflowDatabase>, setup: (trx: WorkflowTransaction) => Promise<void>, callback: (trx: WorkflowTransaction) => Promise<T>): Promise<T>;
|
|
3
25
|
export declare const KyselyTransactionContext: {
|
|
4
|
-
getTransaction(owner: Kysely<WorkflowDatabase>):
|
|
26
|
+
getTransaction(owner: Kysely<WorkflowDatabase>): WorkflowTransaction | undefined;
|
|
5
27
|
/**
|
|
6
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
28
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
29
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
30
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
31
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
32
|
+
* for delivery strictly after COMMIT.
|
|
7
33
|
*
|
|
8
34
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
9
35
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -12,5 +38,20 @@ export declare const KyselyTransactionContext: {
|
|
|
12
38
|
* workflow tables.
|
|
13
39
|
*/
|
|
14
40
|
run<T, DB extends WorkflowDatabase = WorkflowDatabase>(owner: Kysely<WorkflowDatabase>, trx: Transaction<DB>, callback: () => T): T;
|
|
41
|
+
/**
|
|
42
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
43
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
44
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
45
|
+
* rolls back.
|
|
46
|
+
*
|
|
47
|
+
* Called while a transaction for `db` is already active (an outer
|
|
48
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
49
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
50
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
51
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
52
|
+
* dropped).
|
|
53
|
+
*/
|
|
54
|
+
transaction<T, DB extends WorkflowDatabase = WorkflowDatabase>(db: Kysely<DB>, callback: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
|
15
55
|
};
|
|
56
|
+
export {};
|
|
16
57
|
//# sourceMappingURL=kysely-transaction-context.d.ts.map
|
|
@@ -1,21 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// transaction on db A is invisible to
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ScopedTransactionContext, runAfterCommitCallbacks } from "@duraflows/core";
|
|
3
|
+
// One context per Kysely instance, so a transaction on db A is invisible to
|
|
4
|
+
// stores and runners bound to db B.
|
|
5
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
6
|
+
export const kyselyTransactionScopes = new ScopedTransactionContext();
|
|
7
|
+
/**
|
|
8
|
+
* @internal Runs one fixed SQL statement (a savepoint command) on `trx`.
|
|
9
|
+
*
|
|
10
|
+
* `Transaction` has no savepoint API in kysely 0.29 (only `ControlledTransaction`
|
|
11
|
+
* does), and this package imports kysely for types only, since kysely is
|
|
12
|
+
* ESM-only and a runtime import would break the CommonJS build. So the
|
|
13
|
+
* statement is handed to `executeQuery` as a compiled query built by hand,
|
|
14
|
+
* shaped exactly like `CompiledQuery.raw(sql)`. `sql` is always an internally
|
|
15
|
+
* generated `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` statement, never user input.
|
|
16
|
+
*/
|
|
17
|
+
export function executeRawStatement(trx, sql) {
|
|
18
|
+
const query = {
|
|
19
|
+
sql,
|
|
20
|
+
parameters: [],
|
|
21
|
+
query: { kind: "RawNode", sqlFragments: [sql], parameters: [] },
|
|
22
|
+
queryId: { queryId: `duraflows_${randomUUID()}` },
|
|
23
|
+
};
|
|
24
|
+
return trx.executeQuery(query);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
28
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
29
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
30
|
+
* rolls back and the callbacks are dropped.
|
|
31
|
+
*/
|
|
32
|
+
export async function runOwnedKyselyTransaction(db, setup, callback) {
|
|
33
|
+
let root;
|
|
34
|
+
const result = await db.transaction().execute(async (trx) => {
|
|
35
|
+
await setup(trx);
|
|
36
|
+
const scope = kyselyTransactionScopes.createRoot(trx);
|
|
37
|
+
root = scope;
|
|
38
|
+
return kyselyTransactionScopes.run(db, scope, () => callback(trx));
|
|
39
|
+
});
|
|
40
|
+
if (root) {
|
|
41
|
+
await runAfterCommitCallbacks(root.callbacks);
|
|
10
42
|
}
|
|
11
|
-
return
|
|
43
|
+
return result;
|
|
12
44
|
}
|
|
13
45
|
export const KyselyTransactionContext = {
|
|
14
46
|
getTransaction(owner) {
|
|
15
|
-
return
|
|
47
|
+
return kyselyTransactionScopes.current(owner)?.connection;
|
|
16
48
|
},
|
|
17
49
|
/**
|
|
18
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
50
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
51
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
52
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
53
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
54
|
+
* for delivery strictly after COMMIT.
|
|
19
55
|
*
|
|
20
56
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
21
57
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -24,7 +60,28 @@ export const KyselyTransactionContext = {
|
|
|
24
60
|
* workflow tables.
|
|
25
61
|
*/
|
|
26
62
|
run(owner, trx, callback) {
|
|
27
|
-
return
|
|
63
|
+
return kyselyTransactionScopes.runSeeded(owner, trx, callback);
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
67
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
68
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
69
|
+
* rolls back.
|
|
70
|
+
*
|
|
71
|
+
* Called while a transaction for `db` is already active (an outer
|
|
72
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
73
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
74
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
75
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
76
|
+
* dropped).
|
|
77
|
+
*/
|
|
78
|
+
transaction(db, callback) {
|
|
79
|
+
const owner = db;
|
|
80
|
+
const scope = kyselyTransactionScopes.current(owner);
|
|
81
|
+
if (scope) {
|
|
82
|
+
return kyselyTransactionScopes.runInSavepoint(owner, scope, () => callback(scope.connection), (sql) => executeRawStatement(scope.connection, sql));
|
|
83
|
+
}
|
|
84
|
+
return runOwnedKyselyTransaction(owner, async () => { }, (trx) => callback(trx));
|
|
28
85
|
},
|
|
29
86
|
};
|
|
30
87
|
//# sourceMappingURL=kysely-transaction-context.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Kysely } from "kysely";
|
|
2
|
-
import type { WorkflowTransactionRunner } from "@duraflows/core";
|
|
2
|
+
import type { AfterCommitCallback, WorkflowTransactionRunner } from "@duraflows/core";
|
|
3
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
4
|
/**
|
|
5
5
|
* Transaction-scoped PostgreSQL timeouts.
|
|
@@ -31,6 +31,7 @@ export declare class KyselyTransactionRunner implements WorkflowTransactionRunne
|
|
|
31
31
|
private readonly timeoutSettings;
|
|
32
32
|
constructor(db: Kysely<WorkflowDatabase>, options?: KyselyTransactionRunnerOptions);
|
|
33
33
|
runInTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
34
|
+
afterCommit(callback: AfterCommitCallback): void;
|
|
34
35
|
/**
|
|
35
36
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
|
36
37
|
* they are reverted on COMMIT/ROLLBACK and never leak to other users of the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WorkflowError } from "@duraflows/core";
|
|
2
|
-
import {
|
|
2
|
+
import { executeRawStatement, kyselyTransactionScopes, runOwnedKyselyTransaction, } from "./kysely-transaction-context.js";
|
|
3
3
|
/**
|
|
4
4
|
* Rejects anything that is not a finite, non-negative integer. Called at
|
|
5
5
|
* construction time so an invalid value can never reach the database.
|
|
@@ -33,14 +33,16 @@ export class KyselyTransactionRunner {
|
|
|
33
33
|
this.timeoutSettings = buildTimeoutSettings(options);
|
|
34
34
|
}
|
|
35
35
|
async runInTransaction(callback) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
36
|
+
const scope = kyselyTransactionScopes.current(this.db);
|
|
37
|
+
if (scope) {
|
|
38
|
+
// Nested: a savepoint on the outer transaction, so a failure rolls back
|
|
39
|
+
// only this call. The outer transaction's timeouts stay in force.
|
|
40
|
+
return kyselyTransactionScopes.runInSavepoint(this.db, scope, callback, (sql) => executeRawStatement(scope.connection, sql));
|
|
39
41
|
}
|
|
40
|
-
return this.db
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
return runOwnedKyselyTransaction(this.db, (trx) => this.applyTimeouts(trx), () => callback());
|
|
43
|
+
}
|
|
44
|
+
afterCommit(callback) {
|
|
45
|
+
kyselyTransactionScopes.afterCommit(this.db, callback);
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duraflows/kysely",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "PostgreSQL persistence adapter for duraflows using `kysely`. Integrates with kysely's type-safe query builder and AsyncLocalStorage-based transaction context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"duraflows",
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@duraflows/core": "^5.
|
|
55
|
-
"kysely": "^0.29.
|
|
54
|
+
"@duraflows/core": "^5.2.0",
|
|
55
|
+
"kysely": "^0.29.6"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@types/pg": "^8.
|
|
59
|
-
"kysely": "^0.29.
|
|
58
|
+
"@types/pg": "^8.23.1",
|
|
59
|
+
"kysely": "^0.29.6",
|
|
60
60
|
"pg": "^8.23.0",
|
|
61
|
-
"@duraflows/core": "5.
|
|
62
|
-
"@duraflows/pg": "5.
|
|
61
|
+
"@duraflows/core": "5.2.0",
|
|
62
|
+
"@duraflows/pg": "5.2.0"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "tsc --build"
|