@duraflows/kysely 4.0.1 → 5.0.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 CHANGED
@@ -103,13 +103,27 @@ See the [`@duraflows/pg` README](https://github.com/camcima/duraflows/tree/main/
103
103
 
104
104
  ## API
105
105
 
106
- ### `kyselyWorkflowProviders(db): WorkflowPersistenceProvider`
106
+ ### `kyselyWorkflowProviders(db, options?): WorkflowPersistenceProvider`
107
107
 
108
108
  Factory function that creates all required persistence providers from a `Kysely<WorkflowDatabase>` instance. Returns:
109
109
 
110
110
  - `instanceStore` -- `KyselyWorkflowInstanceStore`
111
111
  - `historyStore` -- `KyselyWorkflowHistoryStore`
112
112
  - `transactionRunner` -- `KyselyTransactionRunner`
113
+ - `definitionStore` -- `KyselyWorkflowDefinitionStore`
114
+
115
+ Options (all optional; omitting them keeps the previous behaviour):
116
+
117
+ - `lockTimeoutMs` -- sets `lock_timeout` for the duration of each transaction. Bounds how long a statement **waits for a row lock**, so a stuck lock holder cannot hang `triggerEvent()` while it keeps a pooled connection checked out. **This is the recommended setting.**
118
+ - `statementTimeoutMs` -- sets `statement_timeout` for the duration of each transaction. Bounds how long **any single statement** may run — including SQL your own commands issue inside the transaction, which is why it is left unset by default: a legitimately slow command statement would be aborted and take the whole transition with it.
119
+
120
+ ```ts
121
+ const persistence = kyselyWorkflowProviders(db, { lockTimeoutMs: 3000 });
122
+ ```
123
+
124
+ Both are applied transaction-locally via `set_config(name, value, true)` — the function form of `SET LOCAL` — so they are reverted on `COMMIT`/`ROLLBACK` and never leak to other users of the shared pool. Values must be non-negative integers in milliseconds (`0` means "no timeout"); anything else throws a `WorkflowError` at construction time.
125
+
126
+ `kyselyWorkflowProvidersFromTransaction()` takes no timeout options: the transaction is owned by the caller, so its settings are the caller's to configure.
113
127
 
114
128
  ### `kyselyWorkflowProvidersFromTransaction(trx): WorkflowPersistenceProvider`
115
129
 
@@ -1,11 +1,19 @@
1
1
  import type { Kysely, Transaction } from "kysely";
2
2
  import type { WorkflowPersistenceProvider } from "@duraflows/core";
3
3
  import type { WorkflowDatabase } from "./kysely-database.js";
4
+ import { type KyselyTransactionRunnerOptions } from "./kysely-transaction-runner.js";
4
5
  export { KyselyTransactionContext } from "./kysely-transaction-context.js";
5
6
  export { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
7
+ export type { KyselyTransactionRunnerOptions } from "./kysely-transaction-runner.js";
6
8
  export { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
7
9
  export { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
8
- export type { WorkflowDatabase, WorkflowInstancesTable, WorkflowHistoryTable } from "./kysely-database.js";
10
+ export { KyselyWorkflowDefinitionStore } from "./kysely-definition-store.js";
11
+ export type { WorkflowDatabase, WorkflowInstancesTable, WorkflowHistoryTable, WorkflowDefinitionsTable, } from "./kysely-database.js";
12
+ /**
13
+ * Options accepted by {@link kyselyWorkflowProviders}. Every field is optional
14
+ * and omitting the argument entirely reproduces the pre-existing behaviour.
15
+ */
16
+ export type KyselyWorkflowProvidersOptions = KyselyTransactionRunnerOptions;
9
17
  /**
10
18
  * Creates long-lived persistence providers from a Kysely instance.
11
19
  *
@@ -14,7 +22,7 @@ export type { WorkflowDatabase, WorkflowInstancesTable, WorkflowHistoryTable } f
14
22
  * only access workflow tables; the `unknown` cast is needed because
15
23
  * Kysely is invariant in its DB type parameter).
16
24
  */
17
- export declare function kyselyWorkflowProviders<DB extends WorkflowDatabase>(db: Kysely<DB>): WorkflowPersistenceProvider;
25
+ export declare function kyselyWorkflowProviders<DB extends WorkflowDatabase>(db: Kysely<DB>, options?: KyselyWorkflowProvidersOptions): WorkflowPersistenceProvider;
18
26
  /**
19
27
  * Creates providers pre-bound to an existing Kysely transaction.
20
28
  *
package/dist/cjs/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KyselyWorkflowHistoryStore = exports.KyselyWorkflowInstanceStore = exports.KyselyTransactionRunner = exports.KyselyTransactionContext = void 0;
3
+ exports.KyselyWorkflowDefinitionStore = exports.KyselyWorkflowHistoryStore = exports.KyselyWorkflowInstanceStore = exports.KyselyTransactionRunner = exports.KyselyTransactionContext = void 0;
4
4
  exports.kyselyWorkflowProviders = kyselyWorkflowProviders;
5
5
  exports.kyselyWorkflowProvidersFromTransaction = kyselyWorkflowProvidersFromTransaction;
6
6
  const kysely_instance_store_js_1 = require("./kysely-instance-store.js");
7
7
  const kysely_history_store_js_1 = require("./kysely-history-store.js");
8
+ const kysely_definition_store_js_1 = require("./kysely-definition-store.js");
8
9
  const kysely_transaction_runner_js_1 = require("./kysely-transaction-runner.js");
9
10
  const kysely_transaction_context_js_1 = require("./kysely-transaction-context.js");
10
11
  var kysely_transaction_context_js_2 = require("./kysely-transaction-context.js");
@@ -15,6 +16,8 @@ var kysely_instance_store_js_2 = require("./kysely-instance-store.js");
15
16
  Object.defineProperty(exports, "KyselyWorkflowInstanceStore", { enumerable: true, get: function () { return kysely_instance_store_js_2.KyselyWorkflowInstanceStore; } });
16
17
  var kysely_history_store_js_2 = require("./kysely-history-store.js");
17
18
  Object.defineProperty(exports, "KyselyWorkflowHistoryStore", { enumerable: true, get: function () { return kysely_history_store_js_2.KyselyWorkflowHistoryStore; } });
19
+ var kysely_definition_store_js_2 = require("./kysely-definition-store.js");
20
+ Object.defineProperty(exports, "KyselyWorkflowDefinitionStore", { enumerable: true, get: function () { return kysely_definition_store_js_2.KyselyWorkflowDefinitionStore; } });
18
21
  /**
19
22
  * Creates long-lived persistence providers from a Kysely instance.
20
23
  *
@@ -23,12 +26,13 @@ Object.defineProperty(exports, "KyselyWorkflowHistoryStore", { enumerable: true,
23
26
  * only access workflow tables; the `unknown` cast is needed because
24
27
  * Kysely is invariant in its DB type parameter).
25
28
  */
26
- function kyselyWorkflowProviders(db) {
29
+ function kyselyWorkflowProviders(db, options = {}) {
27
30
  const narrowed = db;
28
- const transactionRunner = new kysely_transaction_runner_js_1.KyselyTransactionRunner(narrowed);
31
+ const transactionRunner = new kysely_transaction_runner_js_1.KyselyTransactionRunner(narrowed, options);
29
32
  const instanceStore = new kysely_instance_store_js_1.KyselyWorkflowInstanceStore(narrowed);
30
33
  const historyStore = new kysely_history_store_js_1.KyselyWorkflowHistoryStore(narrowed);
31
- return { instanceStore, historyStore, transactionRunner };
34
+ const definitionStore = new kysely_definition_store_js_1.KyselyWorkflowDefinitionStore(narrowed);
35
+ return { instanceStore, historyStore, transactionRunner, definitionStore };
32
36
  }
33
37
  /**
34
38
  * Creates providers pre-bound to an existing Kysely transaction.
@@ -49,6 +53,7 @@ function kyselyWorkflowProvidersFromTransaction(trx) {
49
53
  };
50
54
  const instanceStore = new kysely_instance_store_js_1.KyselyWorkflowInstanceStore(narrowed);
51
55
  const historyStore = new kysely_history_store_js_1.KyselyWorkflowHistoryStore(narrowed);
52
- return { instanceStore, historyStore, transactionRunner };
56
+ const definitionStore = new kysely_definition_store_js_1.KyselyWorkflowDefinitionStore(narrowed);
57
+ return { instanceStore, historyStore, transactionRunner, definitionStore };
53
58
  }
54
59
  //# sourceMappingURL=index.js.map
@@ -17,6 +17,7 @@ export interface WorkflowInstancesTable {
17
17
  workflow_name: string;
18
18
  current_state: string;
19
19
  version: number;
20
+ definition_version: number | null;
20
21
  expires_at: Date | null;
21
22
  last_transition_at: Date;
22
23
  context_json: JsonObjectColumn;
@@ -35,11 +36,20 @@ export interface WorkflowHistoryTable {
35
36
  rejected_by: string | null;
36
37
  command_results_json: JsonArrayColumn;
37
38
  trigger_metadata_json: JsonObjectColumn;
39
+ definition_version: number | null;
38
40
  created_at: Generated<Date>;
39
41
  }
42
+ export interface WorkflowDefinitionsTable {
43
+ workflow_name: string;
44
+ version: number;
45
+ content_hash: string;
46
+ definition_json: JsonObjectColumn;
47
+ registered_at: Generated<Date>;
48
+ }
40
49
  export interface WorkflowDatabase {
41
50
  workflow_instances: WorkflowInstancesTable;
42
51
  workflow_history: WorkflowHistoryTable;
52
+ workflow_definitions: WorkflowDefinitionsTable;
43
53
  }
44
54
  export {};
45
55
  //# sourceMappingURL=kysely-database.d.ts.map
@@ -0,0 +1,17 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { WorkflowDefinitionStore, StoredWorkflowDefinition, WorkflowDefinition } from "@duraflows/core";
3
+ import type { WorkflowDatabase } from "./kysely-database.js";
4
+ export declare class KyselyWorkflowDefinitionStore implements WorkflowDefinitionStore {
5
+ private readonly db;
6
+ constructor(db: Kysely<WorkflowDatabase>);
7
+ private getExecutor;
8
+ ensure(record: {
9
+ workflowName: string;
10
+ version: number;
11
+ contentHash: string;
12
+ definitionJson: WorkflowDefinition;
13
+ }): Promise<StoredWorkflowDefinition>;
14
+ findByNameAndVersion(workflowName: string, version: number): Promise<StoredWorkflowDefinition | null>;
15
+ private mapRow;
16
+ }
17
+ //# sourceMappingURL=kysely-definition-store.d.ts.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KyselyWorkflowDefinitionStore = void 0;
4
+ const core_1 = require("@duraflows/core");
5
+ const kysely_transaction_context_js_1 = require("./kysely-transaction-context.js");
6
+ class KyselyWorkflowDefinitionStore {
7
+ db;
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ getExecutor() {
12
+ return kysely_transaction_context_js_1.KyselyTransactionContext.getTransaction(this.db) ?? this.db;
13
+ }
14
+ async ensure(record) {
15
+ const executor = this.getExecutor();
16
+ await executor
17
+ .insertInto("workflow_definitions")
18
+ .values({
19
+ workflow_name: record.workflowName,
20
+ version: record.version,
21
+ content_hash: record.contentHash,
22
+ definition_json: JSON.stringify(record.definitionJson),
23
+ })
24
+ .onConflict((oc) => oc.columns(["workflow_name", "version"]).doNothing())
25
+ .execute();
26
+ const stored = await this.findByNameAndVersion(record.workflowName, record.version);
27
+ if (!stored) {
28
+ // The row we just ensured must exist; its absence means the statement
29
+ // did not do what the adapter assumes. Fail loudly.
30
+ throw new core_1.WorkflowError(`Failed to ensure workflow definition "${record.workflowName}" version ${record.version}`);
31
+ }
32
+ return stored;
33
+ }
34
+ async findByNameAndVersion(workflowName, version) {
35
+ const executor = this.getExecutor();
36
+ const row = await executor
37
+ .selectFrom("workflow_definitions")
38
+ .selectAll()
39
+ .where("workflow_name", "=", workflowName)
40
+ .where("version", "=", version)
41
+ .executeTakeFirst();
42
+ if (!row)
43
+ return null;
44
+ return this.mapRow(row);
45
+ }
46
+ mapRow(row) {
47
+ return {
48
+ workflowName: row.workflow_name,
49
+ version: row.version,
50
+ contentHash: row.content_hash,
51
+ definitionJson: row.definition_json,
52
+ registeredAt: row.registered_at,
53
+ };
54
+ }
55
+ }
56
+ exports.KyselyWorkflowDefinitionStore = KyselyWorkflowDefinitionStore;
57
+ //# sourceMappingURL=kysely-definition-store.js.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KyselyWorkflowHistoryStore = void 0;
4
+ const core_1 = require("@duraflows/core");
4
5
  const kysely_transaction_context_js_1 = require("./kysely-transaction-context.js");
5
6
  class KyselyWorkflowHistoryStore {
6
7
  db;
@@ -24,9 +25,13 @@ class KyselyWorkflowHistoryStore {
24
25
  rejected_by: entry.rejectedBy ?? null,
25
26
  command_results_json: JSON.stringify(entry.commandResultsJson),
26
27
  trigger_metadata_json: JSON.stringify(entry.triggerMetadata ?? {}),
28
+ definition_version: entry.definitionVersion ?? null,
27
29
  })
28
30
  .returning("uuid")
29
- .executeTakeFirstOrThrow();
31
+ // `INSERT ... RETURNING` always yields a row, so an empty result means the
32
+ // statement did not do what the adapter assumes. Surface that as the
33
+ // library's own error type rather than kysely's `NoResultError`.
34
+ .executeTakeFirstOrThrow(() => new core_1.WorkflowError("Failed to append workflow history: INSERT ... RETURNING uuid returned no row"));
30
35
  return row.uuid;
31
36
  }
32
37
  async findByInstanceUuid(workflowInstanceUuid, options) {
@@ -52,6 +57,8 @@ class KyselyWorkflowHistoryStore {
52
57
  rejectedBy: row.rejected_by ?? undefined,
53
58
  commandResultsJson: row.command_results_json,
54
59
  triggerMetadata: row.trigger_metadata_json,
60
+ definitionVersion: row.definition_version ?? undefined,
61
+ createdAt: row.created_at,
55
62
  }));
56
63
  }
57
64
  }
@@ -20,6 +20,7 @@ class KyselyWorkflowInstanceStore {
20
20
  workflow_name: instance.workflowName,
21
21
  current_state: instance.currentState,
22
22
  version: instance.version,
23
+ definition_version: instance.definitionVersion,
23
24
  expires_at: instance.expiresAt,
24
25
  last_transition_at: instance.lastTransitionAt,
25
26
  context_json: JSON.stringify(instance.context),
@@ -59,6 +60,7 @@ class KyselyWorkflowInstanceStore {
59
60
  .set({
60
61
  current_state: instance.currentState,
61
62
  version: instance.version,
63
+ definition_version: instance.definitionVersion,
62
64
  expires_at: instance.expiresAt,
63
65
  last_transition_at: instance.lastTransitionAt,
64
66
  context_json: JSON.stringify(instance.context),
@@ -95,6 +97,7 @@ class KyselyWorkflowInstanceStore {
95
97
  workflowName: row.workflow_name,
96
98
  currentState: row.current_state,
97
99
  version: row.version,
100
+ definitionVersion: row.definition_version ?? null,
98
101
  expiresAt: row.expires_at,
99
102
  lastTransitionAt: row.last_transition_at,
100
103
  context: row.context_json,
@@ -1,9 +1,47 @@
1
1
  import type { Kysely } from "kysely";
2
2
  import type { WorkflowTransactionRunner } from "@duraflows/core";
3
3
  import type { WorkflowDatabase } from "./kysely-database.js";
4
+ /**
5
+ * Transaction-scoped PostgreSQL timeouts.
6
+ *
7
+ * Both settings are optional and both default to unset: when neither is
8
+ * supplied the runner issues no extra statement at all, leaving the session and
9
+ * server defaults exactly as they were before this option existed.
10
+ */
11
+ export interface KyselyTransactionRunnerOptions {
12
+ /**
13
+ * `lock_timeout` in milliseconds -- how long a statement waits for a row lock
14
+ * before it aborts. This is the recommended setting: it bounds the blocking
15
+ * `SELECT ... FOR UPDATE` behind `lockByUuid()`, so a stuck lock holder cannot
16
+ * hang a `triggerEvent()` call indefinitely while it keeps a pooled connection
17
+ * checked out. `0` disables the timeout (PostgreSQL's own default).
18
+ */
19
+ lockTimeoutMs?: number;
20
+ /**
21
+ * `statement_timeout` in milliseconds -- how long any single statement may run
22
+ * before it aborts. Deliberately unset by default: commands run inside the
23
+ * same transaction and may legitimately issue slow statements on the shared
24
+ * connection, and aborting one of those rolls the whole transition back.
25
+ * `0` disables the timeout (PostgreSQL's own default).
26
+ */
27
+ statementTimeoutMs?: number;
28
+ }
4
29
  export declare class KyselyTransactionRunner implements WorkflowTransactionRunner {
5
30
  private readonly db;
6
- constructor(db: Kysely<WorkflowDatabase>);
31
+ private readonly timeoutSettings;
32
+ constructor(db: Kysely<WorkflowDatabase>, options?: KyselyTransactionRunnerOptions);
7
33
  runInTransaction<T>(callback: () => Promise<T>): Promise<T>;
34
+ /**
35
+ * Applies the configured timeouts to `trx` as transaction-local settings, so
36
+ * they are reverted on COMMIT/ROLLBACK and never leak to other users of the
37
+ * shared pool.
38
+ *
39
+ * `set_config(name, value, is_local = true)` is the function form of
40
+ * `SET LOCAL`, reached through the query builder rather than kysely's `sql`
41
+ * template on purpose: this package imports kysely for types only, which is
42
+ * what keeps its CommonJS build loadable (kysely itself is ESM-only). Both
43
+ * arguments are bound parameters, so no value is ever interpolated into SQL.
44
+ */
45
+ private applyTimeouts;
8
46
  }
9
47
  //# sourceMappingURL=kysely-transaction-runner.d.ts.map
@@ -1,11 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KyselyTransactionRunner = void 0;
4
+ const core_1 = require("@duraflows/core");
4
5
  const kysely_transaction_context_js_1 = require("./kysely-transaction-context.js");
6
+ /**
7
+ * Rejects anything that is not a finite, non-negative integer. Called at
8
+ * construction time so an invalid value can never reach the database.
9
+ */
10
+ function assertTimeoutMs(value, name) {
11
+ if (!Number.isSafeInteger(value) || value < 0) {
12
+ throw new core_1.WorkflowError(`${name} must be a non-negative integer number of milliseconds, got ${value}`);
13
+ }
14
+ }
15
+ /**
16
+ * Resolves the configured timeouts into `(setting, value)` pairs, or an empty
17
+ * list when neither is configured.
18
+ */
19
+ function buildTimeoutSettings(options) {
20
+ const settings = [];
21
+ if (options.lockTimeoutMs !== undefined) {
22
+ assertTimeoutMs(options.lockTimeoutMs, "lockTimeoutMs");
23
+ settings.push(["lock_timeout", String(options.lockTimeoutMs)]);
24
+ }
25
+ if (options.statementTimeoutMs !== undefined) {
26
+ assertTimeoutMs(options.statementTimeoutMs, "statementTimeoutMs");
27
+ settings.push(["statement_timeout", String(options.statementTimeoutMs)]);
28
+ }
29
+ return settings;
30
+ }
5
31
  class KyselyTransactionRunner {
6
32
  db;
7
- constructor(db) {
33
+ timeoutSettings;
34
+ constructor(db, options = {}) {
8
35
  this.db = db;
36
+ this.timeoutSettings = buildTimeoutSettings(options);
9
37
  }
10
38
  async runInTransaction(callback) {
11
39
  const existing = kysely_transaction_context_js_1.KyselyTransactionContext.getTransaction(this.db);
@@ -13,9 +41,28 @@ class KyselyTransactionRunner {
13
41
  return callback();
14
42
  }
15
43
  return this.db.transaction().execute(async (trx) => {
44
+ await this.applyTimeouts(trx);
16
45
  return kysely_transaction_context_js_1.KyselyTransactionContext.run(this.db, trx, callback);
17
46
  });
18
47
  }
48
+ /**
49
+ * Applies the configured timeouts to `trx` as transaction-local settings, so
50
+ * they are reverted on COMMIT/ROLLBACK and never leak to other users of the
51
+ * shared pool.
52
+ *
53
+ * `set_config(name, value, is_local = true)` is the function form of
54
+ * `SET LOCAL`, reached through the query builder rather than kysely's `sql`
55
+ * template on purpose: this package imports kysely for types only, which is
56
+ * what keeps its CommonJS build loadable (kysely itself is ESM-only). Both
57
+ * arguments are bound parameters, so no value is ever interpolated into SQL.
58
+ */
59
+ async applyTimeouts(trx) {
60
+ for (const [setting, value] of this.timeoutSettings) {
61
+ await trx
62
+ .selectNoFrom((eb) => eb.fn("set_config", [eb.val(setting), eb.val(value), eb.val(true)]).as("set_config"))
63
+ .executeTakeFirst();
64
+ }
65
+ }
19
66
  }
20
67
  exports.KyselyTransactionRunner = KyselyTransactionRunner;
21
68
  //# sourceMappingURL=kysely-transaction-runner.js.map
package/dist/index.d.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import type { Kysely, Transaction } from "kysely";
2
2
  import type { WorkflowPersistenceProvider } from "@duraflows/core";
3
3
  import type { WorkflowDatabase } from "./kysely-database.js";
4
+ import { type KyselyTransactionRunnerOptions } from "./kysely-transaction-runner.js";
4
5
  export { KyselyTransactionContext } from "./kysely-transaction-context.js";
5
6
  export { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
7
+ export type { KyselyTransactionRunnerOptions } from "./kysely-transaction-runner.js";
6
8
  export { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
7
9
  export { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
8
- export type { WorkflowDatabase, WorkflowInstancesTable, WorkflowHistoryTable } from "./kysely-database.js";
10
+ export { KyselyWorkflowDefinitionStore } from "./kysely-definition-store.js";
11
+ export type { WorkflowDatabase, WorkflowInstancesTable, WorkflowHistoryTable, WorkflowDefinitionsTable, } from "./kysely-database.js";
12
+ /**
13
+ * Options accepted by {@link kyselyWorkflowProviders}. Every field is optional
14
+ * and omitting the argument entirely reproduces the pre-existing behaviour.
15
+ */
16
+ export type KyselyWorkflowProvidersOptions = KyselyTransactionRunnerOptions;
9
17
  /**
10
18
  * Creates long-lived persistence providers from a Kysely instance.
11
19
  *
@@ -14,7 +22,7 @@ export type { WorkflowDatabase, WorkflowInstancesTable, WorkflowHistoryTable } f
14
22
  * only access workflow tables; the `unknown` cast is needed because
15
23
  * Kysely is invariant in its DB type parameter).
16
24
  */
17
- export declare function kyselyWorkflowProviders<DB extends WorkflowDatabase>(db: Kysely<DB>): WorkflowPersistenceProvider;
25
+ export declare function kyselyWorkflowProviders<DB extends WorkflowDatabase>(db: Kysely<DB>, options?: KyselyWorkflowProvidersOptions): WorkflowPersistenceProvider;
18
26
  /**
19
27
  * Creates providers pre-bound to an existing Kysely transaction.
20
28
  *
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
2
2
  import { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
3
+ import { KyselyWorkflowDefinitionStore } from "./kysely-definition-store.js";
3
4
  import { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
4
5
  import { KyselyTransactionContext } from "./kysely-transaction-context.js";
5
6
  export { KyselyTransactionContext } from "./kysely-transaction-context.js";
6
7
  export { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
7
8
  export { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
8
9
  export { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
10
+ export { KyselyWorkflowDefinitionStore } from "./kysely-definition-store.js";
9
11
  /**
10
12
  * Creates long-lived persistence providers from a Kysely instance.
11
13
  *
@@ -14,12 +16,13 @@ export { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
14
16
  * only access workflow tables; the `unknown` cast is needed because
15
17
  * Kysely is invariant in its DB type parameter).
16
18
  */
17
- export function kyselyWorkflowProviders(db) {
19
+ export function kyselyWorkflowProviders(db, options = {}) {
18
20
  const narrowed = db;
19
- const transactionRunner = new KyselyTransactionRunner(narrowed);
21
+ const transactionRunner = new KyselyTransactionRunner(narrowed, options);
20
22
  const instanceStore = new KyselyWorkflowInstanceStore(narrowed);
21
23
  const historyStore = new KyselyWorkflowHistoryStore(narrowed);
22
- return { instanceStore, historyStore, transactionRunner };
24
+ const definitionStore = new KyselyWorkflowDefinitionStore(narrowed);
25
+ return { instanceStore, historyStore, transactionRunner, definitionStore };
23
26
  }
24
27
  /**
25
28
  * Creates providers pre-bound to an existing Kysely transaction.
@@ -40,6 +43,7 @@ export function kyselyWorkflowProvidersFromTransaction(trx) {
40
43
  };
41
44
  const instanceStore = new KyselyWorkflowInstanceStore(narrowed);
42
45
  const historyStore = new KyselyWorkflowHistoryStore(narrowed);
43
- return { instanceStore, historyStore, transactionRunner };
46
+ const definitionStore = new KyselyWorkflowDefinitionStore(narrowed);
47
+ return { instanceStore, historyStore, transactionRunner, definitionStore };
44
48
  }
45
49
  //# sourceMappingURL=index.js.map
@@ -17,6 +17,7 @@ export interface WorkflowInstancesTable {
17
17
  workflow_name: string;
18
18
  current_state: string;
19
19
  version: number;
20
+ definition_version: number | null;
20
21
  expires_at: Date | null;
21
22
  last_transition_at: Date;
22
23
  context_json: JsonObjectColumn;
@@ -35,11 +36,20 @@ export interface WorkflowHistoryTable {
35
36
  rejected_by: string | null;
36
37
  command_results_json: JsonArrayColumn;
37
38
  trigger_metadata_json: JsonObjectColumn;
39
+ definition_version: number | null;
38
40
  created_at: Generated<Date>;
39
41
  }
42
+ export interface WorkflowDefinitionsTable {
43
+ workflow_name: string;
44
+ version: number;
45
+ content_hash: string;
46
+ definition_json: JsonObjectColumn;
47
+ registered_at: Generated<Date>;
48
+ }
40
49
  export interface WorkflowDatabase {
41
50
  workflow_instances: WorkflowInstancesTable;
42
51
  workflow_history: WorkflowHistoryTable;
52
+ workflow_definitions: WorkflowDefinitionsTable;
43
53
  }
44
54
  export {};
45
55
  //# sourceMappingURL=kysely-database.d.ts.map
@@ -0,0 +1,17 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { WorkflowDefinitionStore, StoredWorkflowDefinition, WorkflowDefinition } from "@duraflows/core";
3
+ import type { WorkflowDatabase } from "./kysely-database.js";
4
+ export declare class KyselyWorkflowDefinitionStore implements WorkflowDefinitionStore {
5
+ private readonly db;
6
+ constructor(db: Kysely<WorkflowDatabase>);
7
+ private getExecutor;
8
+ ensure(record: {
9
+ workflowName: string;
10
+ version: number;
11
+ contentHash: string;
12
+ definitionJson: WorkflowDefinition;
13
+ }): Promise<StoredWorkflowDefinition>;
14
+ findByNameAndVersion(workflowName: string, version: number): Promise<StoredWorkflowDefinition | null>;
15
+ private mapRow;
16
+ }
17
+ //# sourceMappingURL=kysely-definition-store.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { WorkflowError } from "@duraflows/core";
2
+ import { KyselyTransactionContext } from "./kysely-transaction-context.js";
3
+ export class KyselyWorkflowDefinitionStore {
4
+ db;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ getExecutor() {
9
+ return KyselyTransactionContext.getTransaction(this.db) ?? this.db;
10
+ }
11
+ async ensure(record) {
12
+ const executor = this.getExecutor();
13
+ await executor
14
+ .insertInto("workflow_definitions")
15
+ .values({
16
+ workflow_name: record.workflowName,
17
+ version: record.version,
18
+ content_hash: record.contentHash,
19
+ definition_json: JSON.stringify(record.definitionJson),
20
+ })
21
+ .onConflict((oc) => oc.columns(["workflow_name", "version"]).doNothing())
22
+ .execute();
23
+ const stored = await this.findByNameAndVersion(record.workflowName, record.version);
24
+ if (!stored) {
25
+ // The row we just ensured must exist; its absence means the statement
26
+ // did not do what the adapter assumes. Fail loudly.
27
+ throw new WorkflowError(`Failed to ensure workflow definition "${record.workflowName}" version ${record.version}`);
28
+ }
29
+ return stored;
30
+ }
31
+ async findByNameAndVersion(workflowName, version) {
32
+ const executor = this.getExecutor();
33
+ const row = await executor
34
+ .selectFrom("workflow_definitions")
35
+ .selectAll()
36
+ .where("workflow_name", "=", workflowName)
37
+ .where("version", "=", version)
38
+ .executeTakeFirst();
39
+ if (!row)
40
+ return null;
41
+ return this.mapRow(row);
42
+ }
43
+ mapRow(row) {
44
+ return {
45
+ workflowName: row.workflow_name,
46
+ version: row.version,
47
+ contentHash: row.content_hash,
48
+ definitionJson: row.definition_json,
49
+ registeredAt: row.registered_at,
50
+ };
51
+ }
52
+ }
53
+ //# sourceMappingURL=kysely-definition-store.js.map
@@ -1,3 +1,4 @@
1
+ import { WorkflowError } from "@duraflows/core";
1
2
  import { KyselyTransactionContext } from "./kysely-transaction-context.js";
2
3
  export class KyselyWorkflowHistoryStore {
3
4
  db;
@@ -21,9 +22,13 @@ export class KyselyWorkflowHistoryStore {
21
22
  rejected_by: entry.rejectedBy ?? null,
22
23
  command_results_json: JSON.stringify(entry.commandResultsJson),
23
24
  trigger_metadata_json: JSON.stringify(entry.triggerMetadata ?? {}),
25
+ definition_version: entry.definitionVersion ?? null,
24
26
  })
25
27
  .returning("uuid")
26
- .executeTakeFirstOrThrow();
28
+ // `INSERT ... RETURNING` always yields a row, so an empty result means the
29
+ // statement did not do what the adapter assumes. Surface that as the
30
+ // library's own error type rather than kysely's `NoResultError`.
31
+ .executeTakeFirstOrThrow(() => new WorkflowError("Failed to append workflow history: INSERT ... RETURNING uuid returned no row"));
27
32
  return row.uuid;
28
33
  }
29
34
  async findByInstanceUuid(workflowInstanceUuid, options) {
@@ -49,6 +54,8 @@ export class KyselyWorkflowHistoryStore {
49
54
  rejectedBy: row.rejected_by ?? undefined,
50
55
  commandResultsJson: row.command_results_json,
51
56
  triggerMetadata: row.trigger_metadata_json,
57
+ definitionVersion: row.definition_version ?? undefined,
58
+ createdAt: row.created_at,
52
59
  }));
53
60
  }
54
61
  }
@@ -17,6 +17,7 @@ export class KyselyWorkflowInstanceStore {
17
17
  workflow_name: instance.workflowName,
18
18
  current_state: instance.currentState,
19
19
  version: instance.version,
20
+ definition_version: instance.definitionVersion,
20
21
  expires_at: instance.expiresAt,
21
22
  last_transition_at: instance.lastTransitionAt,
22
23
  context_json: JSON.stringify(instance.context),
@@ -56,6 +57,7 @@ export class KyselyWorkflowInstanceStore {
56
57
  .set({
57
58
  current_state: instance.currentState,
58
59
  version: instance.version,
60
+ definition_version: instance.definitionVersion,
59
61
  expires_at: instance.expiresAt,
60
62
  last_transition_at: instance.lastTransitionAt,
61
63
  context_json: JSON.stringify(instance.context),
@@ -92,6 +94,7 @@ export class KyselyWorkflowInstanceStore {
92
94
  workflowName: row.workflow_name,
93
95
  currentState: row.current_state,
94
96
  version: row.version,
97
+ definitionVersion: row.definition_version ?? null,
95
98
  expiresAt: row.expires_at,
96
99
  lastTransitionAt: row.last_transition_at,
97
100
  context: row.context_json,
@@ -1,9 +1,47 @@
1
1
  import type { Kysely } from "kysely";
2
2
  import type { WorkflowTransactionRunner } from "@duraflows/core";
3
3
  import type { WorkflowDatabase } from "./kysely-database.js";
4
+ /**
5
+ * Transaction-scoped PostgreSQL timeouts.
6
+ *
7
+ * Both settings are optional and both default to unset: when neither is
8
+ * supplied the runner issues no extra statement at all, leaving the session and
9
+ * server defaults exactly as they were before this option existed.
10
+ */
11
+ export interface KyselyTransactionRunnerOptions {
12
+ /**
13
+ * `lock_timeout` in milliseconds -- how long a statement waits for a row lock
14
+ * before it aborts. This is the recommended setting: it bounds the blocking
15
+ * `SELECT ... FOR UPDATE` behind `lockByUuid()`, so a stuck lock holder cannot
16
+ * hang a `triggerEvent()` call indefinitely while it keeps a pooled connection
17
+ * checked out. `0` disables the timeout (PostgreSQL's own default).
18
+ */
19
+ lockTimeoutMs?: number;
20
+ /**
21
+ * `statement_timeout` in milliseconds -- how long any single statement may run
22
+ * before it aborts. Deliberately unset by default: commands run inside the
23
+ * same transaction and may legitimately issue slow statements on the shared
24
+ * connection, and aborting one of those rolls the whole transition back.
25
+ * `0` disables the timeout (PostgreSQL's own default).
26
+ */
27
+ statementTimeoutMs?: number;
28
+ }
4
29
  export declare class KyselyTransactionRunner implements WorkflowTransactionRunner {
5
30
  private readonly db;
6
- constructor(db: Kysely<WorkflowDatabase>);
31
+ private readonly timeoutSettings;
32
+ constructor(db: Kysely<WorkflowDatabase>, options?: KyselyTransactionRunnerOptions);
7
33
  runInTransaction<T>(callback: () => Promise<T>): Promise<T>;
34
+ /**
35
+ * Applies the configured timeouts to `trx` as transaction-local settings, so
36
+ * they are reverted on COMMIT/ROLLBACK and never leak to other users of the
37
+ * shared pool.
38
+ *
39
+ * `set_config(name, value, is_local = true)` is the function form of
40
+ * `SET LOCAL`, reached through the query builder rather than kysely's `sql`
41
+ * template on purpose: this package imports kysely for types only, which is
42
+ * what keeps its CommonJS build loadable (kysely itself is ESM-only). Both
43
+ * arguments are bound parameters, so no value is ever interpolated into SQL.
44
+ */
45
+ private applyTimeouts;
8
46
  }
9
47
  //# sourceMappingURL=kysely-transaction-runner.d.ts.map
@@ -1,8 +1,36 @@
1
+ import { WorkflowError } from "@duraflows/core";
1
2
  import { KyselyTransactionContext } from "./kysely-transaction-context.js";
3
+ /**
4
+ * Rejects anything that is not a finite, non-negative integer. Called at
5
+ * construction time so an invalid value can never reach the database.
6
+ */
7
+ function assertTimeoutMs(value, name) {
8
+ if (!Number.isSafeInteger(value) || value < 0) {
9
+ throw new WorkflowError(`${name} must be a non-negative integer number of milliseconds, got ${value}`);
10
+ }
11
+ }
12
+ /**
13
+ * Resolves the configured timeouts into `(setting, value)` pairs, or an empty
14
+ * list when neither is configured.
15
+ */
16
+ function buildTimeoutSettings(options) {
17
+ const settings = [];
18
+ if (options.lockTimeoutMs !== undefined) {
19
+ assertTimeoutMs(options.lockTimeoutMs, "lockTimeoutMs");
20
+ settings.push(["lock_timeout", String(options.lockTimeoutMs)]);
21
+ }
22
+ if (options.statementTimeoutMs !== undefined) {
23
+ assertTimeoutMs(options.statementTimeoutMs, "statementTimeoutMs");
24
+ settings.push(["statement_timeout", String(options.statementTimeoutMs)]);
25
+ }
26
+ return settings;
27
+ }
2
28
  export class KyselyTransactionRunner {
3
29
  db;
4
- constructor(db) {
30
+ timeoutSettings;
31
+ constructor(db, options = {}) {
5
32
  this.db = db;
33
+ this.timeoutSettings = buildTimeoutSettings(options);
6
34
  }
7
35
  async runInTransaction(callback) {
8
36
  const existing = KyselyTransactionContext.getTransaction(this.db);
@@ -10,8 +38,27 @@ export class KyselyTransactionRunner {
10
38
  return callback();
11
39
  }
12
40
  return this.db.transaction().execute(async (trx) => {
41
+ await this.applyTimeouts(trx);
13
42
  return KyselyTransactionContext.run(this.db, trx, callback);
14
43
  });
15
44
  }
45
+ /**
46
+ * Applies the configured timeouts to `trx` as transaction-local settings, so
47
+ * they are reverted on COMMIT/ROLLBACK and never leak to other users of the
48
+ * shared pool.
49
+ *
50
+ * `set_config(name, value, is_local = true)` is the function form of
51
+ * `SET LOCAL`, reached through the query builder rather than kysely's `sql`
52
+ * template on purpose: this package imports kysely for types only, which is
53
+ * what keeps its CommonJS build loadable (kysely itself is ESM-only). Both
54
+ * arguments are bound parameters, so no value is ever interpolated into SQL.
55
+ */
56
+ async applyTimeouts(trx) {
57
+ for (const [setting, value] of this.timeoutSettings) {
58
+ await trx
59
+ .selectNoFrom((eb) => eb.fn("set_config", [eb.val(setting), eb.val(value), eb.val(true)]).as("set_config"))
60
+ .executeTakeFirst();
61
+ }
62
+ }
16
63
  }
17
64
  //# sourceMappingURL=kysely-transaction-runner.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duraflows/kysely",
3
- "version": "4.0.1",
3
+ "version": "5.0.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": "^4.0.1",
55
- "kysely": "^0.29.4"
54
+ "@duraflows/core": "^5.0.0",
55
+ "kysely": "^0.29.5"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/pg": "^8.20.4",
59
- "kysely": "^0.29.4",
60
- "pg": "^8.22.0",
61
- "@duraflows/core": "4.0.1",
62
- "@duraflows/pg": "4.0.1"
58
+ "@types/pg": "^8.21.0",
59
+ "kysely": "^0.29.5",
60
+ "pg": "^8.23.0",
61
+ "@duraflows/core": "5.0.0",
62
+ "@duraflows/pg": "5.0.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsc --build"