@pikku/kysely 0.13.24 → 0.13.28
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 +271 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +3 -1
- package/dist/src/kysely-analytics-service.d.ts +30 -0
- package/dist/src/kysely-analytics-service.js +80 -0
- package/dist/src/kysely-feature-flag-store.d.ts +37 -0
- package/dist/src/kysely-feature-flag-store.js +239 -0
- package/dist/src/kysely-scope-service.d.ts +1 -0
- package/dist/src/kysely-scope-service.js +29 -0
- package/dist/src/kysely-tables.d.ts +45 -0
- package/dist/src/schema/analytics.schema.d.ts +17 -0
- package/dist/src/schema/analytics.schema.js +48 -0
- package/dist/src/schema/flag.schema.d.ts +16 -0
- package/dist/src/schema/flag.schema.js +62 -0
- package/dist/src/schema/index.d.ts +2 -0
- package/dist/src/schema/index.js +6 -0
- package/package.json +11 -6
- package/run-tests.sh +9 -1
- package/bin/pikku-kysely-pure.ts +0 -59
- package/dist/src/kysely-lock-vault.d.ts +0 -34
- package/dist/src/kysely-lock-vault.js +0 -77
- package/dist/src/schema/data-lock.schema.d.ts +0 -15
- package/dist/src/schema/data-lock.schema.js +0 -27
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/classification-crypto.test.ts +0 -143
- package/src/classification-crypto.ts +0 -163
- package/src/classification-plugin.pglite.test.ts +0 -226
- package/src/classification-plugin.test.ts +0 -265
- package/src/classification-plugin.ts +0 -249
- package/src/coercion-plugin.test.ts +0 -160
- package/src/coercion-plugin.ts +0 -187
- package/src/create-audited-kysely.test.ts +0 -560
- package/src/create-audited-kysely.ts +0 -254
- package/src/index.ts +0 -81
- package/src/kysely-agent-run-scores.ts +0 -47
- package/src/kysely-agent-run-service.ts +0 -244
- package/src/kysely-agent-run-state-service.ts +0 -200
- package/src/kysely-agent-storage-service.ts +0 -632
- package/src/kysely-audit-service.test.ts +0 -352
- package/src/kysely-audit-service.ts +0 -237
- package/src/kysely-channel-store.ts +0 -107
- package/src/kysely-credential-service.ts +0 -286
- package/src/kysely-deployment-service.ts +0 -163
- package/src/kysely-eventhub-store.ts +0 -53
- package/src/kysely-json.ts +0 -5
- package/src/kysely-scope-service.test.ts +0 -418
- package/src/kysely-scope-service.ts +0 -425
- package/src/kysely-secret-service.ts +0 -242
- package/src/kysely-services.test.ts +0 -405
- package/src/kysely-session-store.ts +0 -57
- package/src/kysely-tables.test.ts +0 -110
- package/src/kysely-tables.ts +0 -434
- package/src/kysely-virtual-user-run-store.test.ts +0 -369
- package/src/kysely-virtual-user-run-store.ts +0 -235
- package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
- package/src/kysely-virtual-user-schedule-store.ts +0 -187
- package/src/kysely-webhook-service.test.ts +0 -153
- package/src/kysely-webhook-service.ts +0 -139
- package/src/kysely-workflow-mirror.test.ts +0 -259
- package/src/kysely-workflow-mirror.ts +0 -374
- package/src/kysely-workflow-run-service.ts +0 -305
- package/src/kysely-workflow-service.test.ts +0 -544
- package/src/kysely-workflow-service.ts +0 -820
- package/src/schema/agent.schema.ts +0 -162
- package/src/schema/audit.schema.ts +0 -57
- package/src/schema/channel.schema.ts +0 -38
- package/src/schema/credential.schema.ts +0 -58
- package/src/schema/deployment.schema.ts +0 -47
- package/src/schema/index.ts +0 -441
- package/src/schema/pikku-schema.types.ts +0 -169
- package/src/schema/schema.test.ts +0 -591
- package/src/schema/scope.schema.ts +0 -110
- package/src/schema/secret.schema.ts +0 -49
- package/src/schema/session.schema.ts +0 -20
- package/src/schema/virtual-user-schedule.schema.ts +0 -48
- package/src/schema/virtual-user.schema.ts +0 -106
- package/src/schema/webhook.schema.ts +0 -58
- package/src/schema/workflow.schema.ts +0 -150
- package/src/serialize-plugin.ts +0 -233
- package/tsconfig.json +0 -17
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { CamelCasePlugin, Kysely, SqliteDialect, sql } from 'kysely'
|
|
4
|
-
import { SerializePlugin } from './serialize-plugin.js'
|
|
5
|
-
import Database from 'better-sqlite3'
|
|
6
|
-
|
|
7
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
8
|
-
import { KyselyWorkflowMirror } from './kysely-workflow-mirror.js'
|
|
9
|
-
import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
|
|
10
|
-
import { applyPikkuSchemas, workflowSchema } from './schema/index.js'
|
|
11
|
-
|
|
12
|
-
let db: Kysely<KyselyPikkuDB>
|
|
13
|
-
let mirror: KyselyWorkflowMirror
|
|
14
|
-
let runService: KyselyWorkflowRunService
|
|
15
|
-
|
|
16
|
-
const RUN_ID = 'run-aaaa'
|
|
17
|
-
const STEP_ID = 'step-bbbb'
|
|
18
|
-
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
db = new Kysely<KyselyPikkuDB>({
|
|
21
|
-
dialect: new SqliteDialect({ database: new Database(':memory:') }),
|
|
22
|
-
plugins: [new CamelCasePlugin(), new SerializePlugin()],
|
|
23
|
-
})
|
|
24
|
-
await applyPikkuSchemas(db, [workflowSchema])
|
|
25
|
-
mirror = new KyselyWorkflowMirror(db)
|
|
26
|
-
await mirror.init()
|
|
27
|
-
runService = new KyselyWorkflowRunService(db)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
afterEach(async () => {
|
|
31
|
-
await db.destroy()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
async function seedRun(opts?: { input?: any; inline?: boolean }) {
|
|
35
|
-
await mirror.createRun(
|
|
36
|
-
RUN_ID,
|
|
37
|
-
'wf',
|
|
38
|
-
opts?.input ?? { foo: 1 },
|
|
39
|
-
opts?.inline ?? false,
|
|
40
|
-
'hash1',
|
|
41
|
-
{ type: 'do', id: RUN_ID } as any
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function seedStep(stepId = STEP_ID, stepName = 'step1') {
|
|
46
|
-
await mirror.insertStepState(RUN_ID, {
|
|
47
|
-
stepId,
|
|
48
|
-
stepName,
|
|
49
|
-
rpcName: 'rpc.fn',
|
|
50
|
-
data: { x: 1 },
|
|
51
|
-
status: 'pending',
|
|
52
|
-
attemptCount: 1,
|
|
53
|
-
createdAt: new Date(),
|
|
54
|
-
updatedAt: new Date(),
|
|
55
|
-
} as any)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
describe('KyselyWorkflowMirror — runs', () => {
|
|
59
|
-
test('createRun is visible to KyselyWorkflowRunService.getRun', async () => {
|
|
60
|
-
await seedRun({ input: { hello: 'world' } })
|
|
61
|
-
const run = await runService.getRun(RUN_ID)
|
|
62
|
-
assert.ok(run)
|
|
63
|
-
assert.equal(run.workflow, 'wf')
|
|
64
|
-
assert.equal(run.status, 'running')
|
|
65
|
-
assert.deepEqual(run.input, { hello: 'world' })
|
|
66
|
-
assert.equal(run.graphHash, 'hash1')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('updateRunStatus reflects in getRun', async () => {
|
|
70
|
-
await seedRun()
|
|
71
|
-
await mirror.updateRunStatus(RUN_ID, 'completed', { ok: true })
|
|
72
|
-
const run = await runService.getRun(RUN_ID)
|
|
73
|
-
assert.equal(run!.status, 'completed')
|
|
74
|
-
assert.deepEqual(run!.output, { ok: true })
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('listRuns surfaces mirrored runs', async () => {
|
|
78
|
-
await seedRun()
|
|
79
|
-
const runs = await runService.listRuns()
|
|
80
|
-
assert.equal(runs.length, 1)
|
|
81
|
-
assert.equal(runs[0]!.id, RUN_ID)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('getDistinctWorkflowNames returns mirrored workflow names', async () => {
|
|
85
|
-
await seedRun()
|
|
86
|
-
const names = await runService.getDistinctWorkflowNames()
|
|
87
|
-
assert.deepEqual(names, ['wf'])
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
describe('KyselyWorkflowMirror — step lifecycle', () => {
|
|
92
|
-
beforeEach(async () => {
|
|
93
|
-
await seedRun()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('insertStepState creates step + initial pending history', async () => {
|
|
97
|
-
await seedStep()
|
|
98
|
-
const steps = await runService.getRunSteps(RUN_ID)
|
|
99
|
-
assert.equal(steps.length, 1)
|
|
100
|
-
assert.equal(steps[0]!.stepName, 'step1')
|
|
101
|
-
assert.equal(steps[0]!.status, 'pending')
|
|
102
|
-
|
|
103
|
-
const history = await runService.getRunHistory(RUN_ID)
|
|
104
|
-
assert.equal(history.length, 1)
|
|
105
|
-
assert.equal(history[0]!.status, 'pending')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test('getRunHistory surfaces step provenance (fromStepName)', async () => {
|
|
109
|
-
// The mirror persists fromStepName and getRunHistory reads it back, so the
|
|
110
|
-
// run history reconstructs the walked path (entry has none, the next step
|
|
111
|
-
// records its predecessor).
|
|
112
|
-
await seedStep(STEP_ID, 'begin')
|
|
113
|
-
await mirror.insertStepState(RUN_ID, {
|
|
114
|
-
stepId: `${STEP_ID}-2`,
|
|
115
|
-
stepName: 'next',
|
|
116
|
-
rpcName: 'rpc.fn',
|
|
117
|
-
data: { x: 2 },
|
|
118
|
-
status: 'pending',
|
|
119
|
-
attemptCount: 1,
|
|
120
|
-
fromStepName: 'begin',
|
|
121
|
-
createdAt: new Date(),
|
|
122
|
-
updatedAt: new Date(),
|
|
123
|
-
} as any)
|
|
124
|
-
|
|
125
|
-
const history = await runService.getRunHistory(RUN_ID)
|
|
126
|
-
const from = new Map(history.map((h) => [h.stepName, h.fromStepName]))
|
|
127
|
-
assert.equal(from.get('begin'), undefined, 'entry step has no predecessor')
|
|
128
|
-
assert.equal(from.get('next'), 'begin', 'next records its predecessor')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('running → succeeded updates the in-place history row', async () => {
|
|
132
|
-
await seedStep()
|
|
133
|
-
await mirror.setStepRunning(STEP_ID)
|
|
134
|
-
await mirror.setStepResult(STEP_ID, { value: 42 })
|
|
135
|
-
|
|
136
|
-
const steps = await runService.getRunSteps(RUN_ID)
|
|
137
|
-
assert.equal(steps[0]!.status, 'succeeded')
|
|
138
|
-
assert.deepEqual(steps[0]!.result, { value: 42 })
|
|
139
|
-
|
|
140
|
-
const history = await runService.getRunHistory(RUN_ID)
|
|
141
|
-
assert.equal(history.length, 1, 'updates collapse onto latest history row')
|
|
142
|
-
assert.equal(history[0]!.status, 'succeeded')
|
|
143
|
-
assert.deepEqual(history[0]!.result, { value: 42 })
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
test('setStepError stores serialized error', async () => {
|
|
147
|
-
await seedStep()
|
|
148
|
-
await mirror.setStepError(STEP_ID, {
|
|
149
|
-
message: 'boom',
|
|
150
|
-
stack: 'stack',
|
|
151
|
-
code: 'EBOOM',
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
const steps = await runService.getRunSteps(RUN_ID)
|
|
155
|
-
assert.equal(steps[0]!.status, 'failed')
|
|
156
|
-
assert.equal(steps[0]!.error?.message, 'boom')
|
|
157
|
-
assert.equal(steps[0]!.error?.code, 'EBOOM')
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test('createRetryAttempt appends a new history row + clears result/error', async () => {
|
|
161
|
-
await seedStep()
|
|
162
|
-
await mirror.setStepError(STEP_ID, { message: 'first try', code: 'X' })
|
|
163
|
-
|
|
164
|
-
await mirror.createRetryAttempt(STEP_ID, {
|
|
165
|
-
stepId: STEP_ID,
|
|
166
|
-
stepName: 'step1',
|
|
167
|
-
status: 'pending',
|
|
168
|
-
attemptCount: 2,
|
|
169
|
-
createdAt: new Date(),
|
|
170
|
-
updatedAt: new Date(),
|
|
171
|
-
} as any)
|
|
172
|
-
|
|
173
|
-
const steps = await runService.getRunSteps(RUN_ID)
|
|
174
|
-
assert.equal(steps[0]!.status, 'pending')
|
|
175
|
-
assert.equal(steps[0]!.error, undefined)
|
|
176
|
-
assert.equal(steps[0]!.result, undefined)
|
|
177
|
-
|
|
178
|
-
const history = await runService.getRunHistory(RUN_ID)
|
|
179
|
-
// pending (initial — collapsed onto failed) + pending (retry append)
|
|
180
|
-
assert.equal(history.length, 2)
|
|
181
|
-
assert.equal(history[0]!.status, 'failed')
|
|
182
|
-
assert.equal(history[1]!.status, 'pending')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
test('setStepChildRunId persists', async () => {
|
|
186
|
-
await seedStep()
|
|
187
|
-
await mirror.setStepChildRunId(STEP_ID, 'child-run-1')
|
|
188
|
-
const steps = await runService.getRunSteps(RUN_ID)
|
|
189
|
-
assert.equal((steps[0] as any).childRunId, 'child-run-1')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
test('setBranchTaken persists the branch key', async () => {
|
|
193
|
-
await seedStep()
|
|
194
|
-
await mirror.setBranchTaken(STEP_ID, 'left')
|
|
195
|
-
const row = await db
|
|
196
|
-
.selectFrom('workflowStep')
|
|
197
|
-
.select('branchTaken')
|
|
198
|
-
.where('workflowStepId', '=', STEP_ID)
|
|
199
|
-
.executeTakeFirst()
|
|
200
|
-
assert.equal(row?.branchTaken, 'left')
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
describe('KyselyWorkflowMirror — run state', () => {
|
|
205
|
-
beforeEach(async () => {
|
|
206
|
-
await seedRun()
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
test('updateRunState merges values', async () => {
|
|
210
|
-
await mirror.updateRunState(RUN_ID, 'counter', 1)
|
|
211
|
-
await mirror.updateRunState(RUN_ID, 'flag', true)
|
|
212
|
-
const row = await db
|
|
213
|
-
.selectFrom('workflowRuns')
|
|
214
|
-
.select('state')
|
|
215
|
-
.where('workflowRunId', '=', RUN_ID)
|
|
216
|
-
.executeTakeFirst()
|
|
217
|
-
const state =
|
|
218
|
-
typeof row?.state === 'string' ? JSON.parse(row.state) : row?.state
|
|
219
|
-
assert.deepEqual(state, { counter: 1, flag: true })
|
|
220
|
-
})
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
describe('KyselyWorkflowMirror — workflow versions', () => {
|
|
224
|
-
test('upsertWorkflowVersion is idempotent on (name, hash)', async () => {
|
|
225
|
-
await mirror.upsertWorkflowVersion('wf', 'h1', { v: 1 }, 'graph')
|
|
226
|
-
await mirror.upsertWorkflowVersion('wf', 'h1', { v: 2 }, 'graph')
|
|
227
|
-
const v = await runService.getWorkflowVersion('wf', 'h1')
|
|
228
|
-
assert.deepEqual(v?.graph, { v: 1 }, 'first write wins (doNothing)')
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
test('updateWorkflowVersionStatus mutates status', async () => {
|
|
232
|
-
await mirror.upsertWorkflowVersion('wf', 'h1', {}, 'graph', 'active')
|
|
233
|
-
await mirror.updateWorkflowVersionStatus('wf', 'h1', 'archived')
|
|
234
|
-
const row = await db
|
|
235
|
-
.selectFrom('workflowVersions')
|
|
236
|
-
.select('status')
|
|
237
|
-
.where('workflowName', '=', 'wf')
|
|
238
|
-
.where('graphHash', '=', 'h1')
|
|
239
|
-
.executeTakeFirst()
|
|
240
|
-
assert.equal(row?.status, 'archived')
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
describe('KyselyWorkflowMirror — init', () => {
|
|
245
|
-
test('init is idempotent', async () => {
|
|
246
|
-
await mirror.init()
|
|
247
|
-
await mirror.init()
|
|
248
|
-
// No-op on second call; just shouldn't throw.
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
test('cohabits with KyselyWorkflowService, which declares the same tables', async () => {
|
|
252
|
-
// Both services own the workflow schema, so both call init() against the
|
|
253
|
-
// same database. The second one to run must find the tables and stop, not
|
|
254
|
-
// try to create them again. Importing the other service needs the
|
|
255
|
-
// singleton-services bootstrap, so a fresh instance stands in for it.
|
|
256
|
-
const second = new KyselyWorkflowMirror(db)
|
|
257
|
-
await assert.doesNotReject(second.init())
|
|
258
|
-
})
|
|
259
|
-
})
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import type { SerializedError } from '@pikku/core/errors'
|
|
2
|
-
import type {
|
|
3
|
-
StepState,
|
|
4
|
-
WorkflowRunMirror,
|
|
5
|
-
WorkflowRunWire,
|
|
6
|
-
WorkflowStatus,
|
|
7
|
-
WorkflowPlannedStep,
|
|
8
|
-
WorkflowVersionStatus,
|
|
9
|
-
} from '@pikku/core/workflow'
|
|
10
|
-
import type { Kysely } from 'kysely'
|
|
11
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
12
|
-
import { requirePikkuSchema } from './schema/index.js'
|
|
13
|
-
import { workflowSchema } from './schema/workflow.schema.js'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Kysely-backed `WorkflowRunMirror`.
|
|
17
|
-
*
|
|
18
|
-
* Forwards executor writes to the same `workflow_runs` / `workflow_step` /
|
|
19
|
-
* `workflow_step_history` / `workflow_versions` tables that
|
|
20
|
-
* `KyselyWorkflowService` uses, so a `KyselyWorkflowRunService` reading
|
|
21
|
-
* those tables will see runs driven by any executor (Cloudflare Durable
|
|
22
|
-
* Object, Redis, MongoDB, in-memory) — not just kysely-driven runs.
|
|
23
|
-
*
|
|
24
|
-
* Errors thrown here are caught by the executor's `safeMirror` wrapper
|
|
25
|
-
* and logged, so a mirror outage cannot break a running workflow.
|
|
26
|
-
*/
|
|
27
|
-
export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
28
|
-
private initialized = false
|
|
29
|
-
|
|
30
|
-
constructor(protected db: Kysely<KyselyPikkuDB>) {}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Create the underlying tables if none of them exist yet.
|
|
34
|
-
*
|
|
35
|
-
* Safe to call from either the mirror or `KyselyWorkflowService.init()` —
|
|
36
|
-
* both apply the same `workflowSchema`, and `requirePikkuSchema` is a no-op
|
|
37
|
-
* once the tables are there. A database holding only *part* of the schema
|
|
38
|
-
* throws rather than filling in the rest: something else owns those tables,
|
|
39
|
-
* and boot is not where that gets reconciled.
|
|
40
|
-
*/
|
|
41
|
-
public async init(): Promise<void> {
|
|
42
|
-
if (this.initialized) return
|
|
43
|
-
await requirePikkuSchema(this.db, workflowSchema)
|
|
44
|
-
this.initialized = true
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async createRun(
|
|
48
|
-
runId: string,
|
|
49
|
-
workflowName: string,
|
|
50
|
-
input: any,
|
|
51
|
-
inline: boolean,
|
|
52
|
-
graphHash: string,
|
|
53
|
-
wire: WorkflowRunWire,
|
|
54
|
-
options?: {
|
|
55
|
-
deterministic?: boolean
|
|
56
|
-
plannedSteps?: WorkflowPlannedStep[]
|
|
57
|
-
}
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
await this.db
|
|
60
|
-
.insertInto('workflowRuns')
|
|
61
|
-
.values({
|
|
62
|
-
workflowRunId: runId,
|
|
63
|
-
workflow: workflowName,
|
|
64
|
-
status: 'running',
|
|
65
|
-
input: JSON.stringify(input),
|
|
66
|
-
inline,
|
|
67
|
-
graphHash: graphHash ?? null,
|
|
68
|
-
deterministic: options?.deterministic ?? false,
|
|
69
|
-
plannedSteps: options?.plannedSteps
|
|
70
|
-
? JSON.stringify(options.plannedSteps)
|
|
71
|
-
: null,
|
|
72
|
-
wire: wire ? JSON.stringify(wire) : null,
|
|
73
|
-
})
|
|
74
|
-
.execute()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async updateRunStatus(
|
|
78
|
-
id: string,
|
|
79
|
-
status: WorkflowStatus,
|
|
80
|
-
output?: any,
|
|
81
|
-
error?: SerializedError
|
|
82
|
-
): Promise<void> {
|
|
83
|
-
await this.db
|
|
84
|
-
.updateTable('workflowRuns')
|
|
85
|
-
.set({
|
|
86
|
-
status,
|
|
87
|
-
output: output !== undefined ? JSON.stringify(output) : null,
|
|
88
|
-
error: error !== undefined ? JSON.stringify(error) : null,
|
|
89
|
-
updatedAt: new Date(),
|
|
90
|
-
})
|
|
91
|
-
.where('workflowRunId', '=', id)
|
|
92
|
-
.execute()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async insertStepState(
|
|
96
|
-
runId: string,
|
|
97
|
-
step: StepState & {
|
|
98
|
-
stepName: string
|
|
99
|
-
rpcName: string | null
|
|
100
|
-
data: any
|
|
101
|
-
}
|
|
102
|
-
): Promise<void> {
|
|
103
|
-
const now = step.createdAt ?? new Date()
|
|
104
|
-
|
|
105
|
-
await this.db
|
|
106
|
-
.insertInto('workflowStep')
|
|
107
|
-
.values({
|
|
108
|
-
workflowStepId: step.stepId,
|
|
109
|
-
workflowRunId: runId,
|
|
110
|
-
stepName: step.stepName,
|
|
111
|
-
rpcName: step.rpcName,
|
|
112
|
-
data: step.data != null ? JSON.stringify(step.data) : null,
|
|
113
|
-
status: step.status,
|
|
114
|
-
retries: step.retries ?? null,
|
|
115
|
-
retryDelay: step.retryDelay?.toString() ?? null,
|
|
116
|
-
fromStepName: step.fromStepName ?? null,
|
|
117
|
-
currentAttempt: 1,
|
|
118
|
-
createdAt: now,
|
|
119
|
-
updatedAt: now,
|
|
120
|
-
})
|
|
121
|
-
.execute()
|
|
122
|
-
|
|
123
|
-
await this.insertHistoryRecord(step.stepId, step.status, 1)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async setStepRunning(stepId: string): Promise<void> {
|
|
127
|
-
await this.updateStepStatus(stepId, 'running')
|
|
128
|
-
await this.appendOrUpdateLatestHistory(stepId, 'running')
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async setStepScheduled(stepId: string): Promise<void> {
|
|
132
|
-
await this.updateStepStatus(stepId, 'scheduled')
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async setStepResult(stepId: string, result: any): Promise<void> {
|
|
136
|
-
const resultJson = JSON.stringify(result)
|
|
137
|
-
await this.db
|
|
138
|
-
.updateTable('workflowStep')
|
|
139
|
-
.set({
|
|
140
|
-
status: 'succeeded',
|
|
141
|
-
result: resultJson,
|
|
142
|
-
error: null,
|
|
143
|
-
updatedAt: new Date(),
|
|
144
|
-
})
|
|
145
|
-
.where('workflowStepId', '=', stepId)
|
|
146
|
-
.execute()
|
|
147
|
-
await this.appendOrUpdateLatestHistory(stepId, 'succeeded', resultJson)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async setStepChildRunId(stepId: string, childRunId: string): Promise<void> {
|
|
151
|
-
await this.db
|
|
152
|
-
.updateTable('workflowStep')
|
|
153
|
-
.set({ childRunId, updatedAt: new Date() })
|
|
154
|
-
.where('workflowStepId', '=', stepId)
|
|
155
|
-
.execute()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async setStepError(stepId: string, error: SerializedError): Promise<void> {
|
|
159
|
-
const errorJson = JSON.stringify(error)
|
|
160
|
-
await this.db
|
|
161
|
-
.updateTable('workflowStep')
|
|
162
|
-
.set({
|
|
163
|
-
status: 'failed',
|
|
164
|
-
error: errorJson,
|
|
165
|
-
result: null,
|
|
166
|
-
updatedAt: new Date(),
|
|
167
|
-
})
|
|
168
|
-
.where('workflowStepId', '=', stepId)
|
|
169
|
-
.execute()
|
|
170
|
-
await this.appendOrUpdateLatestHistory(
|
|
171
|
-
stepId,
|
|
172
|
-
'failed',
|
|
173
|
-
undefined,
|
|
174
|
-
errorJson
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async createRetryAttempt(
|
|
179
|
-
failedStepId: string,
|
|
180
|
-
newStep: StepState & { stepName: string }
|
|
181
|
-
): Promise<void> {
|
|
182
|
-
const previous = await this.db
|
|
183
|
-
.selectFrom('workflowStep')
|
|
184
|
-
.select('currentAttempt')
|
|
185
|
-
.where('workflowStepId', '=', failedStepId)
|
|
186
|
-
.executeTakeFirst()
|
|
187
|
-
const attempt = (previous?.currentAttempt ?? 0) + 1
|
|
188
|
-
|
|
189
|
-
await this.db
|
|
190
|
-
.updateTable('workflowStep')
|
|
191
|
-
.set({
|
|
192
|
-
status: newStep.status,
|
|
193
|
-
result: null,
|
|
194
|
-
error: null,
|
|
195
|
-
currentAttempt: attempt,
|
|
196
|
-
updatedAt: new Date(),
|
|
197
|
-
})
|
|
198
|
-
.where('workflowStepId', '=', failedStepId)
|
|
199
|
-
.execute()
|
|
200
|
-
await this.insertHistoryRecord(failedStepId, newStep.status, attempt)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async setBranchTaken(stepId: string, branchKey: string): Promise<void> {
|
|
204
|
-
await this.db
|
|
205
|
-
.updateTable('workflowStep')
|
|
206
|
-
.set({ branchTaken: branchKey, updatedAt: new Date() })
|
|
207
|
-
.where('workflowStepId', '=', stepId)
|
|
208
|
-
.execute()
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async updateRunState(
|
|
212
|
-
runId: string,
|
|
213
|
-
name: string,
|
|
214
|
-
value: unknown
|
|
215
|
-
): Promise<void> {
|
|
216
|
-
const row = await this.db
|
|
217
|
-
.selectFrom('workflowRuns')
|
|
218
|
-
.select('state')
|
|
219
|
-
.where('workflowRunId', '=', runId)
|
|
220
|
-
.executeTakeFirst()
|
|
221
|
-
|
|
222
|
-
const state: Record<string, unknown> =
|
|
223
|
-
row?.state != null
|
|
224
|
-
? typeof row.state === 'string'
|
|
225
|
-
? JSON.parse(row.state)
|
|
226
|
-
: row.state
|
|
227
|
-
: {}
|
|
228
|
-
state[name] = value
|
|
229
|
-
|
|
230
|
-
await this.db
|
|
231
|
-
.updateTable('workflowRuns')
|
|
232
|
-
.set({ state: JSON.stringify(state), updatedAt: new Date() })
|
|
233
|
-
.where('workflowRunId', '=', runId)
|
|
234
|
-
.execute()
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async upsertWorkflowVersion(
|
|
238
|
-
name: string,
|
|
239
|
-
graphHash: string,
|
|
240
|
-
graph: any,
|
|
241
|
-
source: string,
|
|
242
|
-
status: WorkflowVersionStatus = 'active'
|
|
243
|
-
): Promise<void> {
|
|
244
|
-
await this.db
|
|
245
|
-
.insertInto('workflowVersions')
|
|
246
|
-
.values({
|
|
247
|
-
workflowName: name,
|
|
248
|
-
graphHash,
|
|
249
|
-
graph: JSON.stringify(graph),
|
|
250
|
-
source,
|
|
251
|
-
status,
|
|
252
|
-
})
|
|
253
|
-
.onConflict((oc) => oc.columns(['workflowName', 'graphHash']).doNothing())
|
|
254
|
-
.execute()
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async updateWorkflowVersionStatus(
|
|
258
|
-
name: string,
|
|
259
|
-
graphHash: string,
|
|
260
|
-
status: WorkflowVersionStatus
|
|
261
|
-
): Promise<void> {
|
|
262
|
-
await this.db
|
|
263
|
-
.updateTable('workflowVersions')
|
|
264
|
-
.set({ status })
|
|
265
|
-
.where('workflowName', '=', name)
|
|
266
|
-
.where('graphHash', '=', graphHash)
|
|
267
|
-
.execute()
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ─── internals ────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
private async updateStepStatus(
|
|
273
|
-
stepId: string,
|
|
274
|
-
status: StepState['status']
|
|
275
|
-
): Promise<void> {
|
|
276
|
-
await this.db
|
|
277
|
-
.updateTable('workflowStep')
|
|
278
|
-
.set({ status, updatedAt: new Date() })
|
|
279
|
-
.where('workflowStepId', '=', stepId)
|
|
280
|
-
.execute()
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private async insertHistoryRecord(
|
|
284
|
-
stepId: string,
|
|
285
|
-
status: string,
|
|
286
|
-
attempt: number,
|
|
287
|
-
resultJson?: string | null,
|
|
288
|
-
errorJson?: string | null
|
|
289
|
-
): Promise<void> {
|
|
290
|
-
const now = new Date()
|
|
291
|
-
const values: Record<string, any> = {
|
|
292
|
-
historyId: crypto.randomUUID(),
|
|
293
|
-
workflowStepId: stepId,
|
|
294
|
-
status,
|
|
295
|
-
attempt,
|
|
296
|
-
result: resultJson ?? null,
|
|
297
|
-
error: errorJson ?? null,
|
|
298
|
-
createdAt: now,
|
|
299
|
-
}
|
|
300
|
-
const tsField = timestampFieldFor(status)
|
|
301
|
-
if (tsField) values[tsField] = now
|
|
302
|
-
|
|
303
|
-
await this.db
|
|
304
|
-
.insertInto('workflowStepHistory')
|
|
305
|
-
.values(values as any)
|
|
306
|
-
.execute()
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* The executor pattern is: `insertStepState` writes a 'pending' history
|
|
311
|
-
* row, then later transitions UPDATE that latest row in place rather
|
|
312
|
-
* than appending a new one — except for retry attempts which append.
|
|
313
|
-
*
|
|
314
|
-
* We mirror the same shape so `KyselyWorkflowRunService.getRunHistory`
|
|
315
|
-
* returns the same sequence the executor would have produced.
|
|
316
|
-
*/
|
|
317
|
-
private async appendOrUpdateLatestHistory(
|
|
318
|
-
stepId: string,
|
|
319
|
-
status: string,
|
|
320
|
-
resultJson?: string,
|
|
321
|
-
errorJson?: string
|
|
322
|
-
): Promise<void> {
|
|
323
|
-
const update: Record<string, any> = { status }
|
|
324
|
-
if (resultJson !== undefined) update.result = resultJson
|
|
325
|
-
if (errorJson !== undefined) update.error = errorJson
|
|
326
|
-
const tsField = timestampFieldFor(status)
|
|
327
|
-
if (tsField) update[tsField] = new Date()
|
|
328
|
-
|
|
329
|
-
const result = await this.db
|
|
330
|
-
.updateTable('workflowStepHistory')
|
|
331
|
-
.set(update)
|
|
332
|
-
.where('workflowStepId', '=', stepId)
|
|
333
|
-
// Correlate through the step row rather than sorting this table by
|
|
334
|
-
// createdAt: two attempts can land in the same millisecond, and the
|
|
335
|
-
// step row already knows which attempt is current.
|
|
336
|
-
.where('attempt', '=', (eb) =>
|
|
337
|
-
eb
|
|
338
|
-
.selectFrom('workflowStep')
|
|
339
|
-
.select('currentAttempt')
|
|
340
|
-
.where('workflowStepId', '=', stepId)
|
|
341
|
-
)
|
|
342
|
-
.executeTakeFirst()
|
|
343
|
-
|
|
344
|
-
if (Number(result?.numUpdatedRows ?? 0n) === 0) {
|
|
345
|
-
const step = await this.db
|
|
346
|
-
.selectFrom('workflowStep')
|
|
347
|
-
.select('currentAttempt')
|
|
348
|
-
.where('workflowStepId', '=', stepId)
|
|
349
|
-
.executeTakeFirst()
|
|
350
|
-
await this.insertHistoryRecord(
|
|
351
|
-
stepId,
|
|
352
|
-
status,
|
|
353
|
-
step?.currentAttempt ?? 1,
|
|
354
|
-
resultJson,
|
|
355
|
-
errorJson
|
|
356
|
-
)
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function timestampFieldFor(status: string): string | null {
|
|
362
|
-
switch (status) {
|
|
363
|
-
case 'running':
|
|
364
|
-
return 'runningAt'
|
|
365
|
-
case 'scheduled':
|
|
366
|
-
return 'scheduledAt'
|
|
367
|
-
case 'succeeded':
|
|
368
|
-
return 'succeededAt'
|
|
369
|
-
case 'failed':
|
|
370
|
-
return 'failedAt'
|
|
371
|
-
default:
|
|
372
|
-
return null
|
|
373
|
-
}
|
|
374
|
-
}
|