@pikku/kysely 0.13.5 → 0.13.6
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 +81 -0
- package/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/src/kysely-tables.d.ts +8 -0
- package/dist/src/kysely-workflow-mirror.js +37 -25
- package/dist/src/kysely-workflow-run-service.d.ts +0 -5
- package/dist/src/kysely-workflow-run-service.js +4 -16
- package/dist/src/kysely-workflow-service.d.ts +35 -6
- package/dist/src/kysely-workflow-service.js +194 -134
- package/dist/src/schema/workflow.schema.d.ts +4 -2
- package/dist/src/schema/workflow.schema.js +28 -2
- package/package.json +3 -2
- package/src/kysely-tables.ts +8 -0
- package/src/kysely-workflow-mirror.test.ts +1 -13
- package/src/kysely-workflow-mirror.ts +44 -20
- package/src/kysely-workflow-run-service.ts +7 -20
- package/src/kysely-workflow-service.test.ts +431 -0
- package/src/kysely-workflow-service.ts +212 -161
- package/src/schema/workflow.schema.ts +38 -2
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type WorkflowStatus,
|
|
11
11
|
type WorkflowVersionStatus,
|
|
12
12
|
} from '@pikku/core/workflow'
|
|
13
|
-
import type
|
|
13
|
+
import { sql, type Kysely } from 'kysely'
|
|
14
14
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
15
15
|
import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
|
|
16
16
|
import { parseJson } from './kysely-json.js'
|
|
@@ -112,12 +112,13 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
112
112
|
retries: stepOptions?.retries ?? null,
|
|
113
113
|
retryDelay: stepOptions?.retryDelay?.toString() ?? null,
|
|
114
114
|
fromStepName: fromStepName ?? null,
|
|
115
|
+
currentAttempt: 1,
|
|
115
116
|
createdAt: now,
|
|
116
117
|
updatedAt: now,
|
|
117
118
|
})
|
|
118
119
|
.execute()
|
|
119
120
|
|
|
120
|
-
await this.insertHistoryRecord(stepId, 'pending')
|
|
121
|
+
await this.insertHistoryRecord(stepId, 'pending', 1)
|
|
121
122
|
|
|
122
123
|
return {
|
|
123
124
|
stepId,
|
|
@@ -135,31 +136,24 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
135
136
|
|
|
136
137
|
async getStepState(runId: string, stepName: string): Promise<StepState> {
|
|
137
138
|
const row = await this.db
|
|
138
|
-
.selectFrom('workflowStep
|
|
139
|
+
.selectFrom('workflowStep')
|
|
139
140
|
.select([
|
|
140
|
-
'
|
|
141
|
-
'
|
|
142
|
-
'
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
'workflowStepId',
|
|
142
|
+
'status',
|
|
143
|
+
'result',
|
|
144
|
+
'error',
|
|
145
|
+
'retries',
|
|
146
|
+
'retryDelay',
|
|
147
|
+
'fromStepName',
|
|
148
|
+
// `current_attempt` is bumped alongside every history insert, so it is
|
|
149
|
+
// already the count this used to aggregate — and reading it keeps the
|
|
150
|
+
// engine's hottest query off a table that grows for the life of the run.
|
|
151
|
+
'currentAttempt',
|
|
152
|
+
'createdAt',
|
|
153
|
+
'updatedAt',
|
|
149
154
|
])
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
.selectFrom('workflowStepHistory')
|
|
153
|
-
.select(eb.fn.countAll<number>().as('cnt'))
|
|
154
|
-
.whereRef(
|
|
155
|
-
'workflowStepHistory.workflowStepId',
|
|
156
|
-
'=',
|
|
157
|
-
's.workflowStepId'
|
|
158
|
-
)
|
|
159
|
-
.as('attemptCount')
|
|
160
|
-
)
|
|
161
|
-
.where('s.workflowRunId', '=', runId)
|
|
162
|
-
.where('s.stepName', '=', stepName)
|
|
155
|
+
.where('workflowRunId', '=', runId)
|
|
156
|
+
.where('stepName', '=', stepName)
|
|
163
157
|
.executeTakeFirst()
|
|
164
158
|
|
|
165
159
|
if (!row) {
|
|
@@ -173,7 +167,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
173
167
|
status: row.status as StepState['status'],
|
|
174
168
|
result: parseJson(row.result),
|
|
175
169
|
error: parseJson(row.error),
|
|
176
|
-
attemptCount: Number(row.
|
|
170
|
+
attemptCount: Number(row.currentAttempt ?? 1),
|
|
177
171
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
178
172
|
retryDelay: row.retryDelay ?? undefined,
|
|
179
173
|
fromStepName: row.fromStepName ?? undefined,
|
|
@@ -182,6 +176,42 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
182
176
|
}
|
|
183
177
|
}
|
|
184
178
|
|
|
179
|
+
protected override async listStepStates(
|
|
180
|
+
runId: string
|
|
181
|
+
): Promise<Array<StepState & { stepName: string }>> {
|
|
182
|
+
const rows = await this.db
|
|
183
|
+
.selectFrom('workflowStep')
|
|
184
|
+
.select([
|
|
185
|
+
'workflowStepId',
|
|
186
|
+
'stepName',
|
|
187
|
+
'status',
|
|
188
|
+
'result',
|
|
189
|
+
'error',
|
|
190
|
+
'retries',
|
|
191
|
+
'retryDelay',
|
|
192
|
+
'fromStepName',
|
|
193
|
+
'currentAttempt',
|
|
194
|
+
'createdAt',
|
|
195
|
+
'updatedAt',
|
|
196
|
+
])
|
|
197
|
+
.where('workflowRunId', '=', runId)
|
|
198
|
+
.execute()
|
|
199
|
+
|
|
200
|
+
return rows.map((row) => ({
|
|
201
|
+
stepId: row.workflowStepId,
|
|
202
|
+
stepName: row.stepName,
|
|
203
|
+
status: row.status as StepState['status'],
|
|
204
|
+
result: parseJson(row.result),
|
|
205
|
+
error: parseJson(row.error),
|
|
206
|
+
attemptCount: Number(row.currentAttempt ?? 1),
|
|
207
|
+
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
208
|
+
retryDelay: row.retryDelay ?? undefined,
|
|
209
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
210
|
+
createdAt: new Date(row.createdAt),
|
|
211
|
+
updatedAt: new Date(row.updatedAt),
|
|
212
|
+
}))
|
|
213
|
+
}
|
|
214
|
+
|
|
185
215
|
async getRunHistory(
|
|
186
216
|
runId: string
|
|
187
217
|
): Promise<Array<StepState & { stepName: string }>> {
|
|
@@ -189,27 +219,9 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
protected async setStepRunningImpl(stepId: string): Promise<void> {
|
|
192
|
-
await this.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.where('workflowStepId', '=', stepId)
|
|
196
|
-
.execute()
|
|
197
|
-
|
|
198
|
-
const latestHistory = await this.db
|
|
199
|
-
.selectFrom('workflowStepHistory')
|
|
200
|
-
.select('historyId')
|
|
201
|
-
.where('workflowStepId', '=', stepId)
|
|
202
|
-
.orderBy('createdAt', 'desc')
|
|
203
|
-
.limit(1)
|
|
204
|
-
.executeTakeFirst()
|
|
205
|
-
|
|
206
|
-
if (latestHistory) {
|
|
207
|
-
await this.db
|
|
208
|
-
.updateTable('workflowStepHistory')
|
|
209
|
-
.set({ status: 'running', runningAt: new Date() })
|
|
210
|
-
.where('historyId', '=', latestHistory.historyId)
|
|
211
|
-
.execute()
|
|
212
|
-
}
|
|
222
|
+
await this.writeStepTransition(stepId, 'running', {
|
|
223
|
+
runningAt: new Date(),
|
|
224
|
+
})
|
|
213
225
|
}
|
|
214
226
|
|
|
215
227
|
protected async setStepScheduledImpl(stepId: string): Promise<void> {
|
|
@@ -220,9 +232,88 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
220
232
|
.execute()
|
|
221
233
|
}
|
|
222
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Move a step and its current history attempt to the same status in one
|
|
237
|
+
* transaction.
|
|
238
|
+
*
|
|
239
|
+
* Both halves matter: a crash between them used to leave the step row saying
|
|
240
|
+
* `succeeded` while its history row still said `running`, which silently
|
|
241
|
+
* corrupts `getRunHistory` and every timeline reconstruction built on it.
|
|
242
|
+
* The history row is targeted by its `attempt` rather than by a `LIMIT 1`
|
|
243
|
+
* over `created_at`, so a retry that lands in the same millisecond as the
|
|
244
|
+
* attempt it replaces still resolves the newer row.
|
|
245
|
+
*/
|
|
246
|
+
private async writeStepTransition(
|
|
247
|
+
stepId: string,
|
|
248
|
+
status: StepStatus,
|
|
249
|
+
historyValues: Record<string, unknown>
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const now = new Date()
|
|
252
|
+
const stepValues: Record<string, unknown> = { status, updatedAt: now }
|
|
253
|
+
if ('result' in historyValues) stepValues.result = historyValues.result
|
|
254
|
+
if ('error' in historyValues) stepValues.error = historyValues.error
|
|
255
|
+
// A step carries only its latest outcome, so the other half is cleared.
|
|
256
|
+
if (status === 'succeeded') stepValues.error = null
|
|
257
|
+
if (status === 'failed') stepValues.result = null
|
|
258
|
+
|
|
259
|
+
await this.db.transaction().execute(async (trx) => {
|
|
260
|
+
await trx
|
|
261
|
+
.updateTable('workflowStep')
|
|
262
|
+
.set({
|
|
263
|
+
...stepValues,
|
|
264
|
+
// A step that somehow reaches a transition without a live attempt
|
|
265
|
+
// addresses no history row at all, so give it one here. Written as an
|
|
266
|
+
// expression over the row's own column rather than a subquery, which
|
|
267
|
+
// MySQL would reject against the table being updated.
|
|
268
|
+
currentAttempt: sql`coalesce(current_attempt, 1)`,
|
|
269
|
+
} as any)
|
|
270
|
+
.where('workflowStepId', '=', stepId)
|
|
271
|
+
.execute()
|
|
272
|
+
|
|
273
|
+
const written = await trx
|
|
274
|
+
.updateTable('workflowStepHistory')
|
|
275
|
+
.set({ status, ...historyValues } as any)
|
|
276
|
+
.where('workflowStepId', '=', stepId)
|
|
277
|
+
// Correlate through the step row rather than a MAX() over this table:
|
|
278
|
+
// MySQL rejects a subquery that reads the table being updated, and a
|
|
279
|
+
// primary-key lookup is cheaper than an aggregate anyway.
|
|
280
|
+
.where('attempt', '=', (eb) =>
|
|
281
|
+
eb
|
|
282
|
+
.selectFrom('workflowStep')
|
|
283
|
+
.select('currentAttempt')
|
|
284
|
+
.where('workflowStepId', '=', stepId)
|
|
285
|
+
)
|
|
286
|
+
.executeTakeFirst()
|
|
287
|
+
|
|
288
|
+
// Matching nothing means the attempt has no history row to move, which
|
|
289
|
+
// would commit the step's new status with no record of it — precisely
|
|
290
|
+
// the divergence this transaction exists to prevent. Write the missing
|
|
291
|
+
// row instead of letting the pair drift apart.
|
|
292
|
+
if (Number(written?.numUpdatedRows ?? 0n) === 0) {
|
|
293
|
+
const step = await trx
|
|
294
|
+
.selectFrom('workflowStep')
|
|
295
|
+
.select('currentAttempt')
|
|
296
|
+
.where('workflowStepId', '=', stepId)
|
|
297
|
+
.executeTakeFirst()
|
|
298
|
+
await trx
|
|
299
|
+
.insertInto('workflowStepHistory')
|
|
300
|
+
.values({
|
|
301
|
+
historyId: crypto.randomUUID(),
|
|
302
|
+
workflowStepId: stepId,
|
|
303
|
+
status,
|
|
304
|
+
attempt: step?.currentAttempt ?? 1,
|
|
305
|
+
createdAt: now,
|
|
306
|
+
...historyValues,
|
|
307
|
+
} as any)
|
|
308
|
+
.execute()
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
223
313
|
private async insertHistoryRecord(
|
|
224
314
|
stepId: string,
|
|
225
315
|
status: string,
|
|
316
|
+
attempt: number,
|
|
226
317
|
result?: any,
|
|
227
318
|
error?: SerializedError
|
|
228
319
|
): Promise<void> {
|
|
@@ -231,6 +322,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
231
322
|
historyId: crypto.randomUUID(),
|
|
232
323
|
workflowStepId: stepId,
|
|
233
324
|
status,
|
|
325
|
+
attempt,
|
|
234
326
|
result: result != null ? JSON.stringify(result) : null,
|
|
235
327
|
error: error != null ? JSON.stringify(error) : null,
|
|
236
328
|
createdAt: now,
|
|
@@ -280,38 +372,10 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
280
372
|
stepId: string,
|
|
281
373
|
result: any
|
|
282
374
|
): Promise<void> {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
.set({
|
|
288
|
-
status: 'succeeded',
|
|
289
|
-
result: resultJson,
|
|
290
|
-
error: null,
|
|
291
|
-
updatedAt: new Date(),
|
|
292
|
-
})
|
|
293
|
-
.where('workflowStepId', '=', stepId)
|
|
294
|
-
.execute()
|
|
295
|
-
|
|
296
|
-
const latestHistory = await this.db
|
|
297
|
-
.selectFrom('workflowStepHistory')
|
|
298
|
-
.select('historyId')
|
|
299
|
-
.where('workflowStepId', '=', stepId)
|
|
300
|
-
.orderBy('createdAt', 'desc')
|
|
301
|
-
.limit(1)
|
|
302
|
-
.executeTakeFirst()
|
|
303
|
-
|
|
304
|
-
if (latestHistory) {
|
|
305
|
-
await this.db
|
|
306
|
-
.updateTable('workflowStepHistory')
|
|
307
|
-
.set({
|
|
308
|
-
status: 'succeeded',
|
|
309
|
-
result: resultJson,
|
|
310
|
-
succeededAt: new Date(),
|
|
311
|
-
})
|
|
312
|
-
.where('historyId', '=', latestHistory.historyId)
|
|
313
|
-
.execute()
|
|
314
|
-
}
|
|
375
|
+
await this.writeStepTransition(stepId, 'succeeded', {
|
|
376
|
+
result: JSON.stringify(result),
|
|
377
|
+
succeededAt: new Date(),
|
|
378
|
+
})
|
|
315
379
|
}
|
|
316
380
|
|
|
317
381
|
protected async setStepErrorImpl(
|
|
@@ -323,73 +387,54 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
323
387
|
stack: error.stack,
|
|
324
388
|
code: (error as any).code,
|
|
325
389
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
.set({
|
|
331
|
-
status: 'failed',
|
|
332
|
-
error: errorJson,
|
|
333
|
-
result: null,
|
|
334
|
-
updatedAt: new Date(),
|
|
335
|
-
})
|
|
336
|
-
.where('workflowStepId', '=', stepId)
|
|
337
|
-
.execute()
|
|
338
|
-
|
|
339
|
-
const latestHistory = await this.db
|
|
340
|
-
.selectFrom('workflowStepHistory')
|
|
341
|
-
.select('historyId')
|
|
342
|
-
.where('workflowStepId', '=', stepId)
|
|
343
|
-
.orderBy('createdAt', 'desc')
|
|
344
|
-
.limit(1)
|
|
345
|
-
.executeTakeFirst()
|
|
346
|
-
|
|
347
|
-
if (latestHistory) {
|
|
348
|
-
await this.db
|
|
349
|
-
.updateTable('workflowStepHistory')
|
|
350
|
-
.set({ status: 'failed', error: errorJson, failedAt: new Date() })
|
|
351
|
-
.where('historyId', '=', latestHistory.historyId)
|
|
352
|
-
.execute()
|
|
353
|
-
}
|
|
390
|
+
await this.writeStepTransition(stepId, 'failed', {
|
|
391
|
+
error: JSON.stringify(serializedError),
|
|
392
|
+
failedAt: new Date(),
|
|
393
|
+
})
|
|
354
394
|
}
|
|
355
395
|
|
|
356
396
|
protected async createRetryAttemptImpl(
|
|
357
397
|
stepId: string,
|
|
358
398
|
status: 'pending' | 'running'
|
|
359
399
|
): Promise<StepState> {
|
|
400
|
+
// Read before writing: the new attempt number comes from the history this
|
|
401
|
+
// step already has, and the row it reads is the one returned below.
|
|
402
|
+
const previous = await this.db
|
|
403
|
+
.selectFrom('workflowStep')
|
|
404
|
+
.select('currentAttempt')
|
|
405
|
+
.where('workflowStepId', '=', stepId)
|
|
406
|
+
.executeTakeFirst()
|
|
407
|
+
const attempt = (previous?.currentAttempt ?? 0) + 1
|
|
408
|
+
|
|
360
409
|
await this.db
|
|
361
410
|
.updateTable('workflowStep')
|
|
362
|
-
.set({
|
|
411
|
+
.set({
|
|
412
|
+
status,
|
|
413
|
+
result: null,
|
|
414
|
+
error: null,
|
|
415
|
+
currentAttempt: attempt,
|
|
416
|
+
updatedAt: new Date(),
|
|
417
|
+
})
|
|
363
418
|
.where('workflowStepId', '=', stepId)
|
|
364
419
|
.execute()
|
|
365
420
|
|
|
366
|
-
await this.insertHistoryRecord(stepId, status)
|
|
421
|
+
await this.insertHistoryRecord(stepId, status, attempt)
|
|
367
422
|
|
|
368
423
|
const row = await this.db
|
|
369
|
-
.selectFrom('workflowStep
|
|
424
|
+
.selectFrom('workflowStep')
|
|
370
425
|
.select([
|
|
371
|
-
'
|
|
372
|
-
'
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
'
|
|
376
|
-
'
|
|
377
|
-
'
|
|
378
|
-
'
|
|
379
|
-
'
|
|
426
|
+
'workflowStepId',
|
|
427
|
+
'status',
|
|
428
|
+
'result',
|
|
429
|
+
'error',
|
|
430
|
+
'retries',
|
|
431
|
+
'retryDelay',
|
|
432
|
+
'fromStepName',
|
|
433
|
+
'currentAttempt',
|
|
434
|
+
'createdAt',
|
|
435
|
+
'updatedAt',
|
|
380
436
|
])
|
|
381
|
-
.
|
|
382
|
-
eb
|
|
383
|
-
.selectFrom('workflowStepHistory')
|
|
384
|
-
.select(eb.fn.countAll<number>().as('cnt'))
|
|
385
|
-
.whereRef(
|
|
386
|
-
'workflowStepHistory.workflowStepId',
|
|
387
|
-
'=',
|
|
388
|
-
's.workflowStepId'
|
|
389
|
-
)
|
|
390
|
-
.as('attemptCount')
|
|
391
|
-
)
|
|
392
|
-
.where('s.workflowStepId', '=', stepId)
|
|
437
|
+
.where('workflowStepId', '=', stepId)
|
|
393
438
|
.executeTakeFirstOrThrow()
|
|
394
439
|
|
|
395
440
|
return {
|
|
@@ -397,7 +442,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
397
442
|
status: row.status as StepState['status'],
|
|
398
443
|
result: parseJson(row.result),
|
|
399
444
|
error: parseJson(row.error),
|
|
400
|
-
attemptCount: Number(row.
|
|
445
|
+
attemptCount: Number(row.currentAttempt ?? 1),
|
|
401
446
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
402
447
|
retryDelay: row.retryDelay ?? undefined,
|
|
403
448
|
fromStepName: row.fromStepName ?? undefined,
|
|
@@ -526,23 +571,48 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
526
571
|
.execute()
|
|
527
572
|
}
|
|
528
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Merge one key into the run's JSON state, as a single expression evaluated
|
|
576
|
+
* by the database.
|
|
577
|
+
*
|
|
578
|
+
* The SQL is dialect-specific, so subclasses override this rather than the
|
|
579
|
+
* caller. The default is the SQLite form; MySQL and Postgres differ in how
|
|
580
|
+
* they cast a JSON literal.
|
|
581
|
+
*
|
|
582
|
+
* @param path - JSON path to the key, already quoted (`$."key"`)
|
|
583
|
+
* @param json - The value as JSON text
|
|
584
|
+
*/
|
|
585
|
+
protected jsonSetState(path: string, json: string) {
|
|
586
|
+
return sql<string>`json_set(coalesce(state, '{}'), ${path}, json(${json}))`
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* A JSON path for an arbitrary key. State keys are user-supplied (a graph's
|
|
591
|
+
* `setState` name, an approval's hex-encoded reason), so the key is quoted
|
|
592
|
+
* rather than interpolated bare — an unquoted `$.a.b` would silently address
|
|
593
|
+
* a nested path instead of the key literally called `a.b`.
|
|
594
|
+
*/
|
|
595
|
+
private jsonPathFor(name: string): string {
|
|
596
|
+
return `$.${JSON.stringify(name)}`
|
|
597
|
+
}
|
|
598
|
+
|
|
529
599
|
protected async updateRunStateImpl(
|
|
530
600
|
runId: string,
|
|
531
601
|
name: string,
|
|
532
602
|
value: unknown
|
|
533
603
|
): Promise<void> {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
.where('workflowRunId', '=', runId)
|
|
538
|
-
.executeTakeFirst()
|
|
539
|
-
|
|
540
|
-
const state: Record<string, unknown> = parseJson(row?.state) ?? {}
|
|
541
|
-
state[name] = value
|
|
542
|
-
|
|
604
|
+
// Merged by the database, not read-modify-written in JS: two graph nodes
|
|
605
|
+
// calling setState concurrently used to race, and the later write silently
|
|
606
|
+
// dropped whichever key the earlier one had added.
|
|
543
607
|
await this.db
|
|
544
608
|
.updateTable('workflowRuns')
|
|
545
|
-
.set({
|
|
609
|
+
.set({
|
|
610
|
+
state: this.jsonSetState(
|
|
611
|
+
this.jsonPathFor(name),
|
|
612
|
+
JSON.stringify(value ?? null)
|
|
613
|
+
),
|
|
614
|
+
updatedAt: new Date(),
|
|
615
|
+
})
|
|
546
616
|
.where('workflowRunId', '=', runId)
|
|
547
617
|
.execute()
|
|
548
618
|
}
|
|
@@ -598,24 +668,5 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
598
668
|
return this.runService.getWorkflowVersion(name, graphHash)
|
|
599
669
|
}
|
|
600
670
|
|
|
601
|
-
async getAIGeneratedWorkflows(
|
|
602
|
-
agentName?: string
|
|
603
|
-
): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>> {
|
|
604
|
-
let query = this.db
|
|
605
|
-
.selectFrom('workflowVersions')
|
|
606
|
-
.select(['workflowName', 'graphHash', 'graph'])
|
|
607
|
-
.where('source', '=', 'dynamic-workflow')
|
|
608
|
-
.where('status', '=', 'active')
|
|
609
|
-
if (agentName) {
|
|
610
|
-
query = query.where('workflowName', 'like', `ai:${agentName}:%`)
|
|
611
|
-
}
|
|
612
|
-
const rows = await query.execute()
|
|
613
|
-
return rows.map((row) => ({
|
|
614
|
-
workflowName: row.workflowName,
|
|
615
|
-
graphHash: row.graphHash,
|
|
616
|
-
graph: typeof row.graph === 'string' ? JSON.parse(row.graph) : row.graph,
|
|
617
|
-
}))
|
|
618
|
-
}
|
|
619
|
-
|
|
620
671
|
async close(): Promise<void> {}
|
|
621
672
|
}
|
|
@@ -8,8 +8,10 @@ import type { PikkuSchema } from './pikku-schema.types.js'
|
|
|
8
8
|
* primary keys had `defaultTo(sql.raw("'" + crypto.randomUUID() + "'"))`, which
|
|
9
9
|
* evaluates once while the statement is built — every row would have taken the
|
|
10
10
|
* same default, so it was never a generator; the services supply the id. And
|
|
11
|
-
* `workflowStep.fromStepName
|
|
12
|
-
*
|
|
11
|
+
* `workflowStep.fromStepName`, `workflowStep.currentAttempt` and
|
|
12
|
+
* `workflowStepHistory.attempt` were each backfilled by an
|
|
13
|
+
* `alterTable(...).catch(() => {})` bolted on after the fact, which is the job a
|
|
14
|
+
* migration does — they are declared here and nowhere else.
|
|
13
15
|
*
|
|
14
16
|
* `KyselyWorkflowMirror` created these same four tables separately. There is one
|
|
15
17
|
* declaration now, and both services read it.
|
|
@@ -62,6 +64,7 @@ export const workflowSchema: PikkuSchema = {
|
|
|
62
64
|
.addColumn('retries', 'integer')
|
|
63
65
|
.addColumn('retryDelay', 'text')
|
|
64
66
|
.addColumn('fromStepName', 'text')
|
|
67
|
+
.addColumn('currentAttempt', 'integer')
|
|
65
68
|
.addColumn('createdAt', 'timestamp', (col) =>
|
|
66
69
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
67
70
|
)
|
|
@@ -86,6 +89,7 @@ export const workflowSchema: PikkuSchema = {
|
|
|
86
89
|
.addColumn('status', 'text', (col) => col.notNull())
|
|
87
90
|
.addColumn('result', 'text')
|
|
88
91
|
.addColumn('error', 'text')
|
|
92
|
+
.addColumn('attempt', 'integer')
|
|
89
93
|
.addColumn('createdAt', 'timestamp', (col) =>
|
|
90
94
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
91
95
|
)
|
|
@@ -109,5 +113,37 @@ export const workflowSchema: PikkuSchema = {
|
|
|
109
113
|
'workflowName',
|
|
110
114
|
'graphHash',
|
|
111
115
|
]),
|
|
116
|
+
|
|
117
|
+
// The indexes trail the tables so that a table is never indexed before it
|
|
118
|
+
// exists, whatever order the declarations are read in.
|
|
119
|
+
(db) =>
|
|
120
|
+
db.schema
|
|
121
|
+
.createIndex('idx_workflow_runs_status_created')
|
|
122
|
+
.on('workflowRuns')
|
|
123
|
+
.columns(['status', 'createdAt']),
|
|
124
|
+
|
|
125
|
+
(db) =>
|
|
126
|
+
db.schema
|
|
127
|
+
.createIndex('idx_workflow_runs_workflow_created')
|
|
128
|
+
.on('workflowRuns')
|
|
129
|
+
.columns(['workflow', 'createdAt']),
|
|
130
|
+
|
|
131
|
+
(db) =>
|
|
132
|
+
db.schema
|
|
133
|
+
.createIndex('idx_workflow_step_run_status')
|
|
134
|
+
.on('workflowStep')
|
|
135
|
+
.columns(['workflowRunId', 'status']),
|
|
136
|
+
|
|
137
|
+
(db) =>
|
|
138
|
+
db.schema
|
|
139
|
+
.createIndex('idx_workflow_step_history_step')
|
|
140
|
+
.on('workflowStepHistory')
|
|
141
|
+
.columns(['workflowStepId', 'createdAt']),
|
|
142
|
+
|
|
143
|
+
(db) =>
|
|
144
|
+
db.schema
|
|
145
|
+
.createIndex('idx_workflow_versions_source_status')
|
|
146
|
+
.on('workflowVersions')
|
|
147
|
+
.columns(['source', 'status']),
|
|
112
148
|
],
|
|
113
149
|
}
|