@mastra/pg 0.3.1-alpha.3 → 0.3.1-alpha.5

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.
@@ -4,7 +4,7 @@ import type { MessageType } from '@mastra/core/memory';
4
4
  import { TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, TABLE_THREADS, TABLE_EVALS } from '@mastra/core/storage';
5
5
  import type { WorkflowRunState } from '@mastra/core/workflows';
6
6
  import pgPromise from 'pg-promise';
7
- import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
7
+ import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach, vi } from 'vitest';
8
8
 
9
9
  import { PostgresStore } from '.';
10
10
  import type { PostgresConfig } from '.';
@@ -19,6 +19,8 @@ const TEST_CONFIG: PostgresConfig = {
19
19
 
20
20
  const connectionString = `postgresql://${TEST_CONFIG.user}:${TEST_CONFIG.password}@${TEST_CONFIG.host}:${TEST_CONFIG.port}/${TEST_CONFIG.database}`;
21
21
 
22
+ vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
23
+
22
24
  // Sample test data factory functions
23
25
  const createSampleThread = () => ({
24
26
  id: `thread-${randomUUID()}`,
@@ -29,17 +31,20 @@ const createSampleThread = () => ({
29
31
  metadata: { key: 'value' },
30
32
  });
31
33
 
32
- const createSampleMessage = (threadId: string) =>
33
- ({
34
- id: `msg-${randomUUID()}`,
35
- role: 'user',
36
- type: 'text',
37
- threadId,
38
- content: [{ type: 'text', text: 'Hello' }],
39
- createdAt: new Date(),
40
- }) as any;
41
-
42
- const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
34
+ const createSampleMessage = (threadId: string): MessageType => ({
35
+ id: `msg-${randomUUID()}`,
36
+ resourceId: `resource-${randomUUID()}`,
37
+ role: 'user',
38
+ type: 'text',
39
+ threadId,
40
+ content: [{ type: 'text', text: 'Hello' }],
41
+ createdAt: new Date(),
42
+ });
43
+
44
+ const createSampleWorkflowSnapshot = (
45
+ status: WorkflowRunState['context']['steps'][string]['status'],
46
+ createdAt?: Date,
47
+ ) => {
43
48
  const runId = `run-${randomUUID()}`;
44
49
  const stepId = `step-${randomUUID()}`;
45
50
  const timestamp = createdAt || new Date();
@@ -58,9 +63,10 @@ const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
58
63
  attempts: {},
59
64
  },
60
65
  activePaths: [],
66
+ suspendedPaths: {},
61
67
  runId,
62
68
  timestamp: timestamp.getTime(),
63
- } as WorkflowRunState;
69
+ };
64
70
  return { snapshot, runId, stepId };
65
71
  };
66
72
 
@@ -82,6 +88,13 @@ const createSampleEval = (agentName: string, isTest = false) => {
82
88
  };
83
89
  };
84
90
 
