@mastra/upstash 0.3.1-alpha.3 → 0.3.1-alpha.4

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.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/upstash@0.3.1-alpha.3 build /home/runner/work/mastra/mastra/stores/upstash
2
+ > @mastra/upstash@0.3.1-alpha.4 build /home/runner/work/mastra/mastra/stores/upstash
3
3
  > tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.4.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 10479ms
9
+ TSC ⚡️ Build success in 9760ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
- Analysis will use the bundled TypeScript version 5.8.2
12
+ Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/upstash/dist/_tsup-dts-rollup.d.ts
14
- Analysis will use the bundled TypeScript version 5.8.2
14
+ Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/stores/upstash/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 12075ms
16
+ DTS ⚡️ Build success in 12363ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- ESM dist/index.js 24.02 KB
21
- ESM ⚡️ Build success in 1157ms
22
- CJS dist/index.cjs 24.11 KB
23
- CJS ⚡️ Build success in 1162ms
20
+ ESM dist/index.js 25.43 KB
21
+ ESM ⚡️ Build success in 1277ms
22
+ CJS dist/index.cjs 25.55 KB
23
+ CJS ⚡️ Build success in 1278ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @mastra/upstash
2
2
 
3
+ ## 0.3.1-alpha.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 479f490: [MASTRA-3131] Add getWorkflowRunByID and add resourceId as filter for getWorkflowRuns
8
+ - Updated dependencies [e4943b8]
9
+ - Updated dependencies [479f490]
10
+ - @mastra/core@0.9.1-alpha.4
11
+
3
12
  ## 0.3.1-alpha.3
4
13
 
5
14
  ### Patch Changes
@@ -14,6 +14,8 @@ import type { StorageThreadType } from '@mastra/core/memory';
14
14
  import type { TABLE_NAMES } from '@mastra/core/storage';
15
15
  import type { UpsertVectorParams } from '@mastra/core/vector';
16
16
  import type { VectorFilter } from '@mastra/core/vector/filter';
17
+ import type { WorkflowRun } from '@mastra/core/storage';
18
+ import type { WorkflowRuns } from '@mastra/core/storage';
17
19
  import type { WorkflowRunState } from '@mastra/core/workflows';
18
20
 
19
21
  declare interface UpstashConfig {
@@ -107,23 +109,21 @@ declare class UpstashStore extends MastraStorage {
107
109
  workflowName: string;
108
110
  runId: string;
109
111
  }): Promise<WorkflowRunState | null>;
110
- getWorkflowRuns({ namespace, workflowName, fromDate, toDate, limit, offset, }?: {
112
+ private parseWorkflowRun;
113
+ getWorkflowRuns({ namespace, workflowName, fromDate, toDate, limit, offset, resourceId, }?: {
111
114
  namespace: string;
112
115
  workflowName?: string;
113
116
  fromDate?: Date;
114
117
  toDate?: Date;
115
118
  limit?: number;
116
119
  offset?: number;
117
- }): Promise<{
118
- runs: Array<{
119
- workflowName: string;
120
- runId: string;
121
- snapshot: WorkflowRunState | string;
122
- createdAt: Date;
123
- updatedAt: Date;
124
- }>;
125
- total: number;
126
- }>;
120
+ resourceId?: string;
121
+ }): Promise<WorkflowRuns>;
122
+ getWorkflowRunById({ namespace, runId, workflowName, }: {
123
+ namespace: string;
124
+ runId: string;
125
+ workflowName?: string;
126
+ }): Promise<WorkflowRun | null>;
127
127
  close(): Promise<void>;
128
128
  }
129
129
  export { UpstashStore }
@@ -14,6 +14,8 @@ import type { StorageThreadType } from '@mastra/core/memory';
14
14
  import type { TABLE_NAMES } from '@mastra/core/storage';
15
15
  import type { UpsertVectorParams } from '@mastra/core/vector';
16
16
  import type { VectorFilter } from '@mastra/core/vector/filter';
17
+ import type { WorkflowRun } from '@mastra/core/storage';
18
+ import type { WorkflowRuns } from '@mastra/core/storage';
17
19
  import type { WorkflowRunState } from '@mastra/core/workflows';
18
20
 
19
21
  declare interface UpstashConfig {
@@ -107,23 +109,21 @@ declare class UpstashStore extends MastraStorage {
107
109
  workflowName: string;
108
110
  runId: string;
109
111
  }): Promise<WorkflowRunState | null>;
