@pikku/kysely 0.12.16 → 0.12.18
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 +88 -0
- package/dist/src/kysely-tables.d.ts +1 -0
- package/dist/src/kysely-workflow-mirror.js +10 -0
- package/dist/src/kysely-workflow-run-service.js +2 -0
- package/dist/src/kysely-workflow-service.d.ts +7 -2
- package/dist/src/kysely-workflow-service.js +31 -4
- package/package.json +2 -2
- package/src/kysely-tables.ts +1 -0
- package/src/kysely-workflow-mirror.test.ts +23 -0
- package/src/kysely-workflow-mirror.ts +11 -0
- package/src/kysely-workflow-run-service.ts +2 -0
- package/src/kysely-workflow-service.ts +37 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
+
## 0.12.18
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 5c0ff0f: Fix `getRunHistory` dropping step provenance (`fromStepName`). The value was persisted on the step row and used by the graph planner, but `getRunHistory` built its rows from the per-attempt history and never carried `fromStepName` through — so run history (and any timeline reconstructed from it) reported no predecessors. Redis and Kysely `getRunHistory` now return `fromStepName`. Also adds the missing `from_step_name` column (+ backfill) to the Kysely workflow mirror's `workflow_step` schema and persists it on mirror inserts, so a mirror-side history has identical provenance.
|
|
6
|
+
- 72694f6: feat(workflow): expose per-step attempt count + record running/succeeded/failed timestamps
|
|
7
|
+
|
|
8
|
+
`getRunStatus` now returns `attempts` (the latest attempt count) per step, so
|
|
9
|
+
consumers can show retry counts without a second history query. It already
|
|
10
|
+
computed `duration` from `runningAt`/`succeededAt`, but the kysely and mongodb
|
|
11
|
+
workflow stores only stamped those timestamps on the _insert_ path — the
|
|
12
|
+
`running` / `succeeded` / `failed` status transitions updated the history row's
|
|
13
|
+
status without setting `runningAt` / `succeededAt` / `failedAt`, so `duration`
|
|
14
|
+
was always undefined. The transitions now stamp the matching timestamp, so step
|
|
15
|
+
duration is populated for kysely- and mongodb-backed runs. (Redis already
|
|
16
|
+
stamped on transition.) A shared service-suite test guards both behaviours.
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [4be205f]
|
|
19
|
+
- Updated dependencies [061c717]
|
|
20
|
+
- Updated dependencies [2c55e13]
|
|
21
|
+
- Updated dependencies [c745c26]
|
|
22
|
+
- Updated dependencies [57900b5]
|
|
23
|
+
- Updated dependencies [72694f6]
|
|
24
|
+
- @pikku/core@0.12.39
|
|
25
|
+
|
|
26
|
+
## 0.12.17
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- 92cd5b1: feat(workflow): workflow-owned step retries + stable invocationId
|
|
31
|
+
|
|
32
|
+
The workflow — not the queue — now owns step retry policy, and each step
|
|
33
|
+
invocation gets a stable idempotency key.
|
|
34
|
+
- **Default `retries: 5` with exponential backoff.** A step with no `retries`
|
|
35
|
+
previously inherited the queue's bare default (e.g. pg-boss `retry_limit 2`,
|
|
36
|
+
no backoff) so retries fired instantly and couldn't outlast a transient
|
|
37
|
+
outage. Retries now default to 5 with backoff, resolved at the workflow layer.
|
|
38
|
+
- **`retries: 0` is honored.** Dispatch previously passed `undefined` options
|
|
39
|
+
for `retries: 0`, letting the queue re-run a non-idempotent step up to its own
|
|
40
|
+
default. The resolved policy now always sets `attempts` (`retries: 0` →
|
|
41
|
+
`attempts: 1`), so the queue never second-guesses the workflow. The persisted
|
|
42
|
+
step retries and the dispatched `attempts` are resolved together so
|
|
43
|
+
"retries exhausted" and "no more redeliveries" are the same event.
|
|
44
|
+
- **`workflowStep.invocationId`** — a deterministic, dependency-free
|
|
45
|
+
`uuidv5(runId:stepName)` handed to every step. Unlike `stepId` (minted per
|
|
46
|
+
attempt), it is identical across retries, so a step can dedupe on it
|
|
47
|
+
(`ON CONFLICT (invocationId)`, Stripe idempotency keys, etc.).
|
|
48
|
+
- **queue-bullmq**: `mapPikkuJobToBull` now maps `backoff` (previously dropped,
|
|
49
|
+
so a step's backoff silently never applied on Redis), and `registerQueues`
|
|
50
|
+
throws a clear error when no logger is available (matching queue-pg-boss).
|
|
51
|
+
- **Dispatch failures are recoverable, not fatal.** A step is now marked
|
|
52
|
+
`scheduled` only _after_ it is successfully handed to its transport (queue or
|
|
53
|
+
scheduler) — a failed hand-off leaves it `pending` so a replay re-dispatches
|
|
54
|
+
it, instead of stranding it in `scheduled` (replay would pause forever on a
|
|
55
|
+
job that was never enqueued). A transport outage (e.g. pg-boss momentarily
|
|
56
|
+
down) is surfaced as a new `WorkflowDispatchException`, which the orchestrator
|
|
57
|
+
treats as transient: the run is left running and the orchestrator job is
|
|
58
|
+
rethrown for redelivery (it replays idempotently from the snapshot) rather
|
|
59
|
+
than the whole run being marked `failed`. The orchestrator job now also
|
|
60
|
+
carries its own retry policy, so this holds even when the orchestrator queue
|
|
61
|
+
is configured `retry_limit 0`. A genuine step error still fails the run.
|
|
62
|
+
- **Same step name can be invoked multiple times in one run.** Step rows are now
|
|
63
|
+
keyed per _invocation_: the Nth reach of a step name in a replay resolves to a
|
|
64
|
+
physical key (`name` for the first, `name#N` for repeats), so a literal
|
|
65
|
+
duplicate name no longer clobbers the earlier step's state. The first reach
|
|
66
|
+
keeps the bare name, so existing rows, graph-node matching and `invocationId`s
|
|
67
|
+
are unchanged. Ordinals are derived deterministically from DSL execution order
|
|
68
|
+
and reset each replay.
|
|
69
|
+
- **Step provenance (`fromStepName`) + graph cycles.** Every step now records
|
|
70
|
+
the predecessor it was scheduled from (`fromStepName`; entry steps have none),
|
|
71
|
+
persisted on the step row across all stores (in-memory, kysely, redis,
|
|
72
|
+
mongodb, cloudflare DO) and carried in the queued payload. The DSL wire
|
|
73
|
+
exposes the derived `fromInvocationId` (`uuidv5(runId:fromStepName)`) so
|
|
74
|
+
consumers get the stable predecessor key without a second persisted id —
|
|
75
|
+
`fromStepName` is the source of truth (it is replay-deterministic; `stepId`,
|
|
76
|
+
minted per row, is not). This makes the walked path reconstructable even when
|
|
77
|
+
a node is reached more than once: in `a → b → a → c` the second `a` is a
|
|
78
|
+
distinct ordinal instance (`a#1`) whose `fromStepName` is `b`.
|
|
79
|
+
The graph runner now supports **cycles**: a forward edge into an
|
|
80
|
+
already-started node still collapses to a single run (joins/diamonds are
|
|
81
|
+
unchanged), but a _back-edge_ — one whose target can reach its source — fires
|
|
82
|
+
a fresh ordinal instance, so a node can loop back to itself. Termination is
|
|
83
|
+
the graph's responsibility (branch routing must converge); the engine enforces
|
|
84
|
+
no visit cap.
|
|
85
|
+
|
|
86
|
+
- Updated dependencies [92cd5b1]
|
|
87
|
+
- @pikku/core@0.12.38
|
|
88
|
+
|
|
1
89
|
## 0.12.16
|
|
2
90
|
|
|
3
91
|
### Patch Changes
|
|
@@ -61,6 +61,7 @@ export class KyselyWorkflowMirror {
|
|
|
61
61
|
.addColumn('branch_taken', 'text')
|
|
62
62
|
.addColumn('retries', 'integer')
|
|
63
63
|
.addColumn('retry_delay', 'text')
|
|
64
|
+
.addColumn('from_step_name', 'text')
|
|
64
65
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
65
66
|
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
66
67
|
.addUniqueConstraint('workflow_step_run_name_unique', [
|
|
@@ -68,6 +69,14 @@ export class KyselyWorkflowMirror {
|
|
|
68
69
|
'step_name',
|
|
69
70
|
])
|
|
70
71
|
.execute();
|
|
72
|
+
// Additive: backfill the column on mirror tables created before from_step_name.
|
|
73
|
+
await this.db.schema
|
|
74
|
+
.alterTable('workflow_step')
|
|
75
|
+
.addColumn('from_step_name', 'text')
|
|
76
|
+
.execute()
|
|
77
|
+
.catch(() => {
|
|
78
|
+
// Column already exists (fresh tables include it above) — nothing to do.
|
|
79
|
+
});
|
|
71
80
|
await this.db.schema
|
|
72
81
|
.createTable('workflow_step_history')
|
|
73
82
|
.ifNotExists()
|
|
@@ -144,6 +153,7 @@ export class KyselyWorkflowMirror {
|
|
|
144
153
|
status: step.status,
|
|
145
154
|
retries: step.retries ?? null,
|
|
146
155
|
retryDelay: step.retryDelay?.toString() ?? null,
|
|
156
|
+
fromStepName: step.fromStepName ?? null,
|
|
147
157
|
createdAt: now,
|
|
148
158
|
updatedAt: now,
|
|
149
159
|
})
|
|
@@ -137,6 +137,7 @@ export class KyselyWorkflowRunService {
|
|
|
137
137
|
.select([
|
|
138
138
|
's.workflowStepId',
|
|
139
139
|
's.stepName',
|
|
140
|
+
's.fromStepName',
|
|
140
141
|
's.retries',
|
|
141
142
|
's.retryDelay',
|
|
142
143
|
'h.status',
|
|
@@ -158,6 +159,7 @@ export class KyselyWorkflowRunService {
|
|
|
158
159
|
return {
|
|
159
160
|
stepId,
|
|
160
161
|
stepName: row.stepName,
|
|
162
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
161
163
|
status: row.status,
|
|
162
164
|
result: parseJson(row.result),
|
|
163
165
|
error: parseJson(row.error),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SerializedError } from '@pikku/core';
|
|
2
|
-
import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
|
|
2
|
+
import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type StepStatus, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
|
|
3
3
|
import type { Kysely } from 'kysely';
|
|
4
4
|
import type { KyselyPikkuDB } from './kysely-tables.js';
|
|
5
5
|
export declare class KyselyWorkflowService extends PikkuWorkflowService {
|
|
@@ -17,7 +17,7 @@ export declare class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
17
17
|
protected insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: {
|
|
18
18
|
retries?: number;
|
|
19
19
|
retryDelay?: string | number;
|
|
20
|
-
}): Promise<StepState>;
|
|
20
|
+
}, fromStepName?: string): Promise<StepState>;
|
|
21
21
|
getStepState(runId: string, stepName: string): Promise<StepState>;
|
|
22
22
|
getRunHistory(runId: string): Promise<Array<StepState & {
|
|
23
23
|
stepName: string;
|
|
@@ -38,6 +38,11 @@ export declare class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
38
38
|
branchKeys: Record<string, string>;
|
|
39
39
|
}>;
|
|
40
40
|
getNodesWithoutSteps(runId: string, nodeIds: string[]): Promise<string[]>;
|
|
41
|
+
getStepInstances(runId: string): Promise<Array<{
|
|
42
|
+
stepName: string;
|
|
43
|
+
status: StepStatus;
|
|
44
|
+
fromStepName?: string;
|
|
45
|
+
}>>;
|
|
41
46
|
getNodeResults(runId: string, nodeIds: string[]): Promise<Record<string, any>>;
|
|
42
47
|
protected setBranchTakenImpl(stepId: string, branchKey: string): Promise<void>;
|
|
43
48
|
protected updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
|
|
@@ -55,6 +55,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
55
55
|
.addColumn('branch_taken', 'text')
|
|
56
56
|
.addColumn('retries', 'integer')
|
|
57
57
|
.addColumn('retry_delay', 'text')
|
|
58
|
+
.addColumn('from_step_name', 'text')
|
|
58
59
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
59
60
|
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
60
61
|
.addUniqueConstraint('workflow_step_run_name_unique', [
|
|
@@ -62,6 +63,14 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
62
63
|
'step_name',
|
|
63
64
|
])
|
|
64
65
|
.execute();
|
|
66
|
+
// Additive: backfill the column on tables created before from_step_name.
|
|
67
|
+
await this.db.schema
|
|
68
|
+
.alterTable('workflow_step')
|
|
69
|
+
.addColumn('from_step_name', 'text')
|
|
70
|
+
.execute()
|
|
71
|
+
.catch(() => {
|
|
72
|
+
// Column already exists (fresh tables include it above) — nothing to do.
|
|
73
|
+
});
|
|
65
74
|
await this.db.schema
|
|
66
75
|
.createTable('workflow_step_history')
|
|
67
76
|
.ifNotExists()
|
|
@@ -132,7 +141,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
132
141
|
.where('workflowRunId', '=', id)
|
|
133
142
|
.execute();
|
|
134
143
|
}
|
|
135
|
-
async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions) {
|
|
144
|
+
async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions, fromStepName) {
|
|
136
145
|
const stepId = crypto.randomUUID();
|
|
137
146
|
const now = new Date();
|
|
138
147
|
await this.db
|
|
@@ -146,6 +155,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
146
155
|
status: 'pending',
|
|
147
156
|
retries: stepOptions?.retries ?? null,
|
|
148
157
|
retryDelay: stepOptions?.retryDelay?.toString() ?? null,
|
|
158
|
+
fromStepName: fromStepName ?? null,
|
|
149
159
|
createdAt: now,
|
|
150
160
|
updatedAt: now,
|
|
151
161
|
})
|
|
@@ -159,6 +169,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
159
169
|
attemptCount: 1,
|
|
160
170
|
retries: stepOptions?.retries,
|
|
161
171
|
retryDelay: stepOptions?.retryDelay?.toString(),
|
|
172
|
+
fromStepName,
|
|
162
173
|
createdAt: now,
|
|
163
174
|
updatedAt: now,
|
|
164
175
|
};
|
|
@@ -173,6 +184,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
173
184
|
's.error',
|
|
174
185
|
's.retries',
|
|
175
186
|
's.retryDelay',
|
|
187
|
+
's.fromStepName',
|
|
176
188
|
's.createdAt',
|
|
177
189
|
's.updatedAt',
|
|
178
190
|
])
|
|
@@ -195,6 +207,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
195
207
|
attemptCount: Number(row.attemptCount),
|
|
196
208
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
197
209
|
retryDelay: row.retryDelay ?? undefined,
|
|
210
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
198
211
|
createdAt: new Date(row.createdAt),
|
|
199
212
|
updatedAt: new Date(row.updatedAt),
|
|
200
213
|
};
|
|
@@ -218,7 +231,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
218
231
|
if (latestHistory) {
|
|
219
232
|
await this.db
|
|
220
233
|
.updateTable('workflowStepHistory')
|
|
221
|
-
.set({ status: 'running' })
|
|
234
|
+
.set({ status: 'running', runningAt: new Date() })
|
|
222
235
|
.where('historyId', '=', latestHistory.historyId)
|
|
223
236
|
.execute();
|
|
224
237
|
}
|
|
@@ -295,7 +308,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
295
308
|
if (latestHistory) {
|
|
296
309
|
await this.db
|
|
297
310
|
.updateTable('workflowStepHistory')
|
|
298
|
-
.set({ status: 'succeeded', result: resultJson })
|
|
311
|
+
.set({ status: 'succeeded', result: resultJson, succeededAt: new Date() })
|
|
299
312
|
.where('historyId', '=', latestHistory.historyId)
|
|
300
313
|
.execute();
|
|
301
314
|
}
|
|
@@ -327,7 +340,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
327
340
|
if (latestHistory) {
|
|
328
341
|
await this.db
|
|
329
342
|
.updateTable('workflowStepHistory')
|
|
330
|
-
.set({ status: 'failed', error: errorJson })
|
|
343
|
+
.set({ status: 'failed', error: errorJson, failedAt: new Date() })
|
|
331
344
|
.where('historyId', '=', latestHistory.historyId)
|
|
332
345
|
.execute();
|
|
333
346
|
}
|
|
@@ -348,6 +361,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
348
361
|
's.error',
|
|
349
362
|
's.retries',
|
|
350
363
|
's.retryDelay',
|
|
364
|
+
's.fromStepName',
|
|
351
365
|
's.createdAt',
|
|
352
366
|
's.updatedAt',
|
|
353
367
|
])
|
|
@@ -366,6 +380,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
366
380
|
attemptCount: Number(row.attemptCount),
|
|
367
381
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
368
382
|
retryDelay: row.retryDelay ?? undefined,
|
|
383
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
369
384
|
createdAt: new Date(row.createdAt),
|
|
370
385
|
updatedAt: new Date(row.updatedAt),
|
|
371
386
|
};
|
|
@@ -420,6 +435,18 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
420
435
|
const existingStepNames = new Set(result.map((r) => r.stepName));
|
|
421
436
|
return nodeIds.filter((id) => !existingStepNames.has(id));
|
|
422
437
|
}
|
|
438
|
+
async getStepInstances(runId) {
|
|
439
|
+
const rows = await this.db
|
|
440
|
+
.selectFrom('workflowStep')
|
|
441
|
+
.select(['stepName', 'status', 'fromStepName'])
|
|
442
|
+
.where('workflowRunId', '=', runId)
|
|
443
|
+
.execute();
|
|
444
|
+
return rows.map((r) => ({
|
|
445
|
+
stepName: r.stepName,
|
|
446
|
+
status: r.status,
|
|
447
|
+
fromStepName: r.fromStepName ?? undefined,
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
423
450
|
async getNodeResults(runId, nodeIds) {
|
|
424
451
|
if (nodeIds.length === 0)
|
|
425
452
|
return {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/kysely",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.18",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"prepublishOnly": "yarn build"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@pikku/core": "^0.12.
|
|
23
|
+
"@pikku/core": "^0.12.39"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"kysely": "^0.29.0"
|
package/src/kysely-tables.ts
CHANGED
|
@@ -103,6 +103,29 @@ describe('KyselyWorkflowMirror — step lifecycle', () => {
|
|
|
103
103
|
assert.equal(history[0]!.status, 'pending')
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
+
test('getRunHistory surfaces step provenance (fromStepName)', async () => {
|
|
107
|
+
// The mirror persists fromStepName and getRunHistory reads it back, so the
|
|
108
|
+
// run history reconstructs the walked path (entry has none, the next step
|
|
109
|
+
// records its predecessor).
|
|
110
|
+
await seedStep(STEP_ID, 'begin')
|
|
111
|
+
await mirror.insertStepState(RUN_ID, {
|
|
112
|
+
stepId: `${STEP_ID}-2`,
|
|
113
|
+
stepName: 'next',
|
|
114
|
+
rpcName: 'rpc.fn',
|
|
115
|
+
data: { x: 2 },
|
|
116
|
+
status: 'pending',
|
|
117
|
+
attemptCount: 1,
|
|
118
|
+
fromStepName: 'begin',
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
updatedAt: new Date(),
|
|
121
|
+
} as any)
|
|
122
|
+
|
|
123
|
+
const history = await runService.getRunHistory(RUN_ID)
|
|
124
|
+
const from = new Map(history.map((h) => [h.stepName, h.fromStepName]))
|
|
125
|
+
assert.equal(from.get('begin'), undefined, 'entry step has no predecessor')
|
|
126
|
+
assert.equal(from.get('next'), 'begin', 'next records its predecessor')
|
|
127
|
+
})
|
|
128
|
+
|
|
106
129
|
test('running → succeeded updates the in-place history row', async () => {
|
|
107
130
|
await seedStep()
|
|
108
131
|
await mirror.setStepRunning(STEP_ID)
|
|
@@ -79,6 +79,7 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
79
79
|
.addColumn('branch_taken', 'text')
|
|
80
80
|
.addColumn('retries', 'integer')
|
|
81
81
|
.addColumn('retry_delay', 'text')
|
|
82
|
+
.addColumn('from_step_name', 'text')
|
|
82
83
|
.addColumn('created_at', 'timestamp', (col) =>
|
|
83
84
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
84
85
|
)
|
|
@@ -91,6 +92,15 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
91
92
|
])
|
|
92
93
|
.execute()
|
|
93
94
|
|
|
95
|
+
// Additive: backfill the column on mirror tables created before from_step_name.
|
|
96
|
+
await this.db.schema
|
|
97
|
+
.alterTable('workflow_step')
|
|
98
|
+
.addColumn('from_step_name', 'text')
|
|
99
|
+
.execute()
|
|
100
|
+
.catch(() => {
|
|
101
|
+
// Column already exists (fresh tables include it above) — nothing to do.
|
|
102
|
+
})
|
|
103
|
+
|
|
94
104
|
await this.db.schema
|
|
95
105
|
.createTable('workflow_step_history')
|
|
96
106
|
.ifNotExists()
|
|
@@ -202,6 +212,7 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
202
212
|
status: step.status,
|
|
203
213
|
retries: step.retries ?? null,
|
|
204
214
|
retryDelay: step.retryDelay?.toString() ?? null,
|
|
215
|
+
fromStepName: step.fromStepName ?? null,
|
|
205
216
|
createdAt: now,
|
|
206
217
|
updatedAt: now,
|
|
207
218
|
})
|
|
@@ -192,6 +192,7 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
192
192
|
.select([
|
|
193
193
|
's.workflowStepId',
|
|
194
194
|
's.stepName',
|
|
195
|
+
's.fromStepName',
|
|
195
196
|
's.retries',
|
|
196
197
|
's.retryDelay',
|
|
197
198
|
'h.status',
|
|
@@ -215,6 +216,7 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
215
216
|
return {
|
|
216
217
|
stepId,
|
|
217
218
|
stepName: row.stepName,
|
|
219
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
218
220
|
status: row.status as StepState['status'],
|
|
219
221
|
result: parseJson(row.result),
|
|
220
222
|
error: parseJson(row.error),
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type WorkflowRun,
|
|
6
6
|
type WorkflowRunWire,
|
|
7
7
|
type StepState,
|
|
8
|
+
type StepStatus,
|
|
8
9
|
type WorkflowStatus,
|
|
9
10
|
type WorkflowVersionStatus,
|
|
10
11
|
} from '@pikku/core/workflow'
|
|
@@ -79,6 +80,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
79
80
|
.addColumn('branch_taken', 'text')
|
|
80
81
|
.addColumn('retries', 'integer')
|
|
81
82
|
.addColumn('retry_delay', 'text')
|
|
83
|
+
.addColumn('from_step_name', 'text')
|
|
82
84
|
.addColumn('created_at', 'timestamp', (col) =>
|
|
83
85
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
84
86
|
)
|
|
@@ -91,6 +93,15 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
91
93
|
])
|
|
92
94
|
.execute()
|
|
93
95
|
|
|
96
|
+
// Additive: backfill the column on tables created before from_step_name.
|
|
97
|
+
await this.db.schema
|
|
98
|
+
.alterTable('workflow_step')
|
|
99
|
+
.addColumn('from_step_name', 'text')
|
|
100
|
+
.execute()
|
|
101
|
+
.catch(() => {
|
|
102
|
+
// Column already exists (fresh tables include it above) — nothing to do.
|
|
103
|
+
})
|
|
104
|
+
|
|
94
105
|
await this.db.schema
|
|
95
106
|
.createTable('workflow_step_history')
|
|
96
107
|
.ifNotExists()
|
|
@@ -196,7 +207,8 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
196
207
|
stepName: string,
|
|
197
208
|
rpcName: string | null,
|
|
198
209
|
data: any,
|
|
199
|
-
stepOptions?: { retries?: number; retryDelay?: string | number }
|
|
210
|
+
stepOptions?: { retries?: number; retryDelay?: string | number },
|
|
211
|
+
fromStepName?: string
|
|
200
212
|
): Promise<StepState> {
|
|
201
213
|
const stepId = crypto.randomUUID()
|
|
202
214
|
const now = new Date()
|
|
@@ -212,6 +224,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
212
224
|
status: 'pending',
|
|
213
225
|
retries: stepOptions?.retries ?? null,
|
|
214
226
|
retryDelay: stepOptions?.retryDelay?.toString() ?? null,
|
|
227
|
+
fromStepName: fromStepName ?? null,
|
|
215
228
|
createdAt: now,
|
|
216
229
|
updatedAt: now,
|
|
217
230
|
})
|
|
@@ -227,6 +240,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
227
240
|
attemptCount: 1,
|
|
228
241
|
retries: stepOptions?.retries,
|
|
229
242
|
retryDelay: stepOptions?.retryDelay?.toString(),
|
|
243
|
+
fromStepName,
|
|
230
244
|
createdAt: now,
|
|
231
245
|
updatedAt: now,
|
|
232
246
|
}
|
|
@@ -242,6 +256,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
242
256
|
's.error',
|
|
243
257
|
's.retries',
|
|
244
258
|
's.retryDelay',
|
|
259
|
+
's.fromStepName',
|
|
245
260
|
's.createdAt',
|
|
246
261
|
's.updatedAt',
|
|
247
262
|
])
|
|
@@ -274,6 +289,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
274
289
|
attemptCount: Number(row.attemptCount),
|
|
275
290
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
276
291
|
retryDelay: row.retryDelay ?? undefined,
|
|
292
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
277
293
|
createdAt: new Date(row.createdAt),
|
|
278
294
|
updatedAt: new Date(row.updatedAt),
|
|
279
295
|
}
|
|
@@ -303,7 +319,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
303
319
|
if (latestHistory) {
|
|
304
320
|
await this.db
|
|
305
321
|
.updateTable('workflowStepHistory')
|
|
306
|
-
.set({ status: 'running' })
|
|
322
|
+
.set({ status: 'running', runningAt: new Date() })
|
|
307
323
|
.where('historyId', '=', latestHistory.historyId)
|
|
308
324
|
.execute()
|
|
309
325
|
}
|
|
@@ -401,7 +417,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
401
417
|
if (latestHistory) {
|
|
402
418
|
await this.db
|
|
403
419
|
.updateTable('workflowStepHistory')
|
|
404
|
-
.set({ status: 'succeeded', result: resultJson })
|
|
420
|
+
.set({ status: 'succeeded', result: resultJson, succeededAt: new Date() })
|
|
405
421
|
.where('historyId', '=', latestHistory.historyId)
|
|
406
422
|
.execute()
|
|
407
423
|
}
|
|
@@ -440,7 +456,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
440
456
|
if (latestHistory) {
|
|
441
457
|
await this.db
|
|
442
458
|
.updateTable('workflowStepHistory')
|
|
443
|
-
.set({ status: 'failed', error: errorJson })
|
|
459
|
+
.set({ status: 'failed', error: errorJson, failedAt: new Date() })
|
|
444
460
|
.where('historyId', '=', latestHistory.historyId)
|
|
445
461
|
.execute()
|
|
446
462
|
}
|
|
@@ -467,6 +483,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
467
483
|
's.error',
|
|
468
484
|
's.retries',
|
|
469
485
|
's.retryDelay',
|
|
486
|
+
's.fromStepName',
|
|
470
487
|
's.createdAt',
|
|
471
488
|
's.updatedAt',
|
|
472
489
|
])
|
|
@@ -492,6 +509,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
492
509
|
attemptCount: Number(row.attemptCount),
|
|
493
510
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
494
511
|
retryDelay: row.retryDelay ?? undefined,
|
|
512
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
495
513
|
createdAt: new Date(row.createdAt),
|
|
496
514
|
updatedAt: new Date(row.updatedAt),
|
|
497
515
|
}
|
|
@@ -568,6 +586,21 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
568
586
|
return nodeIds.filter((id) => !existingStepNames.has(id))
|
|
569
587
|
}
|
|
570
588
|
|
|
589
|
+
async getStepInstances(runId: string): Promise<
|
|
590
|
+
Array<{ stepName: string; status: StepStatus; fromStepName?: string }>
|
|
591
|
+
> {
|
|
592
|
+
const rows = await this.db
|
|
593
|
+
.selectFrom('workflowStep')
|
|
594
|
+
.select(['stepName', 'status', 'fromStepName'])
|
|
595
|
+
.where('workflowRunId', '=', runId)
|
|
596
|
+
.execute()
|
|
597
|
+
return rows.map((r) => ({
|
|
598
|
+
stepName: r.stepName,
|
|
599
|
+
status: r.status as StepStatus,
|
|
600
|
+
fromStepName: r.fromStepName ?? undefined,
|
|
601
|
+
}))
|
|
602
|
+
}
|
|
603
|
+
|
|
571
604
|
async getNodeResults(
|
|
572
605
|
runId: string,
|
|
573
606
|
nodeIds: string[]
|