@pikku/kysely 0.11.0 → 0.12.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/src/index.d.ts +10 -0
  3. package/dist/src/index.js +7 -0
  4. package/dist/src/kysely-agent-run-service.d.ts +19 -0
  5. package/dist/src/kysely-agent-run-service.js +171 -0
  6. package/dist/src/kysely-ai-storage-service.d.ts +37 -0
  7. package/dist/src/kysely-ai-storage-service.js +587 -0
  8. package/dist/src/kysely-channel-store.d.ts +18 -0
  9. package/dist/src/kysely-channel-store.js +80 -0
  10. package/dist/src/kysely-deployment-service.d.ts +17 -0
  11. package/dist/src/kysely-deployment-service.js +128 -0
  12. package/dist/src/kysely-eventhub-store.d.ts +13 -0
  13. package/dist/src/kysely-eventhub-store.js +45 -0
  14. package/dist/src/kysely-json.d.ts +1 -0
  15. package/dist/src/kysely-json.js +7 -0
  16. package/dist/src/kysely-tables.d.ts +136 -0
  17. package/dist/src/kysely-tables.js +1 -0
  18. package/dist/src/kysely-workflow-run-service.d.ts +29 -0
  19. package/dist/src/kysely-workflow-run-service.js +194 -0
  20. package/dist/src/kysely-workflow-service.d.ts +47 -0
  21. package/dist/src/kysely-workflow-service.js +485 -0
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +12 -6
  24. package/src/index.ts +12 -0
  25. package/src/kysely-agent-run-service.ts +205 -0
  26. package/src/kysely-ai-storage-service.ts +715 -0
  27. package/src/kysely-channel-store.ts +111 -0
  28. package/src/kysely-deployment-service.ts +172 -0
  29. package/src/kysely-eventhub-store.ts +53 -0
  30. package/src/kysely-json.ts +5 -0
  31. package/src/kysely-services.test.ts +800 -0
  32. package/src/kysely-tables.ts +150 -0
  33. package/src/kysely-workflow-run-service.ts +242 -0
  34. package/src/kysely-workflow-service.ts +643 -0
