@pikku/mongodb 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 (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/src/index.d.ts +12 -0
  3. package/dist/src/index.js +9 -0
  4. package/dist/src/mongodb-agent-run-service.d.ts +21 -0
  5. package/dist/src/mongodb-agent-run-service.js +127 -0
  6. package/dist/src/mongodb-ai-storage-service.d.ts +41 -0
  7. package/dist/src/mongodb-ai-storage-service.js +408 -0
  8. package/dist/src/mongodb-channel-store.d.ts +19 -0
  9. package/dist/src/mongodb-channel-store.js +52 -0
  10. package/dist/src/mongodb-deployment-service.d.ts +17 -0
  11. package/dist/src/mongodb-deployment-service.js +75 -0
  12. package/dist/src/mongodb-eventhub-store.d.ts +13 -0
  13. package/dist/src/mongodb-eventhub-store.js +37 -0
  14. package/dist/src/mongodb-secret-service.d.ts +30 -0
  15. package/dist/src/mongodb-secret-service.js +110 -0
  16. package/dist/src/mongodb-workflow-run-service.d.ts +36 -0
  17. package/dist/src/mongodb-workflow-run-service.js +146 -0
  18. package/dist/src/mongodb-workflow-service.d.ts +55 -0
  19. package/dist/src/mongodb-workflow-service.js +351 -0
  20. package/dist/src/pikku-mongodb.d.ts +12 -0
  21. package/dist/src/pikku-mongodb.js +37 -0
  22. package/dist/tsconfig.tsbuildinfo +1 -0
  23. package/package.json +29 -0
  24. package/run-tests.sh +53 -0
  25. package/src/index.ts +14 -0
  26. package/src/mongodb-agent-run-service.ts +206 -0
  27. package/src/mongodb-ai-storage-service.ts +576 -0
  28. package/src/mongodb-channel-store.ts +96 -0
  29. package/src/mongodb-deployment-service.ts +114 -0
  30. package/src/mongodb-eventhub-store.ts +52 -0
  31. package/src/mongodb-secret-service.ts +178 -0
  32. package/src/mongodb-services.test.ts +149 -0
  33. package/src/mongodb-workflow-run-service.ts +246 -0
  34. package/src/mongodb-workflow-service.ts +583 -0
  35. package/src/pikku-mongodb.ts +41 -0
  36. package/tsconfig.json +17 -0
@@ -0,0 +1,583 @@
1
+ import type { SerializedError } from '@pikku/core'
2
+ import {
3
+ PikkuWorkflowService,
4
+ type WorkflowRun,
5
+ type WorkflowRunWire,
6
+ type StepState,
7
+ type WorkflowStatus,
8
+ type WorkflowVersionStatus,
9
+ } from '@pikku/core/workflow'
10
+ import type { Db, Collection } from 'mongodb'
11
+ import { MongoDBWorkflowRunService } from './mongodb-workflow-run-service.js'
12
+
13
+ interface WorkflowRunDoc {
14
+ _id: string
15
+ workflow: string
16
+ status: string
17
+ input: any
18
+ output: any | null
19
+ error: any | null
20
+ state: Record<string, unknown>
21
+ inline: boolean
22
+ graphHash: string | null
23
+ wire: any | null
24
+ createdAt: Date
25
+ updatedAt: Date
26
+ }
27
+
28
+ interface WorkflowStepDoc {
29
+ _id: string
30
+ workflowRunId: string
31
+ stepName: string
32
+ rpcName: string | null
33
+ data: any | null
34
+ status: string
35
+ result: any | null
36
+ error: any | null
37
+ branchTaken: string | null
38
+ retries: number | null
39
+ retryDelay: string | null
40
+ createdAt: Date
41
+ updatedAt: Date
42
+ }
43
+
44
+ interface WorkflowStepHistoryDoc {
45
+ _id: string
46
+ workflowStepId: string
47
+ status: string
48
+ result: any | null
49
+ error: any | null
50
+ createdAt: Date
51
+ runningAt: Date | null
52
+ scheduledAt: Date | null
53
+ succeededAt: Date | null
54
+ failedAt: Date | null
55
+ }
56
+
57
+ interface WorkflowVersionDoc {
58
+ workflowName: string
59
+ graphHash: string
60
+ graph: any
61
+ source: string
62
+ status: string
63
+ createdAt: Date
64
+ }
65
+
66
+ export class MongoDBWorkflowService extends PikkuWorkflowService {
67
+ private initialized = false
68
+ private runService: MongoDBWorkflowRunService
69
+ private runs: Collection<WorkflowRunDoc>
70
+ private steps: Collection<WorkflowStepDoc>
71
+ private stepHistory: Collection<WorkflowStepHistoryDoc>
72
+ private versions: Collection<WorkflowVersionDoc>
73
+
74
+ constructor(db: Db) {
75
+ super()
76
+ this.runService = new MongoDBWorkflowRunService(db)
77
+ this.runs = db.collection<WorkflowRunDoc>('workflow_runs')
78
+ this.steps = db.collection<WorkflowStepDoc>('workflow_step')
79
+ this.stepHistory = db.collection<WorkflowStepHistoryDoc>(
80
+ 'workflow_step_history'
81
+ )
82
+ this.versions = db.collection<WorkflowVersionDoc>('workflow_versions')
83
+ }
84
+
85
+ public async init(): Promise<void> {
86
+ if (this.initialized) return
87
+
88
+ await this.runs.createIndex({ workflow: 1 })
89
+ await this.runs.createIndex({ status: 1 })
90
+ await this.runs.createIndex({ createdAt: -1 })
91
+
92
+ await this.steps.createIndex(
93
+ { workflowRunId: 1, stepName: 1 },
94
+ { unique: true }
95
+ )
96
+ await this.steps.createIndex({ workflowRunId: 1 })
97
+
98
+ await this.stepHistory.createIndex({ workflowStepId: 1 })
99
+ await this.stepHistory.createIndex({ createdAt: 1 })
100
+
101
+ await this.versions.createIndex(
102
+ { workflowName: 1, graphHash: 1 },
103
+ { unique: true }
104
+ )
105
+
106
+ this.initialized = true
107
+ }
108
+
109
+ async createRun(
110
+ workflowName: string,
111
+ input: any,
112
+ inline: boolean,
113
+ graphHash: string,
114
+ wire: WorkflowRunWire
115
+ ): Promise<string> {
116
+ const id = crypto.randomUUID()
117
+ const now = new Date()
118
+
119
+ await this.runs.insertOne({
120
+ _id: id,
121
+ workflow: workflowName,
122
+ status: 'running',
123
+ input,
124
+ output: null,
125
+ error: null,
126
+ state: {},
127
+ inline,
128
+ graphHash,
129
+ wire,
130
+ createdAt: now,
131
+ updatedAt: now,
132
+ })
133
+
134
+ return id
135
+ }
136
+
137
+ async getRun(id: string): Promise<WorkflowRun | null> {
138
+ return this.runService.getRun(id)
139
+ }
140
+
141
+ async updateRunStatus(
142
+ id: string,
143
+ status: WorkflowStatus,
144
+ output?: any,
145
+ error?: SerializedError
146
+ ): Promise<void> {
147
+ await this.runs.updateOne(
148
+ { _id: id },
149
+ {
150
+ $set: {
151
+ status,
152
+ output: output ?? null,
153
+ error: error ?? null,
154
+ updatedAt: new Date(),
155
+ },
156
+ }
157
+ )
158
+ }
159
+
160
+ async insertStepState(
161
+ runId: string,
162
+ stepName: string,
163
+ rpcName: string | null,
164
+ data: any,
165
+ stepOptions?: { retries?: number; retryDelay?: string | number }
166
+ ): Promise<StepState> {
167
+ const stepId = crypto.randomUUID()
168
+ const now = new Date()
169
+
170
+ await this.steps.insertOne({
171
+ _id: stepId,
172
+ workflowRunId: runId,
173
+ stepName,
174
+ rpcName,
175
+ data: data ?? null,
176
+ status: 'pending',
177
+ result: null,
178
+ error: null,
179
+ branchTaken: null,
180
+ retries: stepOptions?.retries ?? null,
181
+ retryDelay: stepOptions?.retryDelay?.toString() ?? null,
182
+ createdAt: now,
183
+ updatedAt: now,
184
+ })
185
+
186
+ await this.insertHistoryRecord(stepId, 'pending')
187
+
188
+ return {
189
+ stepId,
190
+ status: 'pending',
191
+ result: undefined,
192
+ error: undefined,
193
+ attemptCount: 1,
194
+ retries: stepOptions?.retries,
195
+ retryDelay: stepOptions?.retryDelay?.toString(),
196
+ createdAt: now,
197
+ updatedAt: now,
198
+ }
199
+ }
200
+
201
+ async getStepState(runId: string, stepName: string): Promise<StepState> {
202
+ const row = await this.steps.findOne({
203
+ workflowRunId: runId,
204
+ stepName,
205
+ })
206
+
207
+ if (!row) {
208
+ throw new Error(
209
+ `Step not found: runId=${runId}, stepName=${stepName}. Use insertStepState to create it.`
210
+ )
211
+ }
212
+
213
+ const attemptCount = await this.stepHistory.countDocuments({
214
+ workflowStepId: row._id,
215
+ })
216
+
217
+ return {
218
+ stepId: row._id,
219
+ status: row.status as StepState['status'],
220
+ result: row.result ?? undefined,
221
+ error: row.error ?? undefined,
222
+ attemptCount,
223
+ retries: row.retries != null ? Number(row.retries) : undefined,
224
+ retryDelay: row.retryDelay ?? undefined,
225
+ createdAt: new Date(row.createdAt),
226
+ updatedAt: new Date(row.updatedAt),
227
+ }
228
+ }
229
+
230
+ async getRunHistory(
231
+ runId: string
232
+ ): Promise<Array<StepState & { stepName: string }>> {
233
+ return this.runService.getRunHistory(runId)
234
+ }
235
+
236
+ async setStepRunning(stepId: string): Promise<void> {
237
+ await this.steps.updateOne(
238
+ { _id: stepId },
239
+ { $set: { status: 'running', updatedAt: new Date() } }
240
+ )
241
+
242
+ const latestHistory = await this.stepHistory
243
+ .find({ workflowStepId: stepId })
244
+ .sort({ createdAt: -1 })
245
+ .limit(1)
246
+ .toArray()
247
+
248
+ if (latestHistory.length > 0) {
249
+ await this.stepHistory.updateOne(
250
+ { _id: latestHistory[0]!._id },
251
+ { $set: { status: 'running' } }
252
+ )
253
+ }
254
+ }
255
+
256
+ async setStepScheduled(stepId: string): Promise<void> {
257
+ await this.steps.updateOne(
258
+ { _id: stepId },
259
+ { $set: { status: 'scheduled', updatedAt: new Date() } }
260
+ )
261
+ }
262
+
263
+ private async insertHistoryRecord(
264
+ stepId: string,
265
+ status: string,
266
+ result?: any,
267
+ error?: SerializedError
268
+ ): Promise<void> {
269
+ const now = new Date()
270
+ const doc: any = {
271
+ _id: crypto.randomUUID(),
272
+ workflowStepId: stepId,
273
+ status,
274
+ result: result ?? null,
275
+ error: error ?? null,
276
+ createdAt: now,
277
+ runningAt: null,
278
+ scheduledAt: null,
279
+ succeededAt: null,
280
+ failedAt: null,
281
+ }
282
+
283
+ const timestampField = this.getTimestampFieldForStatus(status)
284
+ if (timestampField !== 'createdAt') {
285
+ doc[timestampField] = now
286
+ }
287
+
288
+ await this.stepHistory.insertOne(doc)
289
+ }
290
+
291
+ private getTimestampFieldForStatus(status: string): string {
292
+ switch (status) {
293
+ case 'running':
294
+ return 'runningAt'
295
+ case 'scheduled':
296
+ return 'scheduledAt'
297
+ case 'succeeded':
298
+ return 'succeededAt'
299
+ case 'failed':
300
+ return 'failedAt'
301
+ default:
302
+ return 'createdAt'
303
+ }
304
+ }
305
+
306
+ async setStepResult(stepId: string, result: any): Promise<void> {
307
+ await this.steps.updateOne(
308
+ { _id: stepId },
309
+ {
310
+ $set: {
311
+ status: 'succeeded',
312
+ result,
313
+ error: null,
314
+ updatedAt: new Date(),
315
+ },
316
+ }
317
+ )
318
+
319
+ const latestHistory = await this.stepHistory
320
+ .find({ workflowStepId: stepId })
321
+ .sort({ createdAt: -1 })
322
+ .limit(1)
323
+ .toArray()
324
+
325
+ if (latestHistory.length > 0) {
326
+ await this.stepHistory.updateOne(
327
+ { _id: latestHistory[0]!._id },
328
+ { $set: { status: 'succeeded', result } }
329
+ )
330
+ }
331
+ }
332
+
333
+ async setStepError(stepId: string, error: Error): Promise<void> {
334
+ const serializedError: SerializedError = {
335
+ message: error.message,
336
+ stack: error.stack,
337
+ code: (error as any).code,
338
+ }
339
+
340
+ await this.steps.updateOne(
341
+ { _id: stepId },
342
+ {
343
+ $set: {
344
+ status: 'failed',
345
+ error: serializedError,
346
+ result: null,
347
+ updatedAt: new Date(),
348
+ },
349
+ }
350
+ )
351
+
352
+ const latestHistory = await this.stepHistory
353
+ .find({ workflowStepId: stepId })
354
+ .sort({ createdAt: -1 })
355
+ .limit(1)
356
+ .toArray()
357
+
358
+ if (latestHistory.length > 0) {
359
+ await this.stepHistory.updateOne(
360
+ { _id: latestHistory[0]!._id },
361
+ { $set: { status: 'failed', error: serializedError } }
362
+ )
363
+ }
364
+ }
365
+
366
+ async createRetryAttempt(
367
+ stepId: string,
368
+ status: 'pending' | 'running'
369
+ ): Promise<StepState> {
370
+ await this.steps.updateOne(
371
+ { _id: stepId },
372
+ {
373
+ $set: {
374
+ status,
375
+ result: null,
376
+ error: null,
377
+ updatedAt: new Date(),
378
+ },
379
+ }
380
+ )
381
+
382
+ await this.insertHistoryRecord(stepId, status)
383
+
384
+ const row = await this.steps.findOne({ _id: stepId })
385
+ if (!row) throw new Error(`Step not found: ${stepId}`)
386
+
387
+ const attemptCount = await this.stepHistory.countDocuments({
388
+ workflowStepId: stepId,
389
+ })
390
+
391
+ return {
392
+ stepId: row._id,
393
+ status: row.status as StepState['status'],
394
+ result: row.result ?? undefined,
395
+ error: row.error ?? undefined,
396
+ attemptCount,
397
+ retries: row.retries != null ? Number(row.retries) : undefined,
398
+ retryDelay: row.retryDelay ?? undefined,
399
+ createdAt: new Date(row.createdAt),
400
+ updatedAt: new Date(row.updatedAt),
401
+ }
402
+ }
403
+
404
+ async withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
405
+ return fn()
406
+ }
407
+
408
+ async withStepLock<T>(
409
+ _runId: string,
410
+ _stepName: string,
411
+ fn: () => Promise<T>
412
+ ): Promise<T> {
413
+ return fn()
414
+ }
415
+
416
+ async getCompletedGraphState(runId: string): Promise<{
417
+ completedNodeIds: string[]
418
+ failedNodeIds: string[]
419
+ branchKeys: Record<string, string>
420
+ }> {
421
+ const results = await this.steps
422
+ .find({
423
+ workflowRunId: runId,
424
+ status: { $in: ['succeeded', 'failed'] },
425
+ })
426
+ .toArray()
427
+
428
+ const completedNodeIds: string[] = []
429
+ const failedNodeIds: string[] = []
430
+ const branchKeys: Record<string, string> = {}
431
+
432
+ for (const row of results) {
433
+ const nodeId = row.stepName
434
+
435
+ if (row.status === 'succeeded') {
436
+ completedNodeIds.push(nodeId)
437
+ if (row.branchTaken) {
438
+ branchKeys[nodeId] = row.branchTaken
439
+ }
440
+ } else if (row.status === 'failed') {
441
+ const maxAttempts = (row.retries ?? 0) + 1
442
+ const attemptCount = await this.stepHistory.countDocuments({
443
+ workflowStepId: row._id,
444
+ })
445
+ if (attemptCount >= maxAttempts) {
446
+ failedNodeIds.push(nodeId)
447
+ }
448
+ }
449
+ }
450
+
451
+ return { completedNodeIds, failedNodeIds, branchKeys }
452
+ }
453
+
454
+ async getNodesWithoutSteps(
455
+ runId: string,
456
+ nodeIds: string[]
457
+ ): Promise<string[]> {
458
+ if (nodeIds.length === 0) return []
459
+
460
+ const result = await this.steps
461
+ .find({
462
+ workflowRunId: runId,
463
+ stepName: { $in: nodeIds },
464
+ })
465
+ .project({ stepName: 1 })
466
+ .toArray()
467
+
468
+ const existingStepNames = new Set(result.map((r) => r.stepName))
469
+ return nodeIds.filter((id) => !existingStepNames.has(id))
470
+ }
471
+
472
+ async getNodeResults(
473
+ runId: string,
474
+ nodeIds: string[]
475
+ ): Promise<Record<string, any>> {
476
+ if (nodeIds.length === 0) return {}
477
+
478
+ const result = await this.steps
479
+ .find({
480
+ workflowRunId: runId,
481
+ stepName: { $in: nodeIds },
482
+ status: 'succeeded',
483
+ })
484
+ .toArray()
485
+
486
+ const results: Record<string, any> = {}
487
+ for (const row of result) {
488
+ results[row.stepName] = row.result
489
+ }
490
+ return results
491
+ }
492
+
493
+ async setBranchTaken(stepId: string, branchKey: string): Promise<void> {
494
+ await this.steps.updateOne(
495
+ { _id: stepId },
496
+ { $set: { branchTaken: branchKey, updatedAt: new Date() } }
497
+ )
498
+ }
499
+
500
+ async updateRunState(
501
+ runId: string,
502
+ name: string,
503
+ value: unknown
504
+ ): Promise<void> {
505
+ await this.runs.updateOne(
506
+ { _id: runId },
507
+ {
508
+ $set: {
509
+ [`state.${name}`]: value,
510
+ updatedAt: new Date(),
511
+ },
512
+ }
513
+ )
514
+ }
515
+
516
+ async getRunState(runId: string): Promise<Record<string, unknown>> {
517
+ const row = await this.runs.findOne(
518
+ { _id: runId },
519
+ { projection: { state: 1 } }
520
+ )
521
+ if (!row) return {}
522
+ return row.state ?? {}
523
+ }
524
+
525
+ async upsertWorkflowVersion(
526
+ name: string,
527
+ graphHash: string,
528
+ graph: any,
529
+ source: string,
530
+ status?: WorkflowVersionStatus
531
+ ): Promise<void> {
532
+ await this.versions.updateOne(
533
+ { workflowName: name, graphHash },
534
+ {
535
+ $setOnInsert: {
536
+ workflowName: name,
537
+ graphHash,
538
+ graph,
539
+ source,
540
+ status: status ?? 'active',
541
+ createdAt: new Date(),
542
+ },
543
+ },
544
+ { upsert: true }
545
+ )
546
+ }
547
+
548
+ async updateWorkflowVersionStatus(
549
+ name: string,
550
+ graphHash: string,
551
+ status: WorkflowVersionStatus
552
+ ): Promise<void> {
553
+ await this.versions.updateOne(
554
+ { workflowName: name, graphHash },
555
+ { $set: { status } }
556
+ )
557
+ }
558
+
559
+ async getWorkflowVersion(
560
+ name: string,
561
+ graphHash: string
562
+ ): Promise<{ graph: any; source: string } | null> {
563
+ return this.runService.getWorkflowVersion(name, graphHash)
564
+ }
565
+
566
+ async getAIGeneratedWorkflows(
567
+ agentName?: string
568
+ ): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>> {
569
+ const filter: Record<string, any> = { source: 'ai-agent', status: 'active' }
570
+ if (agentName) {
571
+ const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
572
+ filter.workflowName = { $regex: `^ai:${escaped}:` }
573
+ }
574
+ const docs = await this.versions.find(filter).toArray()
575
+ return docs.map((doc) => ({
576
+ workflowName: doc.workflowName,
577
+ graphHash: doc.graphHash,
578
+ graph: doc.graph,
579
+ }))
580
+ }
581
+
582
+ async close(): Promise<void> {}
583
+ }
@@ -0,0 +1,41 @@
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
+ process.exit(1)
33
+ }
34
+ }
35
+
36
+ public async close() {
37
+ if (this.ownsConnection) {
38
+ await this.client.close()
39
+ }
40
+ }
41
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "module": "Node18",
6
+ "outDir": "dist",
7
+ "target": "esnext",
8
+ "declaration": true
9
+ },
10
+ "include": ["src/**/*.ts"],
11
+ "exclude": ["**/*.test.ts", "node_modules"],
12
+ "references": [
13
+ {
14
+ "path": "../../core/tsconfig.json"
15
+ }
16
+ ]
17
+ }