@pikku/mongodb 0.12.21 → 0.12.22
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 +28 -0
- package/package.json +10 -6
- package/run-tests.sh +21 -6
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/index.ts +0 -14
- package/src/mongodb-agent-run-service.ts +0 -234
- package/src/mongodb-agent-storage-service.ts +0 -632
- package/src/mongodb-channel-store.ts +0 -109
- package/src/mongodb-deployment-service.ts +0 -136
- package/src/mongodb-eventhub-store.ts +0 -52
- package/src/mongodb-secret-service.ts +0 -232
- package/src/mongodb-services.test.ts +0 -235
- package/src/mongodb-session-store.ts +0 -50
- package/src/mongodb-workflow-run-service.ts +0 -235
- package/src/mongodb-workflow-service.test.ts +0 -238
- package/src/mongodb-workflow-service.ts +0 -687
- package/src/pikku-mongodb.ts +0 -41
- package/tsconfig.json +0 -17
|
@@ -1,687 +0,0 @@
|
|
|
1
|
-
import type { SerializedError } from '@pikku/core/errors'
|
|
2
|
-
import {
|
|
3
|
-
PikkuWorkflowService,
|
|
4
|
-
WorkflowStepFunctionMismatchError,
|
|
5
|
-
} from '@pikku/core/workflow'
|
|
6
|
-
import type {
|
|
7
|
-
WorkflowPlannedStep,
|
|
8
|
-
WorkflowQueueOptions,
|
|
9
|
-
WorkflowRun,
|
|
10
|
-
WorkflowRunWire,
|
|
11
|
-
StepState,
|
|
12
|
-
StepStatus,
|
|
13
|
-
WorkflowStatus,
|
|
14
|
-
WorkflowVersionStatus,
|
|
15
|
-
} from '@pikku/core/workflow'
|
|
16
|
-
import type { Db, Collection } from 'mongodb'
|
|
17
|
-
import { MongoDBWorkflowRunService } from './mongodb-workflow-run-service.js'
|
|
18
|
-
|
|
19
|
-
interface WorkflowRunDoc {
|
|
20
|
-
_id: string
|
|
21
|
-
workflow: string
|
|
22
|
-
status: string
|
|
23
|
-
input: any
|
|
24
|
-
output: any | null
|
|
25
|
-
error: any | null
|
|
26
|
-
state: Record<string, unknown>
|
|
27
|
-
inline: boolean
|
|
28
|
-
graphHash: string | null
|
|
29
|
-
deterministic?: boolean
|
|
30
|
-
plannedSteps?: WorkflowPlannedStep[]
|
|
31
|
-
wire: any | null
|
|
32
|
-
createdAt: Date
|
|
33
|
-
updatedAt: Date
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface WorkflowStepDoc {
|
|
37
|
-
_id: string
|
|
38
|
-
workflowRunId: string
|
|
39
|
-
stepName: string
|
|
40
|
-
rpcName: string | null
|
|
41
|
-
data: any | null
|
|
42
|
-
status: string
|
|
43
|
-
result: any | null
|
|
44
|
-
error: any | null
|
|
45
|
-
childRunId?: string
|
|
46
|
-
branchTaken: string | null
|
|
47
|
-
retries: number | null
|
|
48
|
-
retryDelay: string | null
|
|
49
|
-
fromStepName?: string | null
|
|
50
|
-
createdAt: Date
|
|
51
|
-
updatedAt: Date
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface WorkflowStepHistoryDoc {
|
|
55
|
-
_id: string
|
|
56
|
-
workflowStepId: string
|
|
57
|
-
status: string
|
|
58
|
-
result: any | null
|
|
59
|
-
error: any | null
|
|
60
|
-
createdAt: Date
|
|
61
|
-
runningAt: Date | null
|
|
62
|
-
scheduledAt: Date | null
|
|
63
|
-
succeededAt: Date | null
|
|
64
|
-
failedAt: Date | null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface WorkflowVersionDoc {
|
|
68
|
-
workflowName: string
|
|
69
|
-
graphHash: string
|
|
70
|
-
graph: any
|
|
71
|
-
source: string
|
|
72
|
-
status: string
|
|
73
|
-
createdAt: Date
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
77
|
-
private initialized = false
|
|
78
|
-
private runService: MongoDBWorkflowRunService
|
|
79
|
-
private runs: Collection<WorkflowRunDoc>
|
|
80
|
-
private steps: Collection<WorkflowStepDoc>
|
|
81
|
-
private stepHistory: Collection<WorkflowStepHistoryDoc>
|
|
82
|
-
private versions: Collection<WorkflowVersionDoc>
|
|
83
|
-
|
|
84
|
-
constructor(db: Db, options: WorkflowQueueOptions = {}) {
|
|
85
|
-
super(options)
|
|
86
|
-
this.runService = new MongoDBWorkflowRunService(db)
|
|
87
|
-
this.runs = db.collection<WorkflowRunDoc>('workflow_runs')
|
|
88
|
-
this.steps = db.collection<WorkflowStepDoc>('workflow_step')
|
|
89
|
-
this.stepHistory = db.collection<WorkflowStepHistoryDoc>(
|
|
90
|
-
'workflow_step_history'
|
|
91
|
-
)
|
|
92
|
-
this.versions = db.collection<WorkflowVersionDoc>('workflow_versions')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
public async init(): Promise<void> {
|
|
96
|
-
if (this.initialized) return
|
|
97
|
-
|
|
98
|
-
await this.runs.createIndex({ workflow: 1 })
|
|
99
|
-
await this.runs.createIndex({ status: 1 })
|
|
100
|
-
await this.runs.createIndex({ createdAt: -1 })
|
|
101
|
-
|
|
102
|
-
await this.steps.createIndex(
|
|
103
|
-
{ workflowRunId: 1, stepName: 1 },
|
|
104
|
-
{ unique: true }
|
|
105
|
-
)
|
|
106
|
-
await this.steps.createIndex({ workflowRunId: 1 })
|
|
107
|
-
|
|
108
|
-
await this.stepHistory.createIndex({ workflowStepId: 1 })
|
|
109
|
-
await this.stepHistory.createIndex({ createdAt: 1 })
|
|
110
|
-
|
|
111
|
-
await this.versions.createIndex(
|
|
112
|
-
{ workflowName: 1, graphHash: 1 },
|
|
113
|
-
{ unique: true }
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
this.initialized = true
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
protected async createRunImpl(
|
|
120
|
-
workflowName: string,
|
|
121
|
-
input: any,
|
|
122
|
-
inline: boolean,
|
|
123
|
-
graphHash: string,
|
|
124
|
-
wire: WorkflowRunWire,
|
|
125
|
-
options?: {
|
|
126
|
-
deterministic?: boolean
|
|
127
|
-
plannedSteps?: WorkflowPlannedStep[]
|
|
128
|
-
}
|
|
129
|
-
): Promise<string> {
|
|
130
|
-
const id = crypto.randomUUID()
|
|
131
|
-
const now = new Date()
|
|
132
|
-
|
|
133
|
-
await this.runs.insertOne({
|
|
134
|
-
_id: id,
|
|
135
|
-
workflow: workflowName,
|
|
136
|
-
status: 'running',
|
|
137
|
-
input,
|
|
138
|
-
output: null,
|
|
139
|
-
error: null,
|
|
140
|
-
state: {},
|
|
141
|
-
inline,
|
|
142
|
-
graphHash,
|
|
143
|
-
deterministic: options?.deterministic ?? false,
|
|
144
|
-
plannedSteps: options?.plannedSteps ?? [],
|
|
145
|
-
wire,
|
|
146
|
-
createdAt: now,
|
|
147
|
-
updatedAt: now,
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
return id
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async getRun(id: string): Promise<WorkflowRun | null> {
|
|
154
|
-
return this.runService.getRun(id)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
protected async updateRunStatusImpl(
|
|
158
|
-
id: string,
|
|
159
|
-
status: WorkflowStatus,
|
|
160
|
-
output?: any,
|
|
161
|
-
error?: SerializedError
|
|
162
|
-
): Promise<void> {
|
|
163
|
-
await this.runs.updateOne(
|
|
164
|
-
{ _id: id },
|
|
165
|
-
{
|
|
166
|
-
$set: {
|
|
167
|
-
status,
|
|
168
|
-
output: output ?? null,
|
|
169
|
-
error: error ?? null,
|
|
170
|
-
updatedAt: new Date(),
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
protected async insertStepStateImpl(
|
|
177
|
-
runId: string,
|
|
178
|
-
stepName: string,
|
|
179
|
-
rpcName: string | null,
|
|
180
|
-
data: any,
|
|
181
|
-
stepOptions?: { retries?: number; retryDelay?: string | number },
|
|
182
|
-
fromStepName?: string
|
|
183
|
-
): Promise<StepState> {
|
|
184
|
-
const stepId = crypto.randomUUID()
|
|
185
|
-
const now = new Date()
|
|
186
|
-
|
|
187
|
-
await this.steps.insertOne({
|
|
188
|
-
_id: stepId,
|
|
189
|
-
workflowRunId: runId,
|
|
190
|
-
stepName,
|
|
191
|
-
rpcName,
|
|
192
|
-
data: data ?? null,
|
|
193
|
-
status: 'pending',
|
|
194
|
-
result: null,
|
|
195
|
-
error: null,
|
|
196
|
-
branchTaken: null,
|
|
197
|
-
retries: stepOptions?.retries ?? null,
|
|
198
|
-
retryDelay: stepOptions?.retryDelay?.toString() ?? null,
|
|
199
|
-
fromStepName: fromStepName ?? null,
|
|
200
|
-
createdAt: now,
|
|
201
|
-
updatedAt: now,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
await this.insertHistoryRecord(stepId, 'pending')
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
stepId,
|
|
208
|
-
status: 'pending',
|
|
209
|
-
rpcName,
|
|
210
|
-
result: undefined,
|
|
211
|
-
error: undefined,
|
|
212
|
-
attemptCount: 1,
|
|
213
|
-
retries: stepOptions?.retries,
|
|
214
|
-
retryDelay: stepOptions?.retryDelay?.toString(),
|
|
215
|
-
fromStepName,
|
|
216
|
-
createdAt: now,
|
|
217
|
-
updatedAt: now,
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async getStepState(runId: string, stepName: string): Promise<StepState> {
|
|
222
|
-
const row = await this.steps.findOne({
|
|
223
|
-
workflowRunId: runId,
|
|
224
|
-
stepName,
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
if (!row) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
`Step not found: runId=${runId}, stepName=${stepName}. Use insertStepState to create it.`
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const attemptCount = await this.stepHistory.countDocuments({
|
|
234
|
-
workflowStepId: row._id,
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
stepId: row._id,
|
|
239
|
-
status: row.status as StepState['status'],
|
|
240
|
-
rpcName: row.rpcName ?? null,
|
|
241
|
-
result: row.result ?? undefined,
|
|
242
|
-
error: row.error ?? undefined,
|
|
243
|
-
attemptCount,
|
|
244
|
-
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
245
|
-
retryDelay: row.retryDelay ?? undefined,
|
|
246
|
-
fromStepName: row.fromStepName ?? undefined,
|
|
247
|
-
createdAt: new Date(row.createdAt),
|
|
248
|
-
updatedAt: new Date(row.updatedAt),
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async getRunHistory(
|
|
253
|
-
runId: string
|
|
254
|
-
): Promise<Array<StepState & { stepName: string }>> {
|
|
255
|
-
return this.runService.getRunHistory(runId)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
protected async setStepRunningImpl(stepId: string): Promise<void> {
|
|
259
|
-
await this.steps.updateOne(
|
|
260
|
-
{ _id: stepId },
|
|
261
|
-
{ $set: { status: 'running', updatedAt: new Date() } }
|
|
262
|
-
)
|
|
263
|
-
await this.writeLatestHistory(stepId, 'running')
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
protected async setStepScheduledImpl(stepId: string): Promise<void> {
|
|
267
|
-
await this.steps.updateOne(
|
|
268
|
-
{ _id: stepId },
|
|
269
|
-
{ $set: { status: 'scheduled', updatedAt: new Date() } }
|
|
270
|
-
)
|
|
271
|
-
await this.writeLatestHistory(stepId, 'scheduled')
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Move the current attempt of a step's history to a status.
|
|
276
|
-
*
|
|
277
|
-
* Finding the newest attempt and then updating it by `_id` was two round
|
|
278
|
-
* trips with a window in between; `findOneAndUpdate` applies the same sort
|
|
279
|
-
* server-side and writes the row it selected, so nothing can move underneath
|
|
280
|
-
* it. The attempt is still identified by `createdAt` because Mongo keeps no
|
|
281
|
-
* attempt number — see `getStepState`, which counts history documents.
|
|
282
|
-
*/
|
|
283
|
-
private async writeLatestHistory(
|
|
284
|
-
stepId: string,
|
|
285
|
-
status: string,
|
|
286
|
-
values: Record<string, unknown> = {}
|
|
287
|
-
): Promise<void> {
|
|
288
|
-
const now = new Date()
|
|
289
|
-
const update: Record<string, unknown> = { status, ...values }
|
|
290
|
-
const timestampField = this.getTimestampFieldForStatus(status)
|
|
291
|
-
if (timestampField !== 'createdAt') {
|
|
292
|
-
update[timestampField] = now
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
await this.stepHistory.findOneAndUpdate(
|
|
296
|
-
{ workflowStepId: stepId },
|
|
297
|
-
{ $set: update as any },
|
|
298
|
-
{ sort: { createdAt: -1 } }
|
|
299
|
-
)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
private async insertHistoryRecord(
|
|
303
|
-
stepId: string,
|
|
304
|
-
status: string,
|
|
305
|
-
result?: any,
|
|
306
|
-
error?: SerializedError
|
|
307
|
-
): Promise<void> {
|
|
308
|
-
const now = new Date()
|
|
309
|
-
const doc: any = {
|
|
310
|
-
_id: crypto.randomUUID(),
|
|
311
|
-
workflowStepId: stepId,
|
|
312
|
-
status,
|
|
313
|
-
result: result ?? null,
|
|
314
|
-
error: error ?? null,
|
|
315
|
-
createdAt: now,
|
|
316
|
-
runningAt: null,
|
|
317
|
-
scheduledAt: null,
|
|
318
|
-
succeededAt: null,
|
|
319
|
-
failedAt: null,
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const timestampField = this.getTimestampFieldForStatus(status)
|
|
323
|
-
if (timestampField !== 'createdAt') {
|
|
324
|
-
doc[timestampField] = now
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
await this.stepHistory.insertOne(doc)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private getTimestampFieldForStatus(status: string): string {
|
|
331
|
-
switch (status) {
|
|
332
|
-
case 'running':
|
|
333
|
-
return 'runningAt'
|
|
334
|
-
case 'scheduled':
|
|
335
|
-
return 'scheduledAt'
|
|
336
|
-
case 'succeeded':
|
|
337
|
-
return 'succeededAt'
|
|
338
|
-
case 'failed':
|
|
339
|
-
return 'failedAt'
|
|
340
|
-
default:
|
|
341
|
-
return 'createdAt'
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
protected async setStepChildRunIdImpl(
|
|
346
|
-
stepId: string,
|
|
347
|
-
childRunId: string
|
|
348
|
-
): Promise<void> {
|
|
349
|
-
await this.steps.updateOne(
|
|
350
|
-
{ _id: stepId },
|
|
351
|
-
{
|
|
352
|
-
$set: {
|
|
353
|
-
childRunId,
|
|
354
|
-
updatedAt: new Date(),
|
|
355
|
-
},
|
|
356
|
-
}
|
|
357
|
-
)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
protected async setStepResultImpl(
|
|
361
|
-
stepId: string,
|
|
362
|
-
result: any
|
|
363
|
-
): Promise<void> {
|
|
364
|
-
await this.steps.updateOne(
|
|
365
|
-
{ _id: stepId },
|
|
366
|
-
{
|
|
367
|
-
$set: {
|
|
368
|
-
status: 'succeeded',
|
|
369
|
-
result,
|
|
370
|
-
error: null,
|
|
371
|
-
updatedAt: new Date(),
|
|
372
|
-
},
|
|
373
|
-
}
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
await this.writeLatestHistory(stepId, 'succeeded', { result })
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
protected async setStepErrorImpl(
|
|
380
|
-
stepId: string,
|
|
381
|
-
error: Error
|
|
382
|
-
): Promise<void> {
|
|
383
|
-
const serializedError: SerializedError = {
|
|
384
|
-
message: error.message,
|
|
385
|
-
stack: error.stack,
|
|
386
|
-
code: (error as any).code,
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
await this.steps.updateOne(
|
|
390
|
-
{ _id: stepId },
|
|
391
|
-
{
|
|
392
|
-
$set: {
|
|
393
|
-
status: 'failed',
|
|
394
|
-
error: serializedError,
|
|
395
|
-
result: null,
|
|
396
|
-
updatedAt: new Date(),
|
|
397
|
-
},
|
|
398
|
-
}
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
await this.writeLatestHistory(stepId, 'failed', {
|
|
402
|
-
error: serializedError,
|
|
403
|
-
})
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
protected async createRetryAttemptImpl(
|
|
407
|
-
stepId: string,
|
|
408
|
-
status: 'pending' | 'running'
|
|
409
|
-
): Promise<StepState> {
|
|
410
|
-
await this.steps.updateOne(
|
|
411
|
-
{ _id: stepId },
|
|
412
|
-
{
|
|
413
|
-
$set: {
|
|
414
|
-
status,
|
|
415
|
-
result: null,
|
|
416
|
-
error: null,
|
|
417
|
-
updatedAt: new Date(),
|
|
418
|
-
},
|
|
419
|
-
}
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
await this.insertHistoryRecord(stepId, status)
|
|
423
|
-
|
|
424
|
-
const row = await this.steps.findOne({ _id: stepId })
|
|
425
|
-
if (!row) throw new Error(`Step not found: ${stepId}`)
|
|
426
|
-
|
|
427
|
-
const attemptCount = await this.stepHistory.countDocuments({
|
|
428
|
-
workflowStepId: stepId,
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
return {
|
|
432
|
-
stepId: row._id,
|
|
433
|
-
status: row.status as StepState['status'],
|
|
434
|
-
rpcName: row.rpcName ?? null,
|
|
435
|
-
result: row.result ?? undefined,
|
|
436
|
-
error: row.error ?? undefined,
|
|
437
|
-
attemptCount,
|
|
438
|
-
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
439
|
-
retryDelay: row.retryDelay ?? undefined,
|
|
440
|
-
fromStepName: row.fromStepName ?? undefined,
|
|
441
|
-
createdAt: new Date(row.createdAt),
|
|
442
|
-
updatedAt: new Date(row.updatedAt),
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
|
|
447
|
-
return fn()
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* A pass-through: MongoDB has no advisory-lock primitive to build one on.
|
|
452
|
-
* The one decision that must exclude — claiming a step to execute it — is
|
|
453
|
-
* made by `claimStepForExecution` below as a single conditional write, which
|
|
454
|
-
* needs no lock to be exclusive.
|
|
455
|
-
*/
|
|
456
|
-
async withStepLock<T>(
|
|
457
|
-
_runId: string,
|
|
458
|
-
_stepName: string,
|
|
459
|
-
fn: () => Promise<T>
|
|
460
|
-
): Promise<T> {
|
|
461
|
-
return fn()
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Claim the step with a status-guarded update and read the modified count:
|
|
466
|
-
* a single-document update is atomic in MongoDB, so two dispatches racing for
|
|
467
|
-
* the same step cannot both proceed.
|
|
468
|
-
*
|
|
469
|
-
* This replaces the read-then-write the base engine does under
|
|
470
|
-
* `withStepLock`, which excludes nothing here — the lock above is a
|
|
471
|
-
* pass-through. The winner then goes through the ordinary transition methods,
|
|
472
|
-
* so history rows see exactly what they saw before.
|
|
473
|
-
*/
|
|
474
|
-
protected override async claimStepForExecution(
|
|
475
|
-
runId: string,
|
|
476
|
-
stepName: string,
|
|
477
|
-
rpcName: string
|
|
478
|
-
): Promise<StepState | null> {
|
|
479
|
-
const stepState = await this.getStepState(runId, stepName)
|
|
480
|
-
|
|
481
|
-
// knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
|
|
482
|
-
if (
|
|
483
|
-
stepState.rpcName !== undefined &&
|
|
484
|
-
stepState.rpcName !== (rpcName ?? null)
|
|
485
|
-
) {
|
|
486
|
-
throw new WorkflowStepFunctionMismatchError(runId, stepName)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (stepState.status === 'succeeded' || stepState.status === 'running') {
|
|
490
|
-
return null
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (stepState.status === 'failed') {
|
|
494
|
-
if (!(await this.claimStepStatus(stepState.stepId, ['failed']))) {
|
|
495
|
-
return null
|
|
496
|
-
}
|
|
497
|
-
return this.createRetryAttempt(stepState.stepId, 'running')
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (stepState.status === 'pending' || stepState.status === 'scheduled') {
|
|
501
|
-
if (
|
|
502
|
-
!(await this.claimStepStatus(stepState.stepId, [
|
|
503
|
-
'pending',
|
|
504
|
-
'scheduled',
|
|
505
|
-
]))
|
|
506
|
-
) {
|
|
507
|
-
return null
|
|
508
|
-
}
|
|
509
|
-
await this.setStepRunning(stepState.stepId)
|
|
510
|
-
return { ...stepState, status: 'running' }
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return stepState
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Move a step to `running` only if it is still in one of `from`, reporting
|
|
518
|
-
* whether this caller is the one that moved it.
|
|
519
|
-
*/
|
|
520
|
-
private async claimStepStatus(
|
|
521
|
-
stepId: string,
|
|
522
|
-
from: StepStatus[]
|
|
523
|
-
): Promise<boolean> {
|
|
524
|
-
const claimed = await this.steps.updateOne(
|
|
525
|
-
{ _id: stepId, status: { $in: from } },
|
|
526
|
-
{ $set: { status: 'running', updatedAt: new Date() } }
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
return claimed.modifiedCount > 0
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async getCompletedGraphState(runId: string): Promise<{
|
|
533
|
-
completedNodeIds: string[]
|
|
534
|
-
failedNodeIds: string[]
|
|
535
|
-
branchKeys: Record<string, string>
|
|
536
|
-
}> {
|
|
537
|
-
const results = await this.steps
|
|
538
|
-
.find({
|
|
539
|
-
workflowRunId: runId,
|
|
540
|
-
status: { $in: ['succeeded', 'failed'] },
|
|
541
|
-
})
|
|
542
|
-
.toArray()
|
|
543
|
-
|
|
544
|
-
const completedNodeIds: string[] = []
|
|
545
|
-
const failedNodeIds: string[] = []
|
|
546
|
-
const branchKeys: Record<string, string> = {}
|
|
547
|
-
|
|
548
|
-
for (const row of results) {
|
|
549
|
-
const nodeId = row.stepName
|
|
550
|
-
|
|
551
|
-
if (row.status === 'succeeded') {
|
|
552
|
-
completedNodeIds.push(nodeId)
|
|
553
|
-
if (row.branchTaken) {
|
|
554
|
-
branchKeys[nodeId] = row.branchTaken
|
|
555
|
-
}
|
|
556
|
-
} else if (row.status === 'failed') {
|
|
557
|
-
const maxAttempts = (row.retries ?? 0) + 1
|
|
558
|
-
const attemptCount = await this.stepHistory.countDocuments({
|
|
559
|
-
workflowStepId: row._id,
|
|
560
|
-
})
|
|
561
|
-
if (attemptCount >= maxAttempts) {
|
|
562
|
-
failedNodeIds.push(nodeId)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return { completedNodeIds, failedNodeIds, branchKeys }
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
async getStepInstances(
|
|
571
|
-
runId: string
|
|
572
|
-
): Promise<
|
|
573
|
-
Array<{ stepName: string; status: StepStatus; fromStepName?: string }>
|
|
574
|
-
> {
|
|
575
|
-
const rows = await this.steps
|
|
576
|
-
.find({ workflowRunId: runId })
|
|
577
|
-
.project({ stepName: 1, status: 1, fromStepName: 1 })
|
|
578
|
-
.toArray()
|
|
579
|
-
return rows.map((r: any) => ({
|
|
580
|
-
stepName: r.stepName,
|
|
581
|
-
status: r.status as StepStatus,
|
|
582
|
-
fromStepName: r.fromStepName ?? undefined,
|
|
583
|
-
}))
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async getNodeResults(
|
|
587
|
-
runId: string,
|
|
588
|
-
nodeIds: string[]
|
|
589
|
-
): Promise<Record<string, any>> {
|
|
590
|
-
if (nodeIds.length === 0) return {}
|
|
591
|
-
|
|
592
|
-
const result = await this.steps
|
|
593
|
-
.find({
|
|
594
|
-
workflowRunId: runId,
|
|
595
|
-
stepName: { $in: nodeIds },
|
|
596
|
-
status: 'succeeded',
|
|
597
|
-
})
|
|
598
|
-
.toArray()
|
|
599
|
-
|
|
600
|
-
const results: Record<string, any> = {}
|
|
601
|
-
for (const row of result) {
|
|
602
|
-
results[row.stepName] = row.result
|
|
603
|
-
}
|
|
604
|
-
return results
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
protected async setBranchTakenImpl(
|
|
608
|
-
stepId: string,
|
|
609
|
-
branchKey: string
|
|
610
|
-
): Promise<void> {
|
|
611
|
-
await this.steps.updateOne(
|
|
612
|
-
{ _id: stepId },
|
|
613
|
-
{ $set: { branchTaken: branchKey, updatedAt: new Date() } }
|
|
614
|
-
)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
protected async updateRunStateImpl(
|
|
618
|
-
runId: string,
|
|
619
|
-
name: string,
|
|
620
|
-
value: unknown
|
|
621
|
-
): Promise<void> {
|
|
622
|
-
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
623
|
-
throw new Error('Invalid state key name')
|
|
624
|
-
}
|
|
625
|
-
await this.runs.updateOne(
|
|
626
|
-
{ _id: runId },
|
|
627
|
-
{
|
|
628
|
-
$set: {
|
|
629
|
-
[`state.${name}`]: value,
|
|
630
|
-
updatedAt: new Date(),
|
|
631
|
-
},
|
|
632
|
-
}
|
|
633
|
-
)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
async getRunState(runId: string): Promise<Record<string, unknown>> {
|
|
637
|
-
const row = await this.runs.findOne(
|
|
638
|
-
{ _id: runId },
|
|
639
|
-
{ projection: { state: 1 } }
|
|
640
|
-
)
|
|
641
|
-
if (!row) return {}
|
|
642
|
-
return row.state ?? {}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
protected async upsertWorkflowVersionImpl(
|
|
646
|
-
name: string,
|
|
647
|
-
graphHash: string,
|
|
648
|
-
graph: any,
|
|
649
|
-
source: string,
|
|
650
|
-
status?: WorkflowVersionStatus
|
|
651
|
-
): Promise<void> {
|
|
652
|
-
await this.versions.updateOne(
|
|
653
|
-
{ workflowName: name, graphHash },
|
|
654
|
-
{
|
|
655
|
-
$setOnInsert: {
|
|
656
|
-
workflowName: name,
|
|
657
|
-
graphHash,
|
|
658
|
-
graph,
|
|
659
|
-
source,
|
|
660
|
-
status: status ?? 'active',
|
|
661
|
-
createdAt: new Date(),
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
|
-
{ upsert: true }
|
|
665
|
-
)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
protected async updateWorkflowVersionStatusImpl(
|
|
669
|
-
name: string,
|
|
670
|
-
graphHash: string,
|
|
671
|
-
status: WorkflowVersionStatus
|
|
672
|
-
): Promise<void> {
|
|
673
|
-
await this.versions.updateOne(
|
|
674
|
-
{ workflowName: name, graphHash },
|
|
675
|
-
{ $set: { status } }
|
|
676
|
-
)
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async getWorkflowVersion(
|
|
680
|
-
name: string,
|
|
681
|
-
graphHash: string
|
|
682
|
-
): Promise<{ graph: any; source: string } | null> {
|
|
683
|
-
return this.runService.getWorkflowVersion(name, graphHash)
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async close(): Promise<void> {}
|
|
687
|
-
}
|
package/src/pikku-mongodb.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { Logger } from '@pikku/core/services'
|
|
2
|
-
import { MongoClient, type MongoClientOptions, type Db } from 'mongodb'
|
|
3
|
-
|
|
4
|
-
export class PikkuMongoDB {
|
|
5
|
-
public db: Db
|
|
6
|
-
private client: MongoClient
|
|
7
|
-
private ownsConnection: boolean
|
|
8
|
-
|
|
9
|
-
constructor(
|
|
10
|
-
private logger: Logger,
|
|
11
|
-
clientOrUri: MongoClient | string,
|
|
12
|
-
private dbName: string,
|
|
13
|
-
options?: MongoClientOptions
|
|
14
|
-
) {
|
|
15
|
-
if (typeof clientOrUri === 'string') {
|
|
16
|
-
this.client = new MongoClient(clientOrUri, options)
|
|
17
|
-
this.ownsConnection = true
|
|
18
|
-
} else {
|
|
19
|
-
this.client = clientOrUri
|
|
20
|
-
this.ownsConnection = false
|
|
21
|
-
}
|
|
22
|
-
this.db = this.client.db(dbName)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public async init() {
|
|
26
|
-
this.logger.info(`Connecting to MongoDB database: ${this.dbName}`)
|
|
27
|
-
try {
|
|
28
|
-
await this.client.db('admin').command({ ping: 1 })
|
|
29
|
-
this.logger.info('MongoDB connection successful')
|
|
30
|
-
} catch (error) {
|
|
31
|
-
this.logger.error('Error connecting to MongoDB', error)
|
|
32
|
-
throw error
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public async close() {
|
|
37
|
-
if (this.ownsConnection) {
|
|
38
|
-
await this.client.close()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|