91
+ const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
92
+ if (typeof snapshot === 'string') {
93
+ throw new Error('Expected WorkflowRunState, got string');
94
+ }
95
+ expect(snapshot.context?.steps[stepId]?.status).toBe(status);
96
+ };
97
+
85
98
  describe('PostgresStore', () => {
86
99
  let store: PostgresStore;
87
100
 
@@ -233,7 +246,11 @@ describe('PostgresStore', () => {
233
246
  // Retrieve messages
234
247
  const retrievedMessages = await store.getMessages({ threadId: thread.id });
235
248
  expect(retrievedMessages).toHaveLength(2);
236
- expect(retrievedMessages).toEqual(expect.arrayContaining(messages));
249
+ const checkMessages = messages.map(m => {
250
+ const { resourceId, ...rest } = m;
251
+ return rest;
252
+ });
253
+ expect(retrievedMessages).toEqual(expect.arrayContaining(checkMessages));
237
254
  });
238
255
 
239
256
  it('should handle empty message array', async () => {
@@ -253,12 +270,13 @@ describe('PostgresStore', () => {
253
270
 
254
271
  await store.saveMessages({ messages });
255
272
 
256
- const retrievedMessages = await store.getMessages({ threadId: thread.id });
273
+ const retrievedMessages = await store.getMessages<MessageType>({ threadId: thread.id });
257
274
  expect(retrievedMessages).toHaveLength(3);
258
275
 
259
276
  // Verify order is maintained
260
277
  retrievedMessages.forEach((msg, idx) => {
261
- expect((msg.content[0] as any).text).toBe((messages[idx].content[0] as any).text);
278
+ // @ts-expect-error
279
+ expect(msg.content[0].text).toBe(messages[idx].content[0].text);
262
280
  });
263
281
  });
264
282
 
@@ -338,11 +356,17 @@ describe('PostgresStore', () => {
338
356
  const snapshot = {
339
357
  status: 'running',
340
358
  context: {
359
+ steps: {},
341
360
  stepResults: {},
342
361
  attempts: {},
343
362
  triggerData: { type: 'manual' },
344
363
  },
345
- } as any;
364
+ value: {},
365
+ activePaths: [],
366
+ suspendedPaths: {},
367
+ runId,
368
+ timestamp: new Date().getTime(),
369
+ };
346
370
 
347
371
  await store.persistWorkflowSnapshot({
348
372
  workflowName,
@@ -373,28 +397,40 @@ describe('PostgresStore', () => {
373
397
  const initialSnapshot = {
374
398
  status: 'running',
375
399
  context: {
400
+ steps: {},
376
401
  stepResults: {},
377
402
  attempts: {},
378
403
  triggerData: { type: 'manual' },
379
404
  },
405
+ value: {},
406
+ activePaths: [],
407
+ suspendedPaths: {},
408
+ runId,
409
+ timestamp: new Date().getTime(),
380
410
  };
381
411
 
382
412
  await store.persistWorkflowSnapshot({
383
413
  workflowName,
384
414
  runId,
385
- snapshot: initialSnapshot as any,
415
+ snapshot: initialSnapshot,
386
416
  });
387
417
 
388
418
  const updatedSnapshot = {
389
419
  status: 'completed',
390
420
  context: {
421
+ steps: {},
391
422
  stepResults: {
392
423
  'step-1': { status: 'success', result: { data: 'test' } },
393
424
  },
394
425
  attempts: { 'step-1': 1 },
395
426
  triggerData: { type: 'manual' },
396
427
  },
397
- } as any;
428
+ value: {},
429
+ activePaths: [],
430
+ suspendedPaths: {},
431
+ runId,
432
+ timestamp: new Date().getTime(),
433
+ };
398
434
 
399
435
  await store.persistWorkflowSnapshot({
400
436
  workflowName,
@@ -432,6 +468,7 @@ describe('PostgresStore', () => {
432
468
  dependencies: ['step-3', 'step-4'],
433
469
  },
434
470
  },
471
+ steps: {},
435
472
  attempts: { 'step-1': 1, 'step-2': 0 },
436
473
  triggerData: {
437
474
  type: 'scheduled',
@@ -453,6 +490,7 @@ describe('PostgresStore', () => {
453
490
  status: 'waiting',
454
491
  },
455
492
  ],
493
+ suspendedPaths: {},
456
494
  runId: runId,
457
495
  timestamp: Date.now(),
458
496
  };
@@ -460,7 +498,7 @@ describe('PostgresStore', () => {
460
498
  await store.persistWorkflowSnapshot({
461
499
  workflowName,
462
500
  runId,
463
- snapshot: complexSnapshot as unknown as WorkflowRunState,
501
+ snapshot: complexSnapshot,
464
502
  });
465
503
 
466
504
  const loadedSnapshot = await store.loadWorkflowSnapshot({
@@ -486,8 +524,8 @@ describe('PostgresStore', () => {
486
524
  const workflowName1 = 'default_test_1';
487
525
  const workflowName2 = 'default_test_2';
488
526
 
489
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('completed');
490
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('running');
527
+ const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
528
+ const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('failed');
491
529
 
492
530
  await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
493
531
  await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
@@ -498,17 +536,17 @@ describe('PostgresStore', () => {
498
536
  expect(total).toBe(2);
499
537
  expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
500
538
  expect(runs[1]!.workflowName).toBe(workflowName1);
501
- const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
502
- const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
503
- expect(firstSnapshot.context?.steps[stepId2]?.status).toBe('running');
504
- expect(secondSnapshot.context?.steps[stepId1]?.status).toBe('completed');
539
+ const firstSnapshot = runs[0]!.snapshot;
540
+ const secondSnapshot = runs[1]!.snapshot;
541
+ checkWorkflowSnapshot(firstSnapshot, stepId2, 'failed');
542
+ checkWorkflowSnapshot(secondSnapshot, stepId1, 'success');
505
543
  });
506
544
 
507
545
  it('filters by workflow name', async () => {
508
546
  const workflowName1 = 'filter_test_1';
509
547
  const workflowName2 = 'filter_test_2';
510
548
 
511
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('completed');
549
+ const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
512
550
  const { snapshot: workflow2, runId: runId2 } = createSampleWorkflowSnapshot('failed');
513
551
 
514
552
  await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
@@ -519,8 +557,8 @@ describe('PostgresStore', () => {
519
557
  expect(runs).toHaveLength(1);
520
558
  expect(total).toBe(1);
521
559
  expect(runs[0]!.workflowName).toBe(workflowName1);
522
- const snapshot = runs[0]!.snapshot as WorkflowRunState;
523
- expect(snapshot.context?.steps[stepId1]?.status).toBe('completed');
560
+ const snapshot = runs[0]!.snapshot;
561
+ checkWorkflowSnapshot(snapshot, stepId1, 'success');
524
562
  });
525
563
 
526
564
  it('filters by date range', async () => {
@@ -531,9 +569,9 @@ describe('PostgresStore', () => {
531
569
  const workflowName2 = 'date_test_2';
532
570
  const workflowName3 = 'date_test_3';
533
571
 
534
- const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('completed');
535
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('running');
536
- const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('waiting');
572
+ const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('success');
573
+ const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('failed');
574
+ const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('suspended');
537
575
 
538
576
  await store.insert({
539
577
  tableName: TABLE_WORKFLOW_SNAPSHOT,
@@ -574,10 +612,10 @@ describe('PostgresStore', () => {
574
612
  expect(runs).toHaveLength(2);
575
613
  expect(runs[0]!.workflowName).toBe(workflowName3);
576
614
  expect(runs[1]!.workflowName).toBe(workflowName2);
577
- const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
578
- const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
579
- expect(firstSnapshot.context?.steps[stepId3]?.status).toBe('waiting');
580
- expect(secondSnapshot.context?.steps[stepId2]?.status).toBe('running');
615
+ const firstSnapshot = runs[0]!.snapshot;
616
+ const secondSnapshot = runs[1]!.snapshot;
617
+ checkWorkflowSnapshot(firstSnapshot, stepId3, 'suspended');
618
+ checkWorkflowSnapshot(secondSnapshot, stepId2, 'failed');
581
619
  });
582
620
 
583
621
  it('handles pagination', async () => {
@@ -585,9 +623,9 @@ describe('PostgresStore', () => {
585
623
  const workflowName2 = 'page_test_2';
586
624
  const workflowName3 = 'page_test_3';
587
625
 
588
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('completed');
589
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('running');
590
- const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('waiting');
626
+ const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
627
+ const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('failed');
628
+ const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('suspended');
591
629
 
592
630
  await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
593
631
  await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
@@ -601,18 +639,119 @@ describe('PostgresStore', () => {
601
639
  expect(page1.total).toBe(3); // Total count of all records
602
640
  expect(page1.runs[0]!.workflowName).toBe(workflowName3);
603
641
  expect(page1.runs[1]!.workflowName).toBe(workflowName2);
604
- const firstSnapshot = page1.runs[0]!.snapshot as WorkflowRunState;
605
- const secondSnapshot = page1.runs[1]!.snapshot as WorkflowRunState;
606
- expect(firstSnapshot.context?.steps[stepId3]?.status).toBe('waiting');
607
- expect(secondSnapshot.context?.steps[stepId2]?.status).toBe('running');
642
+ const firstSnapshot = page1.runs[0]!.snapshot;
643
+ const secondSnapshot = page1.runs[1]!.snapshot;
644
+ checkWorkflowSnapshot(firstSnapshot, stepId3, 'suspended');
645
+ checkWorkflowSnapshot(secondSnapshot, stepId2, 'failed');
608
646
 
609
647
  // Get second page
610
648
  const page2 = await store.getWorkflowRuns({ limit: 2, offset: 2 });
611
649
  expect(page2.runs).toHaveLength(1);
612
650
  expect(page2.total).toBe(3);
613
651
  expect(page2.runs[0]!.workflowName).toBe(workflowName1);
614
- const snapshot = page2.runs[0]!.snapshot as WorkflowRunState;
615
- expect(snapshot.context?.steps[stepId1]?.status).toBe('completed');
652
+ const snapshot = page2.runs[0]!.snapshot;
653
+ checkWorkflowSnapshot(snapshot, stepId1, 'success');
654
+ });
655
+ });
656
+
657
+ describe('getWorkflowRunById', () => {
658
+ const workflowName = 'workflow-id-test';
659
+ let runId: string;
660
+ let stepId: string;
661
+
662
+ beforeEach(async () => {
663
+ // Insert a workflow run for positive test
664
+ const sample = createSampleWorkflowSnapshot('success');
665
+ runId = sample.runId;
666
+ stepId = sample.stepId;
667
+ await store.insert({
668
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
669
+ record: {
670
+ workflow_name: workflowName,
671
+ run_id: runId,
672
+ resourceId: 'resource-abc',
673
+ snapshot: sample.snapshot,
674
+ createdAt: new Date(),
675
+ updatedAt: new Date(),
676
+ },
677
+ });
678
+ });
679
+
680
+ it('should retrieve a workflow run by ID', async () => {
681
+ const found = await store.getWorkflowRunById({
682
+ runId,
683
+ workflowName,
684
+ });
685
+ expect(found).not.toBeNull();
686
+ expect(found?.runId).toBe(runId);
687
+ checkWorkflowSnapshot(found?.snapshot!, stepId, 'success');
688
+ });
689
+
690
+ it('should return null for non-existent workflow run ID', async () => {
691
+ const notFound = await store.getWorkflowRunById({
692
+ runId: 'non-existent-id',
693
+ workflowName,
694
+ });
695
+ expect(notFound).toBeNull();
696
+ });
697
+ });
698
+ describe('getWorkflowRuns with resourceId', () => {
699
+ const workflowName = 'workflow-id-test';
700
+ let resourceId: string;
701
+ let runIds: string[] = [];
702
+
703
+ beforeEach(async () => {
704
+ // Insert multiple workflow runs for the same resourceId
705
+ resourceId = 'resource-shared';
706
+ for (const status of ['success', 'failed']) {
707
+ const sample = createSampleWorkflowSnapshot(status as WorkflowRunState['context']['steps'][string]['status']);
708
+ runIds.push(sample.runId);
709
+ await store.insert({
710
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
711
+ record: {
712
+ workflow_name: workflowName,
713
+ run_id: sample.runId,
714
+ resourceId,
715
+ snapshot: sample.snapshot,
716
+ createdAt: new Date(),
717
+ updatedAt: new Date(),
718
+ },
719
+ });
720
+ }
721
+ // Insert a run with a different resourceId
722
+ const other = createSampleWorkflowSnapshot('waiting');
723
+ await store.insert({
724
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
725
+ record: {
726
+ workflow_name: workflowName,
727
+ run_id: other.runId,
728
+ resourceId: 'resource-other',
729
+ snapshot: other.snapshot,
730
+ createdAt: new Date(),
731
+ updatedAt: new Date(),
732
+ },
733
+ });
734
+ });
735
+
736
+ it('should retrieve all workflow runs by resourceId', async () => {
737
+ const { runs } = await store.getWorkflowRuns({
738
+ resourceId,
739
+ workflowName,
740
+ });
741
+ expect(Array.isArray(runs)).toBe(true);
742
+ expect(runs.length).toBeGreaterThanOrEqual(2);
743
+ for (const run of runs) {
744
+ expect(run.resourceId).toBe(resourceId);
745
+ }
746
+ });
747
+
748
+ it('should return an empty array if no workflow runs match resourceId', async () => {
749
+ const { runs } = await store.getWorkflowRuns({
750
+ resourceId: 'non-existent-resource',
751
+ workflowName,
752
+ });
753
+ expect(Array.isArray(runs)).toBe(true);
754
+ expect(runs.length).toBe(0);
616
755
  });
617
756
  });
618
757
 
@@ -699,6 +838,38 @@ describe('PostgresStore', () => {
699
838
  });
700
839
  });
701
840
 
841
+ describe('hasColumn', () => {
842
+ const tempTable = 'temp_test_table';
843
+
844
+ beforeEach(async () => {
845
+ // Always try to drop the table before each test, ignore errors if it doesn't exist
846
+ try {
847
+ await store['db'].query(`DROP TABLE IF EXISTS ${tempTable}`);
848
+ } catch {
849
+ /* ignore */
850
+ }
851
+ });
852
+
853
+ it('returns true if the column exists', async () => {
854
+ await store['db'].query(`CREATE TABLE ${tempTable} (id SERIAL PRIMARY KEY, resourceId TEXT)`);
855
+ expect(await store['hasColumn'](tempTable, 'resourceId')).toBe(true);
856
+ });
857
+
858
+ it('returns false if the column does not exist', async () => {
859
+ await store['db'].query(`CREATE TABLE ${tempTable} (id SERIAL PRIMARY KEY)`);
860
+ expect(await store['hasColumn'](tempTable, 'resourceId')).toBe(false);
861
+ });
862
+
863
+ afterEach(async () => {
864
+ // Always try to drop the table after each test, ignore errors if it doesn't exist
865
+ try {
866
+ await store['db'].query(`DROP TABLE IF EXISTS ${tempTable}`);
867
+ } catch {
868
+ /* ignore */
869
+ }
870
+ });
871
+ });
872
+
702
873
  describe('Schema Support', () => {
703
874
  const customSchema = 'mastra_test';
704
875
  let customSchemaStore: PostgresStore;