110
- getWorkflowRuns({ namespace, workflowName, fromDate, toDate, limit, offset, }?: {
112
+ private parseWorkflowRun;
113
+ getWorkflowRuns({ namespace, workflowName, fromDate, toDate, limit, offset, resourceId, }?: {
111
114
  namespace: string;
112
115
  workflowName?: string;
113
116
  fromDate?: Date;
114
117
  toDate?: Date;
115
118
  limit?: number;
116
119
  offset?: number;
117
- }): Promise<{
118
- runs: Array<{
119
- workflowName: string;
120
- runId: string;
121
- snapshot: WorkflowRunState | string;
122
- createdAt: Date;
123
- updatedAt: Date;
124
- }>;
125
- total: number;
126
- }>;
120
+ resourceId?: string;
121
+ }): Promise<WorkflowRuns>;
122
+ getWorkflowRunById({ namespace, runId, workflowName, }: {
123
+ namespace: string;
124
+ runId: string;
125
+ workflowName?: string;
126
+ }): Promise<WorkflowRun | null>;
127
127
  close(): Promise<void>;
128
128
  }
129
129
  export { UpstashStore }
package/dist/index.cjs CHANGED
@@ -207,7 +207,8 @@ var UpstashStore = class extends storage.MastraStorage {
207
207
  key = this.getKey(tableName, {
208
208
  namespace: record.namespace || "workflows",
209
209
  workflow_name: record.workflow_name,
210
- run_id: record.run_id
210
+ run_id: record.run_id,
211
+ ...record.resourceId ? { resourceId: record.resourceId } : {}
211
212
  });
212
213
  } else if (tableName === storage.TABLE_EVALS) {
213
214
  key = this.getKey(tableName, { id: record.run_id });
@@ -372,48 +373,90 @@ var UpstashStore = class extends storage.MastraStorage {
372
373
  if (!data) return null;
373
374
  return data.snapshot;
374
375
  }
376
+ parseWorkflowRun(row) {
377
+ let parsedSnapshot = row.snapshot;
378
+ if (typeof parsedSnapshot === "string") {
379
+ try {
380
+ parsedSnapshot = JSON.parse(row.snapshot);
381
+ } catch (e) {
382
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
383
+ }
384
+ }
385
+ return {
386
+ workflowName: row.workflow_name,
387
+ runId: row.run_id,
388
+ snapshot: parsedSnapshot,
389
+ createdAt: this.ensureDate(row.createdAt),
390
+ updatedAt: this.ensureDate(row.updatedAt),
391
+ resourceId: row.resourceId
392
+ };
393
+ }
375
394
  async getWorkflowRuns({
376
395
  namespace,
377
396
  workflowName,
378
397
  fromDate,
379
398
  toDate,
380
399
  limit,
381
- offset
400
+ offset,
401
+ resourceId
382
402
  } = { namespace: "workflows" }) {
383
- const pattern = workflowName ? this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ":*" : this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ":*";
384
- const keys = await this.redis.keys(pattern);
385
- const workflows = await Promise.all(
386
- keys.map(async (key) => {
387
- const data = await this.redis.get(key);
388
- return data;
389
- })
390
- );
391
- let runs = workflows.filter((w) => w !== null).map((w) => {
392
- let parsedSnapshot = w.snapshot;
393
- if (typeof parsedSnapshot === "string") {
394
- try {
395
- parsedSnapshot = JSON.parse(w.snapshot);
396
- } catch {
397
- console.warn(`Failed to parse snapshot for workflow ${w.workflow_name}:`);
398
- }
403
+ try {
404
+ let pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ":*";
405
+ if (workflowName && resourceId) {
406
+ pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, {
407
+ namespace,
408
+ workflow_name: workflowName,
409
+ run_id: "*",
410
+ resourceId
411
+ });
412
+ } else if (workflowName) {
413
+ pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ":*";
414
+ } else if (resourceId) {
415
+ pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: "*", run_id: "*", resourceId });
399
416
  }
400
- return {
401
- workflowName: w.workflow_name,
402
- runId: w.run_id,
403
- snapshot: parsedSnapshot,
404
- createdAt: this.ensureDate(w.createdAt),
405
- updatedAt: this.ensureDate(w.updatedAt)
406
- };
407
- }).filter((w) => {
408
- if (fromDate && w.createdAt < fromDate) return false;
409
- if (toDate && w.createdAt > toDate) return false;
410
- return true;
411
- }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
412
- const total = runs.length;
413
- if (limit !== void 0 && offset !== void 0) {
414
- runs = runs.slice(offset, offset + limit);
415
- }
416
- return { runs, total };
417
+ const keys = await this.redis.keys(pattern);
418
+ const workflows = await Promise.all(
419
+ keys.map(async (key) => {
420
+ const data = await this.redis.get(key);
421
+ return data;
422
+ })
423
+ );
424
+ let runs = workflows.filter((w) => w !== null).map((w) => this.parseWorkflowRun(w)).filter((w) => {
425
+ if (fromDate && w.createdAt < fromDate) return false;
426
+ if (toDate && w.createdAt > toDate) return false;
427
+ return true;
428
+ }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
429
+ const total = runs.length;
430
+ if (limit !== void 0 && offset !== void 0) {
431
+ runs = runs.slice(offset, offset + limit);
432
+ }
433
+ return { runs, total };
434
+ } catch (error) {
435
+ console.error("Error getting workflow runs:", error);
436
+ throw error;
437
+ }
438
+ }
439
+ async getWorkflowRunById({
440
+ namespace = "workflows",
441
+ runId,
442
+ workflowName
443
+ }) {
444
+ try {
445
+ const key = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + "*";
446
+ const keys = await this.redis.keys(key);
447
+ const workflows = await Promise.all(
448
+ keys.map(async (key2) => {
449
+ const data2 = await this.redis.get(key2);
450
+ return data2;
451
+ })
452
+ );
453
+ const data = workflows.find((w) => w?.run_id === runId && w?.workflow_name === workflowName);
454
+ if (!data) return null;
455
+ return this.parseWorkflowRun(data);
456
+ } catch (error) {
457
+ console.error("Error getting workflow run by ID:", error);
458
+ throw error;
459
+ }
417
460
  }
418
461
  async close() {
419
462
  }
package/dist/index.js CHANGED
@@ -205,7 +205,8 @@ var UpstashStore = class extends MastraStorage {
205
205
  key = this.getKey(tableName, {
206
206
  namespace: record.namespace || "workflows",
207
207
  workflow_name: record.workflow_name,
208
- run_id: record.run_id
208
+ run_id: record.run_id,
209
+ ...record.resourceId ? { resourceId: record.resourceId } : {}
209
210
  });
210
211
  } else if (tableName === TABLE_EVALS) {
211
212
  key = this.getKey(tableName, { id: record.run_id });
@@ -370,48 +371,90 @@ var UpstashStore = class extends MastraStorage {
370
371
  if (!data) return null;
371
372
  return data.snapshot;
372
373
  }
374
+ parseWorkflowRun(row) {
375
+ let parsedSnapshot = row.snapshot;
376
+ if (typeof parsedSnapshot === "string") {
377
+ try {
378
+ parsedSnapshot = JSON.parse(row.snapshot);
379
+ } catch (e) {
380
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
381
+ }
382
+ }
383
+ return {
384
+ workflowName: row.workflow_name,
385
+ runId: row.run_id,
386
+ snapshot: parsedSnapshot,
387
+ createdAt: this.ensureDate(row.createdAt),
388
+ updatedAt: this.ensureDate(row.updatedAt),
389
+ resourceId: row.resourceId
390
+ };
391
+ }
373
392
  async getWorkflowRuns({
374
393
  namespace,
375
394
  workflowName,
376
395
  fromDate,
377
396
  toDate,
378
397
  limit,
379
- offset
398
+ offset,
399
+ resourceId
380
400
  } = { namespace: "workflows" }) {
381
- const pattern = workflowName ? this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ":*" : this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ":*";
382
- const keys = await this.redis.keys(pattern);
383
- const workflows = await Promise.all(
384
- keys.map(async (key) => {
385
- const data = await this.redis.get(key);
386
- return data;
387
- })
388
- );
389
- let runs = workflows.filter((w) => w !== null).map((w) => {
390
- let parsedSnapshot = w.snapshot;
391
- if (typeof parsedSnapshot === "string") {
392
- try {
393
- parsedSnapshot = JSON.parse(w.snapshot);
394
- } catch {
395
- console.warn(`Failed to parse snapshot for workflow ${w.workflow_name}:`);
396
- }
401
+ try {
402
+ let pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ":*";
403
+ if (workflowName && resourceId) {
404
+ pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
405
+ namespace,
406
+ workflow_name: workflowName,
407
+ run_id: "*",
408
+ resourceId
409
+ });
410
+ } else if (workflowName) {
411
+ pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ":*";
412
+ } else if (resourceId) {
413
+ pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: "*", run_id: "*", resourceId });
397
414
  }
398
- return {
399
- workflowName: w.workflow_name,
400
- runId: w.run_id,
401
- snapshot: parsedSnapshot,
402
- createdAt: this.ensureDate(w.createdAt),
403
- updatedAt: this.ensureDate(w.updatedAt)
404
- };
405
- }).filter((w) => {
406
- if (fromDate && w.createdAt < fromDate) return false;
407
- if (toDate && w.createdAt > toDate) return false;
408
- return true;
409
- }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
410
- const total = runs.length;
411
- if (limit !== void 0 && offset !== void 0) {
412
- runs = runs.slice(offset, offset + limit);
413
- }
414
- return { runs, total };
415
+ const keys = await this.redis.keys(pattern);
416
+ const workflows = await Promise.all(
417
+ keys.map(async (key) => {
418
+ const data = await this.redis.get(key);
419
+ return data;
420
+ })
421
+ );
422
+ let runs = workflows.filter((w) => w !== null).map((w) => this.parseWorkflowRun(w)).filter((w) => {
423
+ if (fromDate && w.createdAt < fromDate) return false;
424
+ if (toDate && w.createdAt > toDate) return false;
425
+ return true;
426
+ }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
427
+ const total = runs.length;
428
+ if (limit !== void 0 && offset !== void 0) {
429
+ runs = runs.slice(offset, offset + limit);
430
+ }
431
+ return { runs, total };
432
+ } catch (error) {
433
+ console.error("Error getting workflow runs:", error);
434
+ throw error;
435
+ }
436
+ }
437
+ async getWorkflowRunById({
438
+ namespace = "workflows",
439
+ runId,
440
+ workflowName
441
+ }) {
442
+ try {
443
+ const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + "*";
444
+ const keys = await this.redis.keys(key);
445
+ const workflows = await Promise.all(
446
+ keys.map(async (key2) => {
447
+ const data2 = await this.redis.get(key2);
448
+ return data2;
449
+ })
450
+ );
451
+ const data = workflows.find((w) => w?.run_id === runId && w?.workflow_name === workflowName);
452
+ if (!data) return null;
453
+ return this.parseWorkflowRun(data);
454
+ } catch (error) {
455
+ console.error("Error getting workflow run by ID:", error);
456
+ throw error;
457
+ }
415
458
  }
416
459
  async close() {
417
460
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/upstash",
3
- "version": "0.3.1-alpha.3",
3
+ "version": "0.3.1-alpha.4",
4
4
  "description": "Upstash provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,16 +22,16 @@
22
22
  "dependencies": {
23
23
  "@upstash/redis": "^1.34.5",
24
24
  "@upstash/vector": "^1.2.1",
25
- "@mastra/core": "^0.9.1-alpha.3"
25
+ "@mastra/core": "^0.9.1-alpha.4"
26
26
  },
27
27
  "devDependencies": {
28
- "@microsoft/api-extractor": "^7.52.1",
28
+ "@microsoft/api-extractor": "^7.52.5",
29
29
  "@types/node": "^20.17.27",
30
30
  "dotenv": "^16.4.7",
31
31
  "eslint": "^9.23.0",
32
32
  "tsup": "^8.4.0",
33
33
  "typescript": "^5.8.2",
34
- "vitest": "^3.0.9",
34
+ "vitest": "^3.1.2",
35
35
  "@internal/lint": "0.0.2"
36
36
  },
37
37
  "scripts": {
@@ -8,7 +8,14 @@ import {
8
8
  TABLE_EVALS,
9
9
  TABLE_TRACES,
10
10
  } from '@mastra/core/storage';
11
- import type { TABLE_NAMES, StorageColumn, StorageGetMessagesArg, EvalRow } from '@mastra/core/storage';
11
+ import type {
12
+ TABLE_NAMES,
13
+ StorageColumn,
14
+ StorageGetMessagesArg,
15
+ EvalRow,
16
+ WorkflowRuns,
17
+ WorkflowRun,
18
+ } from '@mastra/core/storage';
12
19
  import type { WorkflowRunState } from '@mastra/core/workflows';
13
20
  import { Redis } from '@upstash/redis';
14
21
 
@@ -298,6 +305,7 @@ export class UpstashStore extends MastraStorage {
298
305
  namespace: record.namespace || 'workflows',
299
306
  workflow_name: record.workflow_name,
300
307
  run_id: record.run_id,
308
+ ...(record.resourceId ? { resourceId: record.resourceId } : {}),
301
309
  });
302
310
  } else if (tableName === TABLE_EVALS) {
303
311
  key = this.getKey(tableName, { id: record.run_id });
@@ -531,6 +539,27 @@ export class UpstashStore extends MastraStorage {
531
539
  return data.snapshot;
532
540
  }
533
541
 
542
+ private parseWorkflowRun(row: any): WorkflowRun {
543
+ let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
544
+ if (typeof parsedSnapshot === 'string') {
545
+ try {
546
+ parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
547
+ } catch (e) {
548
+ // If parsing fails, return the raw snapshot string
549
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
550
+ }
551
+ }
552
+
553
+ return {
554
+ workflowName: row.workflow_name,
555
+ runId: row.run_id,
556
+ snapshot: parsedSnapshot,
557
+ createdAt: this.ensureDate(row.createdAt)!,
558
+ updatedAt: this.ensureDate(row.updatedAt)!,
559
+ resourceId: row.resourceId,
560
+ };
561
+ }
562
+
534
563
  async getWorkflowRuns(
535
564
  {
536
565
  namespace,
@@ -539,6 +568,7 @@ export class UpstashStore extends MastraStorage {
539
568
  toDate,
540
569
  limit,
541
570
  offset,
571
+ resourceId,
542
572
  }: {
543
573
  namespace: string;
544
574
  workflowName?: string;
@@ -546,74 +576,98 @@ export class UpstashStore extends MastraStorage {
546
576
  toDate?: Date;
547
577
  limit?: number;
548
578
  offset?: number;
579
+ resourceId?: string;
549
580
  } = { namespace: 'workflows' },
550
- ): Promise<{
551
- runs: Array<{
552
- workflowName: string;
553
- runId: string;
554
- snapshot: WorkflowRunState | string;
555
- createdAt: Date;
556
- updatedAt: Date;
557
- }>;
558
- total: number;
559
- }> {
560
- // Get all workflow keys
561
- const pattern = workflowName
562
- ? this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ':*'
563
- : this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ':*';
581
+ ): Promise<WorkflowRuns> {
582
+ try {
583
+ // Get all workflow keys
584
+ let pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ':*';
585
+ if (workflowName && resourceId) {
586
+ pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
587
+ namespace,
588
+ workflow_name: workflowName,
589
+ run_id: '*',
590
+ resourceId,
591
+ });
592
+ } else if (workflowName) {
593
+ pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ':*';
594
+ } else if (resourceId) {
595
+ pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: '*', run_id: '*', resourceId });
596
+ }
597
+ const keys = await this.redis.keys(pattern);
564
598
 
565
- const keys = await this.redis.keys(pattern);
599
+ // Get all workflow data
600
+ const workflows = await Promise.all(
601
+ keys.map(async key => {
602
+ const data = await this.redis.get<{
603
+ workflow_name: string;
604
+ run_id: string;
605
+ snapshot: WorkflowRunState | string;
606
+ createdAt: string | Date;
607
+ updatedAt: string | Date;
608
+ resourceId: string;
609
+ }>(key);
610
+ return data;
611
+ }),
612
+ );
566
613
 
567
- // Get all workflow data
568
- const workflows = await Promise.all(
569
- keys.map(async key => {
570
- const data = await this.redis.get<{
571
- workflow_name: string;
572
- run_id: string;
573
- snapshot: WorkflowRunState | string;
574
- createdAt: string | Date;
575
- updatedAt: string | Date;
576
- }>(key);
577
- return data;
578
- }),
579
- );
614
+ // Filter and transform results
615
+ let runs = workflows
616
+ .filter(w => w !== null)
617
+ .map(w => this.parseWorkflowRun(w!))
618
+ .filter(w => {
619
+ if (fromDate && w.createdAt < fromDate) return false;
620
+ if (toDate && w.createdAt > toDate) return false;
621
+ return true;
622
+ })
623
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
624
+
625
+ const total = runs.length;
626
+
627
+ // Apply pagination if requested
628
+ if (limit !== undefined && offset !== undefined) {
629
+ runs = runs.slice(offset, offset + limit);
630
+ }
580
631
 
581
- // Filter and transform results
582
- let runs = workflows
583
- .filter(w => w !== null)
584
- .map(w => {
585
- let parsedSnapshot: WorkflowRunState | string = w!.snapshot as string;
586
- if (typeof parsedSnapshot === 'string') {
587
- try {
588
- parsedSnapshot = JSON.parse(w!.snapshot as string) as WorkflowRunState;
589
- } catch {
590
- // If parsing fails, return the raw snapshot string
591
- console.warn(`Failed to parse snapshot for workflow ${w!.workflow_name}:`);
592
- }
593
- }
594
- return {
595
- workflowName: w!.workflow_name,
596
- runId: w!.run_id,
597
- snapshot: parsedSnapshot,
598
- createdAt: this.ensureDate(w!.createdAt)!,
599
- updatedAt: this.ensureDate(w!.updatedAt)!,
600
- };
601
- })
602
- .filter(w => {
603
- if (fromDate && w.createdAt < fromDate) return false;
604
- if (toDate && w.createdAt > toDate) return false;
605
- return true;
606
- })
607
- .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
608
-
609
- const total = runs.length;
610
-
611
- // Apply pagination if requested
612
- if (limit !== undefined && offset !== undefined) {
613
- runs = runs.slice(offset, offset + limit);
632
+ return { runs, total };
633
+ } catch (error) {
634
+ console.error('Error getting workflow runs:', error);
635
+ throw error;
614
636
  }
637
+ }
615
638
 
616
- return { runs, total };
639
+ async getWorkflowRunById({
640
+ namespace = 'workflows',
641
+ runId,
642
+ workflowName,
643
+ }: {
644
+ namespace: string;
645
+ runId: string;
646
+ workflowName?: string;
647
+ }): Promise<WorkflowRun | null> {
648
+ try {
649
+ const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + '*';
650
+ const keys = await this.redis.keys(key);
651
+ const workflows = await Promise.all(
652
+ keys.map(async key => {
653
+ const data = await this.redis.get<{
654
+ workflow_name: string;
655
+ run_id: string;
656
+ snapshot: WorkflowRunState | string;
657
+ createdAt: string | Date;
658
+ updatedAt: string | Date;
659
+ resourceId: string;
660
+ }>(key);
661
+ return data;
662
+ }),
663
+ );
664
+ const data = workflows.find(w => w?.run_id === runId && w?.workflow_name === workflowName) as WorkflowRun | null;
665
+ if (!data) return null;
666
+ return this.parseWorkflowRun(data);
667
+ } catch (error) {
668
+ console.error('Error getting workflow run by ID:', error);
669
+ throw error;
670
+ }
617
671
  }
618
672
 
619
673
  async close(): Promise<void> {
@@ -25,27 +25,26 @@ const createSampleThread = (date?: Date) => ({
25
25
  metadata: { key: 'value' },
26
26
  });
27
27
 
28
- const createSampleMessage = (threadId: string, content: string = 'Hello') =>
29
- ({
30
- id: `msg-${randomUUID()}`,
31
- role: 'user',
32
- type: 'text',
33
- threadId,
34
- content: [{ type: 'text', text: content }],
35
- createdAt: new Date(),
36
- }) as any;
28
+ const createSampleMessage = (threadId: string, content: string = 'Hello'): MessageType => ({
29
+ id: `msg-${randomUUID()}`,
30
+ role: 'user',
31
+ type: 'text',
32
+ threadId,
33
+ content: [{ type: 'text', text: content }],
34
+ createdAt: new Date(),
35
+ resourceId: `resource-${randomUUID()}`,
36
+ });
37
37
 
38
38
  const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
39
39
  const runId = `run-${randomUUID()}`;
40
40
  const stepId = `step-${randomUUID()}`;
41
41
  const timestamp = createdAt || new Date();
42
- const snapshot = {
43
- result: { success: true },
42
+ const snapshot: WorkflowRunState = {
44
43
  value: {},
45
44
  context: {
46
45
  steps: {
47
46
  [stepId]: {
48
- status,
47
+ status: status as WorkflowRunState['context']['steps'][string]['status'],
49
48
  payload: {},
50
49
  error: undefined,
51
50
  },
@@ -54,9 +53,10 @@ const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
54
53
  attempts: {},
55
54
  },
56
55
  activePaths: [],
56
+ suspendedPaths: {},
57
57
  runId,
58
58
  timestamp: timestamp.getTime(),
59
- } as WorkflowRunState;
59
+ };
60
60
  return { snapshot, runId, stepId };
61
61
  };
62
62
 
@@ -94,6 +94,13 @@ const createSampleEval = (agentName: string, isTest = false) => {
94
94
  };
95
95
  };
96
96
 
97
+ const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
98
+ if (typeof snapshot === 'string') {
99
+ throw new Error('Expected WorkflowRunState, got string');
100
+ }
101
+ expect(snapshot.context?.steps[stepId]?.status).toBe(status);
102
+ };
103
+
97
104
  describe('UpstashStore', () => {
98
105
  let store: UpstashStore;
99
106
  const testTableName = 'test_table';
@@ -303,17 +310,17 @@ describe('UpstashStore', () => {
303
310
  });
304
311
 
305
312
  it('should save and retrieve messages in order', async () => {
306
- const messages = [
313
+ const messages: MessageType[] = [
307
314
  createSampleMessage(threadId, 'First'),
308
315
  createSampleMessage(threadId, 'Second'),
309
316
  createSampleMessage(threadId, 'Third'),
310
317
  ];
311
318
 
312
- await store.saveMessages({ messages: messages as MessageType[] });
319
+ await store.saveMessages({ messages: messages });
313
320
 
314
- const retrievedMessages = await store.getMessages({ threadId });
321
+ const retrievedMessages = await store.getMessages<MessageType[]>({ threadId });
315
322
  expect(retrievedMessages).toHaveLength(3);
316
- expect(retrievedMessages.map(m => m.content[0].text)).toEqual(['First', 'Second', 'Third']);
323
+ expect(retrievedMessages.map((m: any) => m.content[0].text)).toEqual(['First', 'Second', 'Third']);
317
324
  });
318
325
 
319
326
  it('should handle empty message array', async () => {
@@ -335,11 +342,11 @@ describe('UpstashStore', () => {
335
342
  ],
336
343
  createdAt: new Date(),
337
344
  },
338
- ];
345
+ ] as MessageType[];
339
346
 
340
- await store.saveMessages({ messages: messages as MessageType[] });
347
+ await store.saveMessages({ messages });
341
348
 
342
- const retrievedMessages = await store.getMessages({ threadId });
349
+ const retrievedMessages = await store.getMessages<MessageType>({ threadId });
343
350
  expect(retrievedMessages[0].content).toEqual(messages[0].content);
344
351
  });
345
352
  });
