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