@@ -0,0 +1,643 @@
1
+ import type { SerializedError } from '@pikku/core'
2
+ import {
3
+ PikkuWorkflowService,
4
+ type WorkflowRun,
5
+ type WorkflowRunWire,
6
+ type StepState,
7
+ type WorkflowStatus,
8
+ } from '@pikku/core/workflow'
9
+ import type { Kysely } from 'kysely'
10
+ import { sql } from 'kysely'
11
+ import type { KyselyPikkuDB } from './kysely-tables.js'
12
+ import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
13
+ import { parseJson } from './kysely-json.js'
14
+
15
+ export class KyselyWorkflowService extends PikkuWorkflowService {
16
+ private initialized = false
17
+ private runService: KyselyWorkflowRunService
18
+
19
+ constructor(private db: Kysely<KyselyPikkuDB>) {
20
+ super()
21
+ this.runService = new KyselyWorkflowRunService(db)
22
+ }
23
+
24
+ public async init(): Promise<void> {
25
+ if (this.initialized) {
26
+ return
27
+ }
28
+
29
+ await this.db.schema
30
+ .createTable('workflow_runs')
31
+ .ifNotExists()
32
+ .addColumn('workflow_run_id', 'text', (col) =>
33
+ col
34
+ .primaryKey()
35
+ .defaultTo(sql`${sql.raw("'" + crypto.randomUUID() + "'")}`)
36
+ )
37
+ .addColumn('workflow', 'text', (col) => col.notNull())
38
+ .addColumn('status', 'text', (col) => col.notNull())
39
+ .addColumn('input', 'text', (col) => col.notNull())
40
+ .addColumn('output', 'text')
41
+ .addColumn('error', 'text')
42
+ .addColumn('state', 'text', (col) => col.defaultTo('{}'))
43
+ .addColumn('inline', 'boolean', (col) => col.defaultTo(false))
44
+ .addColumn('graph_hash', 'text')
45
+ .addColumn('wire', 'text')
46
+ .addColumn('created_at', 'timestamp', (col) =>
47
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
48
+ )
49
+ .addColumn('updated_at', 'timestamp', (col) =>
50
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
51
+ )
52
+ .execute()
53
+
54
+ await this.db.schema
55
+ .createTable('workflow_step')
56
+ .ifNotExists()
57
+ .addColumn('workflow_step_id', 'text', (col) =>
58
+ col
59
+ .primaryKey()
60
+ .defaultTo(sql`${sql.raw("'" + crypto.randomUUID() + "'")}`)
61
+ )
62
+ .addColumn('workflow_run_id', 'text', (col) =>
63
+ col
64
+ .notNull()
65
+ .references('workflow_runs.workflow_run_id')
66
+ .onDelete('cascade')
67
+ )
68
+ .addColumn('step_name', 'text', (col) => col.notNull())
69
+ .addColumn('rpc_name', 'text')
70
+ .addColumn('data', 'text')
71
+ .addColumn('status', 'text', (col) => col.notNull().defaultTo('pending'))
72
+ .addColumn('result', 'text')
73
+ .addColumn('error', 'text')
74
+ .addColumn('branch_taken', 'text')
75
+ .addColumn('retries', 'integer')
76
+ .addColumn('retry_delay', 'text')
77
+ .addColumn('created_at', 'timestamp', (col) =>
78
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
79
+ )
80
+ .addColumn('updated_at', 'timestamp', (col) =>
81
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
82
+ )
83
+ .addUniqueConstraint('workflow_step_run_name_unique', [
84
+ 'workflow_run_id',
85
+ 'step_name',
86
+ ])
87
+ .execute()
88
+
89
+ await this.db.schema
90
+ .createTable('workflow_step_history')
91
+ .ifNotExists()
92
+ .addColumn('history_id', 'text', (col) =>
93
+ col
94
+ .primaryKey()
95
+ .defaultTo(sql`${sql.raw("'" + crypto.randomUUID() + "'")}`)
96
+ )
97
+ .addColumn('workflow_step_id', 'text', (col) =>
98
+ col
99
+ .notNull()
100
+ .references('workflow_step.workflow_step_id')
101
+ .onDelete('cascade')
102
+ )
103
+ .addColumn('status', 'text', (col) => col.notNull())
104
+ .addColumn('result', 'text')
105
+ .addColumn('error', 'text')
106
+ .addColumn('created_at', 'timestamp', (col) =>
107
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
108
+ )
109
+ .addColumn('running_at', 'timestamp')
110
+ .addColumn('scheduled_at', 'timestamp')
111
+ .addColumn('succeeded_at', 'timestamp')
112
+ .addColumn('failed_at', 'timestamp')
113
+ .execute()
114
+
115
+ await this.db.schema
116
+ .createTable('workflow_versions')
117
+ .ifNotExists()
118
+ .addColumn('workflow_name', 'text', (col) => col.notNull())
119
+ .addColumn('graph_hash', 'text', (col) => col.notNull())
120
+ .addColumn('graph', 'text', (col) => col.notNull())
121
+ .addColumn('source', 'text', (col) => col.notNull())
122
+ .addColumn('created_at', 'timestamp', (col) =>
123
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
124
+ )
125
+ .addPrimaryKeyConstraint('workflow_versions_pk', [
126
+ 'workflow_name',
127
+ 'graph_hash',
128
+ ])
129
+ .execute()
130
+
131
+ this.initialized = true
132
+ }
133
+
134
+ async createRun(
135
+ workflowName: string,
136
+ input: any,
137
+ inline: boolean,
138
+ graphHash: string,
139
+ wire: WorkflowRunWire
140
+ ): Promise<string> {
141
+ const id = crypto.randomUUID()
142
+ await this.db
143
+ .insertInto('workflow_runs')
144
+ .values({
145
+ workflow_run_id: id,
146
+ workflow: workflowName,
147
+ status: 'running',
148
+ input: JSON.stringify(input),
149
+ inline,
150
+ graph_hash: graphHash,
151
+ wire: JSON.stringify(wire),
152
+ })
153
+ .execute()
154
+
155
+ return id
156
+ }
157
+
158
+ async getRun(id: string): Promise<WorkflowRun | null> {
159
+ return this.runService.getRun(id)
160
+ }
161
+
162
+ async updateRunStatus(
163
+ id: string,
164
+ status: WorkflowStatus,
165
+ output?: any,
166
+ error?: SerializedError
167
+ ): Promise<void> {
168
+ await this.db
169
+ .updateTable('workflow_runs')
170
+ .set({
171
+ status,
172
+ output: output ? JSON.stringify(output) : null,
173
+ error: error ? JSON.stringify(error) : null,
174
+ updated_at: new Date(),
175
+ })
176
+ .where('workflow_run_id', '=', id)
177
+ .execute()
178
+ }
179
+
180
+ async insertStepState(
181
+ runId: string,
182
+ stepName: string,
183
+ rpcName: string | null,
184
+ data: any,
185
+ stepOptions?: { retries?: number; retryDelay?: string | number }
186
+ ): Promise<StepState> {
187
+ const stepId = crypto.randomUUID()
188
+ const now = new Date()
189
+
190
+ await this.db
191
+ .insertInto('workflow_step')
192
+ .values({
193
+ workflow_step_id: stepId,
194
+ workflow_run_id: runId,
195
+ step_name: stepName,
196
+ rpc_name: rpcName,
197
+ data: data != null ? JSON.stringify(data) : null,
198
+ status: 'pending',
199
+ retries: stepOptions?.retries ?? null,
200
+ retry_delay: stepOptions?.retryDelay?.toString() ?? null,
201
+ created_at: now,
202
+ updated_at: now,
203
+ })
204
+ .execute()
205
+
206
+ await this.insertHistoryRecord(stepId, 'pending')
207
+
208
+ return {
209
+ stepId,
210
+ status: 'pending',
211
+ result: undefined,
212
+ error: undefined,
213
+ attemptCount: 1,
214
+ retries: stepOptions?.retries,
215
+ retryDelay: stepOptions?.retryDelay?.toString(),
216
+ createdAt: now,
217
+ updatedAt: now,
218
+ }
219
+ }
220
+
221
+ async getStepState(runId: string, stepName: string): Promise<StepState> {
222
+ const row = await this.db
223
+ .selectFrom('workflow_step as s')
224
+ .select([
225
+ 's.workflow_step_id',
226
+ 's.status',
227
+ 's.result',
228
+ 's.error',
229
+ 's.retries',
230
+ 's.retry_delay',
231
+ 's.created_at',
232
+ 's.updated_at',
233
+ ])
234
+ .select((eb) =>
235
+ eb
236
+ .selectFrom('workflow_step_history')
237
+ .select(eb.fn.countAll<number>().as('cnt'))
238
+ .whereRef(
239
+ 'workflow_step_history.workflow_step_id',
240
+ '=',
241
+ 's.workflow_step_id'
242
+ )
243
+ .as('attempt_count')
244
+ )
245
+ .where('s.workflow_run_id', '=', runId)
246
+ .where('s.step_name', '=', stepName)
247
+ .executeTakeFirst()
248
+
249
+ if (!row) {
250
+ throw new Error(
251
+ `Step not found: runId=${runId}, stepName=${stepName}. Use insertStepState to create it.`
252
+ )
253
+ }
254
+
255
+ return {
256
+ stepId: row.workflow_step_id,
257
+ status: row.status as StepState['status'],
258
+ result: parseJson(row.result),
259
+ error: parseJson(row.error),
260
+ attemptCount: Number(row.attempt_count),
261
+ retries: row.retries != null ? Number(row.retries) : undefined,
262
+ retryDelay: row.retry_delay ?? undefined,
263
+ createdAt: new Date(row.created_at),
264
+ updatedAt: new Date(row.updated_at),
265
+ }
266
+ }
267
+
268
+ async getRunHistory(
269
+ runId: string
270
+ ): Promise<Array<StepState & { stepName: string }>> {
271
+ return this.runService.getRunHistory(runId)
272
+ }
273
+
274
+ async setStepRunning(stepId: string): Promise<void> {
275
+ await this.db
276
+ .updateTable('workflow_step')
277
+ .set({ status: 'running', updated_at: new Date() })
278
+ .where('workflow_step_id', '=', stepId)
279
+ .execute()
280
+
281
+ const latestHistory = await this.db
282
+ .selectFrom('workflow_step_history')
283
+ .select('history_id')
284
+ .where('workflow_step_id', '=', stepId)
285
+ .orderBy('created_at', 'desc')
286
+ .limit(1)
287
+ .executeTakeFirst()
288
+
289
+ if (latestHistory) {
290
+ await this.db
291
+ .updateTable('workflow_step_history')
292
+ .set({ status: 'running' })
293
+ .where('history_id', '=', latestHistory.history_id)
294
+ .execute()
295
+ }
296
+ }
297
+
298
+ async setStepScheduled(stepId: string): Promise<void> {
299
+ await this.db
300
+ .updateTable('workflow_step')
301
+ .set({ status: 'scheduled', updated_at: new Date() })
302
+ .where('workflow_step_id', '=', stepId)
303
+ .execute()
304
+ }
305
+
306
+ private async insertHistoryRecord(
307
+ stepId: string,
308
+ status: string,
309
+ result?: any,
310
+ error?: SerializedError
311
+ ): Promise<void> {
312
+ const now = new Date()
313
+ const values: Record<string, any> = {
314
+ history_id: crypto.randomUUID(),
315
+ workflow_step_id: stepId,
316
+ status,
317
+ result: result != null ? JSON.stringify(result) : null,
318
+ error: error != null ? JSON.stringify(error) : null,
319
+ created_at: now,
320
+ }
321
+
322
+ const timestampField = this.getTimestampFieldForStatus(status)
323
+ if (timestampField !== 'created_at') {
324
+ values[timestampField] = now
325
+ }
326
+
327
+ await this.db
328
+ .insertInto('workflow_step_history')
329
+ .values(values as any)
330
+ .execute()
331
+ }
332
+
333
+ private getTimestampFieldForStatus(status: string): string {
334
+ switch (status) {
335
+ case 'running':
336
+ return 'running_at'
337
+ case 'scheduled':
338
+ return 'scheduled_at'
339
+ case 'succeeded':
340
+ return 'succeeded_at'
341
+ case 'failed':
342
+ return 'failed_at'
343
+ default:
344
+ return 'created_at'
345
+ }
346
+ }
347
+
348
+ async setStepResult(stepId: string, result: any): Promise<void> {
349
+ const resultJson = JSON.stringify(result)
350
+
351
+ await this.db
352
+ .updateTable('workflow_step')
353
+ .set({
354
+ status: 'succeeded',
355
+ result: resultJson,
356
+ error: null,
357
+ updated_at: new Date(),
358
+ })
359
+ .where('workflow_step_id', '=', stepId)
360
+ .execute()
361
+
362
+ const latestHistory = await this.db
363
+ .selectFrom('workflow_step_history')
364
+ .select('history_id')
365
+ .where('workflow_step_id', '=', stepId)
366
+ .orderBy('created_at', 'desc')
367
+ .limit(1)
368
+ .executeTakeFirst()
369
+
370
+ if (latestHistory) {
371
+ await this.db
372
+ .updateTable('workflow_step_history')
373
+ .set({ status: 'succeeded', result: resultJson })
374
+ .where('history_id', '=', latestHistory.history_id)
375
+ .execute()
376
+ }
377
+ }
378
+
379
+ async setStepError(stepId: string, error: Error): Promise<void> {
380
+ const serializedError: SerializedError = {
381
+ message: error.message,
382
+ stack: error.stack,
383
+ code: (error as any).code,
384
+ }
385
+ const errorJson = JSON.stringify(serializedError)
386
+
387
+ await this.db
388
+ .updateTable('workflow_step')
389
+ .set({
390
+ status: 'failed',
391
+ error: errorJson,
392
+ result: null,
393
+ updated_at: new Date(),
394
+ })
395
+ .where('workflow_step_id', '=', stepId)
396
+ .execute()
397
+
398
+ const latestHistory = await this.db
399
+ .selectFrom('workflow_step_history')
400
+ .select('history_id')
401
+ .where('workflow_step_id', '=', stepId)
402
+ .orderBy('created_at', 'desc')
403
+ .limit(1)
404
+ .executeTakeFirst()
405
+
406
+ if (latestHistory) {
407
+ await this.db
408
+ .updateTable('workflow_step_history')
409
+ .set({ status: 'failed', error: errorJson })
410
+ .where('history_id', '=', latestHistory.history_id)
411
+ .execute()
412
+ }
413
+ }
414
+
415
+ async createRetryAttempt(
416
+ stepId: string,
417
+ status: 'pending' | 'running'
418
+ ): Promise<StepState> {
419
+ await this.db
420
+ .updateTable('workflow_step')
421
+ .set({ status, result: null, error: null, updated_at: new Date() })
422
+ .where('workflow_step_id', '=', stepId)
423
+ .execute()
424
+
425
+ await this.insertHistoryRecord(stepId, status)
426
+
427
+ const row = await this.db
428
+ .selectFrom('workflow_step as s')
429
+ .select([
430
+ 's.workflow_step_id',
431
+ 's.status',
432
+ 's.result',
433
+ 's.error',
434
+ 's.retries',
435
+ 's.retry_delay',
436
+ 's.created_at',
437
+ 's.updated_at',
438
+ ])
439
+ .select((eb) =>
440
+ eb
441
+ .selectFrom('workflow_step_history')
442
+ .select(eb.fn.countAll<number>().as('cnt'))
443
+ .whereRef(
444
+ 'workflow_step_history.workflow_step_id',
445
+ '=',
446
+ 's.workflow_step_id'
447
+ )
448
+ .as('attempt_count')
449
+ )
450
+ .where('s.workflow_step_id', '=', stepId)
451
+ .executeTakeFirstOrThrow()
452
+
453
+ return {
454
+ stepId: row.workflow_step_id,
455
+ status: row.status as StepState['status'],
456
+ result: parseJson(row.result),
457
+ error: parseJson(row.error),
458
+ attemptCount: Number(row.attempt_count),
459
+ retries: row.retries != null ? Number(row.retries) : undefined,
460
+ retryDelay: row.retry_delay ?? undefined,
461
+ createdAt: new Date(row.created_at),
462
+ updatedAt: new Date(row.updated_at),
463
+ }
464
+ }
465
+
466
+ async withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
467
+ return this.db.transaction().execute(async (trx) => {
468
+ await trx
469
+ .selectFrom('workflow_runs')
470
+ .select('workflow_run_id')
471
+ .where('workflow_run_id', '=', id)
472
+ .forUpdate()
473
+ .executeTakeFirst()
474
+ return fn()
475
+ })
476
+ }
477
+
478
+ async withStepLock<T>(
479
+ runId: string,
480
+ stepName: string,
481
+ fn: () => Promise<T>
482
+ ): Promise<T> {
483
+ return this.db.transaction().execute(async (trx) => {
484
+ await trx
485
+ .selectFrom('workflow_step')
486
+ .select('workflow_step_id')
487
+ .where('workflow_run_id', '=', runId)
488
+ .where('step_name', '=', stepName)
489
+ .forUpdate()
490
+ .executeTakeFirst()
491
+ return fn()
492
+ })
493
+ }
494
+
495
+ async getCompletedGraphState(runId: string): Promise<{
496
+ completedNodeIds: string[]
497
+ failedNodeIds: string[]
498
+ branchKeys: Record<string, string>
499
+ }> {
500
+ const results = await this.db
501
+ .selectFrom('workflow_step as ws')
502
+ .select(['ws.step_name', 'ws.status', 'ws.branch_taken', 'ws.retries'])
503
+ .select((eb) =>
504
+ eb
505
+ .selectFrom('workflow_step_history as h')
506
+ .select(eb.fn.countAll<number>().as('cnt'))
507
+ .whereRef('h.workflow_step_id', '=', 'ws.workflow_step_id')
508
+ .as('attempt_count')
509
+ )
510
+ .where('ws.workflow_run_id', '=', runId)
511
+ .where('ws.status', 'in', ['succeeded', 'failed'])
512
+ .execute()
513
+
514
+ const completedNodeIds: string[] = []
515
+ const failedNodeIds: string[] = []
516
+ const branchKeys: Record<string, string> = {}
517
+
518
+ for (const row of results) {
519
+ const nodeId = row.step_name
520
+
521
+ if (row.status === 'succeeded') {
522
+ completedNodeIds.push(nodeId)
523
+ if (row.branch_taken) {
524
+ branchKeys[nodeId] = row.branch_taken
525
+ }
526
+ } else if (row.status === 'failed') {
527
+ const maxAttempts = (row.retries ?? 0) + 1
528
+ if (Number(row.attempt_count) >= maxAttempts) {
529
+ failedNodeIds.push(nodeId)
530
+ }
531
+ }
532
+ }
533
+
534
+ return { completedNodeIds, failedNodeIds, branchKeys }
535
+ }
536
+
537
+ async getNodesWithoutSteps(
538
+ runId: string,
539
+ nodeIds: string[]
540
+ ): Promise<string[]> {
541
+ if (nodeIds.length === 0) return []
542
+
543
+ const result = await this.db
544
+ .selectFrom('workflow_step')
545
+ .select('step_name')
546
+ .where('workflow_run_id', '=', runId)
547
+ .where('step_name', 'in', nodeIds)
548
+ .execute()
549
+
550
+ const existingStepNames = new Set(result.map((r) => r.step_name))
551
+ return nodeIds.filter((id) => !existingStepNames.has(id))
552
+ }
553
+
554
+ async getNodeResults(
555
+ runId: string,
556
+ nodeIds: string[]
557
+ ): Promise<Record<string, any>> {
558
+ if (nodeIds.length === 0) return {}
559
+
560
+ const result = await this.db
561
+ .selectFrom('workflow_step')
562
+ .select(['step_name', 'result'])
563
+ .where('workflow_run_id', '=', runId)
564
+ .where('step_name', 'in', nodeIds)
565
+ .where('status', '=', 'succeeded')
566
+ .execute()
567
+
568
+ const results: Record<string, any> = {}
569
+ for (const row of result) {
570
+ results[row.step_name] = parseJson(row.result)
571
+ }
572
+ return results
573
+ }
574
+
575
+ async setBranchTaken(stepId: string, branchKey: string): Promise<void> {
576
+ await this.db
577
+ .updateTable('workflow_step')
578
+ .set({ branch_taken: branchKey, updated_at: new Date() })
579
+ .where('workflow_step_id', '=', stepId)
580
+ .execute()
581
+ }
582
+
583
+ async updateRunState(
584
+ runId: string,
585
+ name: string,
586
+ value: unknown
587
+ ): Promise<void> {
588
+ const row = await this.db
589
+ .selectFrom('workflow_runs')
590
+ .select('state')
591
+ .where('workflow_run_id', '=', runId)
592
+ .executeTakeFirst()
593
+
594
+ const state: Record<string, unknown> = parseJson(row?.state) ?? {}
595
+ state[name] = value
596
+
597
+ await this.db
598
+ .updateTable('workflow_runs')
599
+ .set({ state: JSON.stringify(state), updated_at: new Date() })
600
+ .where('workflow_run_id', '=', runId)
601
+ .execute()
602
+ }
603
+
604
+ async getRunState(runId: string): Promise<Record<string, unknown>> {
605
+ const row = await this.db
606
+ .selectFrom('workflow_runs')
607
+ .select('state')
608
+ .where('workflow_run_id', '=', runId)
609
+ .executeTakeFirst()
610
+
611
+ if (!row) return {}
612
+ return parseJson(row.state) ?? {}
613
+ }
614
+
615
+ async upsertWorkflowVersion(
616
+ name: string,
617
+ graphHash: string,
618
+ graph: any,
619
+ source: string
620
+ ): Promise<void> {
621
+ await this.db
622
+ .insertInto('workflow_versions')
623
+ .values({
624
+ workflow_name: name,
625
+ graph_hash: graphHash,
626
+ graph: JSON.stringify(graph),
627
+ source,
628
+ })
629
+ .onConflict((oc) =>
630
+ oc.columns(['workflow_name', 'graph_hash']).doNothing()
631
+ )
632
+ .execute()
633
+ }
634
+
635
+ async getWorkflowVersion(
636
+ name: string,
637
+ graphHash: string
638
+ ): Promise<{ graph: any; source: string } | null> {
639
+ return this.runService.getWorkflowVersion(name, graphHash)
640
+ }
641
+
642
+ async close(): Promise<void> {}
643
+ }