@@ -451,13 +458,15 @@ describe('UpstashStore', () => {
451
458
  stepResults: {
452
459
  step1: { status: 'success', payload: { result: 'done' } },
453
460
  },
461
+ steps: {},
454
462
  attempts: {},
455
463
  triggerData: {},
456
464
  },
457
465
  runId: testRunId,
458
466
  activePaths: [],
467
+ suspendedPaths: {},
459
468
  timestamp: Date.now(),
460
- } as unknown as WorkflowRunState;
469
+ };
461
470
 
462
471
  await store.persistWorkflowSnapshot({
463
472
  namespace: testNamespace,
@@ -556,8 +565,8 @@ describe('UpstashStore', () => {
556
565
  const workflowName1 = 'default_test_1';
557
566
  const workflowName2 = 'default_test_2';
558
567
 
559
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('completed');
560
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('running');
568
+ const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
569
+ const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
561
570
 
562
571
  await store.persistWorkflowSnapshot({
563
572
  namespace: testNamespace,
@@ -578,17 +587,17 @@ describe('UpstashStore', () => {
578
587
  expect(total).toBe(2);
579
588
  expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
580
589
  expect(runs[1]!.workflowName).toBe(workflowName1);
581
- const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
582
- const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
583
- expect(firstSnapshot.context?.steps[stepId2]?.status).toBe('running');
584
- expect(secondSnapshot.context?.steps[stepId1]?.status).toBe('completed');
590
+ const firstSnapshot = runs[0]!.snapshot;
591
+ const secondSnapshot = runs[1]!.snapshot;
592
+ checkWorkflowSnapshot(firstSnapshot, stepId2, 'waiting');
593
+ checkWorkflowSnapshot(secondSnapshot, stepId1, 'success');
585
594
  });
586
595
 
587
596
  it('filters by workflow name', async () => {
588
597
  const workflowName1 = 'filter_test_1';
589
598
  const workflowName2 = 'filter_test_2';
590
599
 
591
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('completed');
600
+ const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
592
601
  const { snapshot: workflow2, runId: runId2 } = createSampleWorkflowSnapshot('failed');
593
602
 
594
603
  await store.persistWorkflowSnapshot({
@@ -609,8 +618,11 @@ describe('UpstashStore', () => {
609
618
  expect(runs).toHaveLength(1);
610
619
  expect(total).toBe(1);
611
620
  expect(runs[0]!.workflowName).toBe(workflowName1);
612
- const snapshot = runs[0]!.snapshot as WorkflowRunState;
613
- expect(snapshot.context?.steps[stepId1]?.status).toBe('completed');
621
+ const snapshot = runs[0]!.snapshot;
622
+ if (typeof snapshot === 'string') {
623
+ throw new Error('Expected WorkflowRunState, got string');
624
+ }
625
+ expect(snapshot.context?.steps[stepId1]?.status).toBe('success');
614
626
  });
615
627
 
616
628
  it('filters by date range', async () => {
@@ -621,9 +633,9 @@ describe('UpstashStore', () => {
621
633
  const workflowName2 = 'date_test_2';
622
634
  const workflowName3 = 'date_test_3';
623
635
 
624
- const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('completed');
625
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('running');
626
- const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('waiting');
636
+ const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('success');
637
+ const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
638
+ const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
627
639
 
628
640
  await store.insert({
629
641
  tableName: TABLE_WORKFLOW_SNAPSHOT,
@@ -668,10 +680,10 @@ describe('UpstashStore', () => {
668
680
  expect(runs).toHaveLength(2);
669
681
  expect(runs[0]!.workflowName).toBe(workflowName3);
670
682
  expect(runs[1]!.workflowName).toBe(workflowName2);
671
- const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
672
- const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
673
- expect(firstSnapshot.context?.steps[stepId3]?.status).toBe('waiting');
674
- expect(secondSnapshot.context?.steps[stepId2]?.status).toBe('running');
683
+ const firstSnapshot = runs[0]!.snapshot;
684
+ const secondSnapshot = runs[1]!.snapshot;
685
+ checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
686
+ checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
675
687
  });
676
688
 
677
689
  it('handles pagination', async () => {
@@ -679,9 +691,9 @@ describe('UpstashStore', () => {
679
691
  const workflowName2 = 'page_test_2';
680
692
  const workflowName3 = 'page_test_3';
681
693
 
682
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('completed');
683
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('running');
684
- const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('waiting');
694
+ const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
695
+ const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
696
+ const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
685
697
 
686
698
  await store.persistWorkflowSnapshot({
687
699
  namespace: testNamespace,
@@ -714,10 +726,10 @@ describe('UpstashStore', () => {
714
726
  expect(page1.total).toBe(3); // Total count of all records
715
727
  expect(page1.runs[0]!.workflowName).toBe(workflowName3);
716
728
  expect(page1.runs[1]!.workflowName).toBe(workflowName2);
717
- const firstSnapshot = page1.runs[0]!.snapshot as WorkflowRunState;
718
- const secondSnapshot = page1.runs[1]!.snapshot as WorkflowRunState;
719
- expect(firstSnapshot.context?.steps[stepId3]?.status).toBe('waiting');
720
- expect(secondSnapshot.context?.steps[stepId2]?.status).toBe('running');
729
+ const firstSnapshot = page1.runs[0]!.snapshot;
730
+ const secondSnapshot = page1.runs[1]!.snapshot;
731
+ checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
732
+ checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
721
733
 
722
734
  // Get second page
723
735
  const page2 = await store.getWorkflowRuns({
@@ -728,8 +740,118 @@ describe('UpstashStore', () => {
728
740
  expect(page2.runs).toHaveLength(1);
729
741
  expect(page2.total).toBe(3);
730
742
  expect(page2.runs[0]!.workflowName).toBe(workflowName1);
731
- const snapshot = page2.runs[0]!.snapshot as WorkflowRunState;
732
- expect(snapshot.context?.steps[stepId1]?.status).toBe('completed');
743
+ const snapshot = page2.runs[0]!.snapshot;
744
+ checkWorkflowSnapshot(snapshot, stepId1, 'success');
745
+ });
746
+ });
747
+ describe('getWorkflowRunById', () => {
748
+ const testNamespace = 'test-workflows-id';
749
+ const workflowName = 'workflow-id-test';
750
+ let runId: string;
751
+ let stepId: string;
752
+
753
+ beforeAll(async () => {
754
+ // Insert a workflow run for positive test
755
+ const sample = createSampleWorkflowSnapshot('success');
756
+ runId = sample.runId;
757
+ stepId = sample.stepId;
758
+ await store.insert({
759
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
760
+ record: {
761
+ namespace: testNamespace,
762
+ workflow_name: workflowName,
763
+ run_id: runId,
764
+ resourceId: 'resource-abc',
765
+ snapshot: sample.snapshot,
766
+ createdAt: new Date(),
767
+ updatedAt: new Date(),
768
+ },
769
+ });
770
+ });
771
+
772
+ it('should retrieve a workflow run by ID', async () => {
773
+ const found = await store.getWorkflowRunById({
774
+ namespace: testNamespace,
775
+ runId,
776
+ workflowName,
777
+ });
778
+ expect(found).not.toBeNull();
779
+ expect(found?.runId).toBe(runId);
780
+ const snapshot = found?.snapshot;
781
+ checkWorkflowSnapshot(snapshot!, stepId, 'success');
782
+ });
783
+
784
+ it('should return null for non-existent workflow run ID', async () => {
785
+ const notFound = await store.getWorkflowRunById({
786
+ namespace: testNamespace,
787
+ runId: 'non-existent-id',
788
+ workflowName,
789
+ });
790
+ expect(notFound).toBeNull();
791
+ });
792
+ });
793
+ describe('getWorkflowRuns with resourceId', () => {
794
+ const testNamespace = 'test-workflows-id';
795
+ const workflowName = 'workflow-id-test';
796
+ let resourceId: string;
797
+ let runIds: string[] = [];
798
+
799
+ beforeAll(async () => {
800
+ // Insert multiple workflow runs for the same resourceId
801
+ resourceId = 'resource-shared';
802
+ for (const status of ['success', 'waiting']) {
803
+ const sample = createSampleWorkflowSnapshot(status);
804
+ runIds.push(sample.runId);
805
+ await store.insert({
806
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
807
+ record: {
808
+ namespace: testNamespace,
809
+ workflow_name: workflowName,
810
+ run_id: sample.runId,
811
+ resourceId,
812
+ snapshot: sample.snapshot,
813
+ createdAt: new Date(),
814
+ updatedAt: new Date(),
815
+ },
816
+ });
817
+ }
818
+ // Insert a run with a different resourceId
819
+ const other = createSampleWorkflowSnapshot('waiting');
820
+ await store.insert({
821
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
822
+ record: {
823
+ namespace: testNamespace,
824
+ workflow_name: workflowName,
825
+ run_id: other.runId,
826
+ resourceId: 'resource-other',
827
+ snapshot: other.snapshot,
828
+ createdAt: new Date(),
829
+ updatedAt: new Date(),
830
+ },
831
+ });
832
+ });
833
+
834
+ it('should retrieve all workflow runs by resourceId', async () => {
835
+ const { runs } = await store.getWorkflowRuns({
836
+ namespace: testNamespace,
837
+ resourceId,
838
+ workflowName,
839
+ });
840
+ expect(Array.isArray(runs)).toBe(true);
841
+ expect(runs.length).toBeGreaterThanOrEqual(2);
842
+ for (const run of runs) {
843
+ expect(run.resourceId).toBe(resourceId);
844
+ }
845
+ });
846
+
847
+ it('should return an empty array if no workflow runs match resourceId', async () => {
848
+ const { runs } = await store.getWorkflowRuns({
849
+ namespace: testNamespace,
850
+ resourceId: 'non-existent-resource',
851
+ workflowName,
852
+ });
853
+ expect(Array.isArray(runs)).toBe(true);
854
+ expect(runs.length).toBe(0);
733
855
  });
734
856
  });
735
857
  });