@mastra/dynamodb 0.0.0-pass-headers-for-create-mastra-client-20250529200245 → 0.0.0-support-d1-client-20250701191943

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/package.json CHANGED
@@ -1,37 +1,51 @@
1
1
  {
2
2
  "name": "@mastra/dynamodb",
3
- "version": "0.0.0-pass-headers-for-create-mastra-client-20250529200245",
3
+ "version": "0.0.0-support-d1-client-20250701191943",
4
4
  "description": "DynamoDB storage adapter for Mastra",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
8
21
  "files": [
9
22
  "dist",
10
23
  "src"
11
24
  ],
12
25
  "dependencies": {
13
- "@aws-sdk/client-dynamodb": "^3.0.0",
14
- "@aws-sdk/lib-dynamodb": "^3.0.0",
15
- "electrodb": "^3.4.1"
26
+ "@aws-sdk/client-dynamodb": "^3.828.0",
27
+ "@aws-sdk/lib-dynamodb": "^3.830.0",
28
+ "electrodb": "^3.4.3"
16
29
  },
17
30
  "peerDependencies": {
18
- "@mastra/core": "^0.10.0"
31
+ "@mastra/core": "0.0.0-support-d1-client-20250701191943"
19
32
  },
20
33
  "devDependencies": {
21
- "@microsoft/api-extractor": "^7.52.1",
22
- "@types/node": "^20.17.27",
23
- "@vitest/coverage-v8": "3.0.9",
24
- "@vitest/ui": "3.0.9",
25
- "axios": "^1.8.4",
26
- "eslint": "^9.23.0",
27
- "tsup": "^8.4.0",
28
- "typescript": "^5.8.2",
29
- "vitest": "^3.0.9",
30
- "@internal/lint": "0.0.0-pass-headers-for-create-mastra-client-20250529200245",
31
- "@mastra/core": "0.0.0-pass-headers-for-create-mastra-client-20250529200245"
34
+ "@microsoft/api-extractor": "^7.52.8",
35
+ "@types/node": "^20.19.0",
36
+ "@vitest/coverage-v8": "3.2.3",
37
+ "@vitest/ui": "3.2.3",
38
+ "axios": "^1.10.0",
39
+ "eslint": "^9.29.0",
40
+ "tsup": "^8.5.0",
41
+ "typescript": "^5.8.3",
42
+ "vitest": "^3.2.4",
43
+ "@internal/storage-test-utils": "0.0.11",
44
+ "@mastra/core": "0.0.0-support-d1-client-20250701191943",
45
+ "@internal/lint": "0.0.0-support-d1-client-20250701191943"
32
46
  },
33
47
  "scripts": {
34
- "build": "tsup src/index.ts --format esm,cjs --clean --treeshake=smallest --splitting",
48
+ "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
35
49
  "dev": "tsup --watch",
36
50
  "clean": "rm -rf dist",
37
51
  "lint": "eslint .",
@@ -11,6 +11,7 @@ import {
11
11
  waitUntilTableExists,
12
12
  waitUntilTableNotExists,
13
13
  } from '@aws-sdk/client-dynamodb';
14
+ import { createSampleMessageV2, createSampleThread } from '@internal/storage-test-utils';
14
15
  import type { MastraMessageV1, StorageThreadType, WorkflowRun, WorkflowRunState } from '@mastra/core';
15
16
  import type { MastraMessageV2 } from '@mastra/core/agent';
16
17
  import { TABLE_EVALS, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
@@ -320,10 +321,12 @@ describe('DynamoDBStore Integration Tests', () => {
320
321
  input: { source: 'test' },
321
322
  step1: { status: 'success', output: { data: 'test' } },
322
323
  } as unknown as WorkflowRunState['context'],
324
+ serializedStepGraph: [],
323
325
  activePaths: [{ stepPath: ['test'], stepId: 'step1', status: 'success' }],
324
326
  suspendedPaths: { test: [1] },
325
327
  runId: 'test-run-large', // Use unique runId
326
328
  timestamp: now,
329
+ status: 'success',
327
330
  };
328
331
 
329
332
  await expect(
@@ -386,6 +389,98 @@ describe('DynamoDBStore Integration Tests', () => {
386
389
  expect(retrieved?.title).toBe('Updated Thread 2');
387
390
  expect(retrieved?.metadata?.update).toBe(2);
388
391
  });
392
+
393
+ test('getMessages should return the N most recent messages [v2 storage]', async () => {
394
+ const threadId = 'last-selector-thread';
395
+ const start = Date.now();
396
+
397
+ // Insert 10 messages with increasing timestamps
398
+ const messages: MastraMessageV2[] = Array.from({ length: 10 }, (_, i) => ({
399
+ id: `m-${i}`,
400
+ threadId,
401
+ resourceId: 'r',
402
+ content: { format: 2, parts: [{ type: 'text', text: `msg-${i}` }] },
403
+ createdAt: new Date(start + i), // 0..9 ms apart
404
+ role: 'user',
405
+ type: 'text',
406
+ }));
407
+ await store.saveMessages({ messages, format: 'v2' });
408
+
409
+ const last3 = await store.getMessages({
410
+ format: 'v2',
411
+ threadId,
412
+ selectBy: { last: 3 },
413
+ });
414
+
415
+ expect(last3).toHaveLength(3);
416
+ expect(last3.map(m => (m.content.parts[0] as { type: string; text: string }).text)).toEqual([
417
+ 'msg-7',
418
+ 'msg-8',
419
+ 'msg-9',
420
+ ]);
421
+ });
422
+
423
+ test('getMessages should return the N most recent messages [v1 storage]', async () => {
424
+ const threadId = 'last-selector-thread';
425
+ const start = Date.now();
426
+
427
+ // Insert 10 messages with increasing timestamps
428
+ const messages: MastraMessageV1[] = Array.from({ length: 10 }, (_, i) => ({
429
+ id: `m-${i}`,
430
+ threadId,
431
+ resourceId: 'r',
432
+ content: `msg-${i}`,
433
+ createdAt: new Date(start + i), // 0..9 ms apart
434
+ role: 'user',
435
+ type: 'text',
436
+ }));
437
+ await store.saveMessages({ messages });
438
+
439
+ const last3 = await store.getMessages({
440
+ threadId,
441
+ selectBy: { last: 3 },
442
+ });
443
+
444
+ expect(last3).toHaveLength(3);
445
+ expect(last3.map(m => m.content)).toEqual(['msg-7', 'msg-8', 'msg-9']);
446
+ });
447
+
448
+ test('should update thread updatedAt when a message is saved to it', async () => {
449
+ const thread: StorageThreadType = {
450
+ id: 'thread-update-test',
451
+ resourceId: 'resource-update',
452
+ title: 'Update Test Thread',
453
+ createdAt: new Date(),
454
+ updatedAt: new Date(),
455
+ metadata: { test: true },
456
+ };
457
+ await store.saveThread({ thread });
458
+
459
+ // Get the initial thread to capture the original updatedAt
460
+ const initialThread = await store.getThreadById({ threadId: thread.id });
461
+ expect(initialThread).toBeDefined();
462
+ const originalUpdatedAt = initialThread!.updatedAt;
463
+
464
+ // Wait a small amount to ensure different timestamp
465
+ await new Promise(resolve => setTimeout(resolve, 100));
466
+
467
+ // Create and save a message to the thread
468
+ const message: MastraMessageV1 = {
469
+ id: 'msg-update-test',
470
+ threadId: thread.id,
471
+ resourceId: 'resource-update',
472
+ content: 'Test message for update',
473
+ createdAt: new Date(),
474
+ role: 'user',
475
+ type: 'text',
476
+ };
477
+ await store.saveMessages({ messages: [message] });
478
+
479
+ // Retrieve the thread again and check that updatedAt was updated
480
+ const updatedThread = await store.getThreadById({ threadId: thread.id });
481
+ expect(updatedThread).toBeDefined();
482
+ expect(updatedThread!.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime());
483
+ });
389
484
  });
390
485
 
391
486
  describe('Batch Operations', () => {
@@ -461,6 +556,82 @@ describe('DynamoDBStore Integration Tests', () => {
461
556
  expect(retrieved[0]?.content).toBe('Large Message 0');
462
557
  expect(retrieved[29]?.content).toBe('Large Message 29');
463
558
  });
559
+
560
+ test('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
561
+ const thread = await createSampleThread();
562
+ await store.saveThread({ thread });
563
+ const baseMessage = createSampleMessageV2({
564
+ threadId: thread.id,
565
+ createdAt: new Date(),
566
+ content: { content: 'Original' },
567
+ resourceId: thread.resourceId,
568
+ });
569
+
570
+ // Insert the message for the first time
571
+ await store.saveMessages({ messages: [baseMessage], format: 'v2' });
572
+
573
+ // // Insert again with the same id and threadId but different content
574
+ const updatedMessage = {
575
+ ...createSampleMessageV2({
576
+ threadId: thread.id,
577
+ createdAt: new Date(),
578
+ content: { content: 'Updated' },
579
+ resourceId: thread.resourceId,
580
+ }),
581
+ id: baseMessage.id,
582
+ };
583
+
584
+ await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
585
+
586
+ // Retrieve messages for the thread
587
+ const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
588
+
589
+ // Only one message should exist for that id+threadId
590
+ expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
591
+
592
+ // The content should be the updated one
593
+ expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
594
+ });
595
+
596
+ test('should upsert messages: duplicate id and different threadid', async () => {
597
+ const thread1 = await createSampleThread();
598
+ const thread2 = await createSampleThread();
599
+ await store.saveThread({ thread: thread1 });
600
+ await store.saveThread({ thread: thread2 });
601
+
602
+ const message = createSampleMessageV2({
603
+ threadId: thread1.id,
604
+ createdAt: new Date(),
605
+ content: { content: 'Thread1 Content' },
606
+ resourceId: thread1.resourceId,
607
+ });
608
+
609
+ // Insert message into thread1
610
+ await store.saveMessages({ messages: [message], format: 'v2' });
611
+
612
+ // Attempt to insert a message with the same id but different threadId
613
+ const conflictingMessage = {
614
+ ...createSampleMessageV2({
615
+ threadId: thread2.id, // different thread
616
+ content: { content: 'Thread2 Content' },
617
+ resourceId: thread2.resourceId,
618
+ }),
619
+ id: message.id,
620
+ };
621
+
622
+ // Save should save the message to the new thread
623
+ await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
624
+
625
+ // Retrieve messages for both threads
626
+ const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
627
+ const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
628
+
629
+ // Thread 1 should NOT have the message with that id
630
+ expect(thread1Messages.find(m => m.id === message.id)).toBeUndefined();
631
+
632
+ // Thread 2 should have the message with that id
633
+ expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
634
+ });
464
635
  });
465
636
 
466
637
  describe('Single-Table Design', () => {
@@ -486,10 +657,12 @@ describe('DynamoDBStore Integration Tests', () => {
486
657
  step1: { status: 'success', output: { data: 'test' } },
487
658
  input: { source: 'test' },
488
659
  } as unknown as WorkflowRunState['context'],
660
+ serializedStepGraph: [],
489
661
  activePaths: [{ stepPath: ['test'], stepId: 'step1', status: 'success' }],
490
662
  suspendedPaths: { test: [1] },
491
663
  runId: 'mixed-run',
492
664
  timestamp: Date.now(),
665
+ status: 'success',
493
666
  };
494
667
  await store.persistWorkflowSnapshot({ workflowName, runId: 'mixed-run', snapshot: workflowSnapshot });
495
668
 
@@ -587,6 +760,37 @@ describe('DynamoDBStore Integration Tests', () => {
587
760
  expect(allTraces.length).toBe(3);
588
761
  });
589
762
 
763
+ test('should handle Date objects for createdAt/updatedAt fields in batchTraceInsert', async () => {
764
+ // This test specifically verifies the bug from the issue where Date objects
765
+ // were passed instead of ISO strings and ElectroDB validation failed
766
+ const now = new Date();
767
+ const traceWithDateObjects = {
768
+ id: `trace-${randomUUID()}`,
769
+ parentSpanId: `span-${randomUUID()}`,
770
+ traceId: `traceid-${randomUUID()}`,
771
+ name: 'test-trace-with-dates',
772
+ scope: 'default-tracer',
773
+ kind: 1,
774
+ startTime: now.getTime(),
775
+ endTime: now.getTime() + 100,
776
+ status: JSON.stringify({ code: 0 }),
777
+ attributes: JSON.stringify({ key: 'value' }),
778
+ events: JSON.stringify([]),
779
+ links: JSON.stringify([]),
780
+ // These are Date objects, not ISO strings - this should be handled by ElectroDB attribute setters
781
+ createdAt: now,
782
+ updatedAt: now,
783
+ };
784
+
785
+ // This should not throw a validation error due to Date object type
786
+ await expect(store.batchTraceInsert({ records: [traceWithDateObjects] })).resolves.not.toThrow();
787
+
788
+ // Verify the trace was saved correctly
789
+ const allTraces = await store.getTraces({ name: 'test-trace-with-dates', page: 1, perPage: 10 });
790
+ expect(allTraces.length).toBe(1);
791
+ expect(allTraces[0].name).toBe('test-trace-with-dates');
792
+ });
793
+
590
794
  test('should retrieve traces filtered by name using GSI', async () => {
591
795
  const trace1 = sampleTrace('trace-filter-name', 'scope-X');
592
796
  const trace2 = sampleTrace('trace-filter-name', 'scope-Y', Date.now() + 10);
@@ -668,6 +872,40 @@ describe('DynamoDBStore Integration Tests', () => {
668
872
  };
669
873
  };
670
874
 
875
+ test('should handle Date objects for createdAt/updatedAt fields in eval batchInsert', async () => {
876
+ // Test that eval entity properly handles Date objects in createdAt/updatedAt fields
877
+ const now = new Date();
878
+ const evalWithDateObjects = {
879
+ entity: 'eval',
880
+ agent_name: 'test-agent-dates',
881
+ input: 'Test input',
882
+ output: 'Test output',
883
+ result: JSON.stringify({ score: 0.95 }),
884
+ metric_name: 'test-metric',
885
+ instructions: 'Test instructions',
886
+ global_run_id: `global-${randomUUID()}`,
887
+ run_id: `run-${randomUUID()}`,
888
+ created_at: now, // Date object instead of ISO string
889
+ // These are Date objects, not ISO strings - should be handled by ElectroDB attribute setters
890
+ createdAt: now,
891
+ updatedAt: now,
892
+ metadata: JSON.stringify({ test: 'meta' }),
893
+ };
894
+
895
+ // This should not throw a validation error due to Date object type
896
+ await expect(
897
+ store.batchInsert({
898
+ tableName: TABLE_EVALS,
899
+ records: [evalWithDateObjects],
900
+ }),
901
+ ).resolves.not.toThrow();
902
+
903
+ // Verify the eval was saved correctly
904
+ const evals = await store.getEvalsByAgentName('test-agent-dates');
905
+ expect(evals.length).toBe(1);
906
+ expect(evals[0].agentName).toBe('test-agent-dates');
907
+ });
908
+
671
909
  test('should retrieve evals by agent name using GSI and filter by type', async () => {
672
910
  const agent1 = 'eval-agent-1';
673
911
  const agent2 = 'eval-agent-2';
@@ -734,10 +972,12 @@ describe('DynamoDBStore Integration Tests', () => {
734
972
  step1: { status: 'success', output: { data: 'test' } },
735
973
  input: { source: 'test' },
736
974
  } as unknown as WorkflowRunState['context'],
975
+ serializedStepGraph: [],
737
976
  activePaths: [],
738
977
  suspendedPaths: {},
739
978
  runId: runId,
740
979
  timestamp: createdAt.getTime(),
980
+ status: 'success',
741
981
  ...(resourceId && { resourceId: resourceId }), // Conditionally add resourceId to snapshot
742
982
  };
743
983
  return {
@@ -778,6 +1018,53 @@ describe('DynamoDBStore Integration Tests', () => {
778
1018
  expect(loadedSnapshot?.context).toEqual(snapshot.context);
779
1019
  });
780
1020
 
1021
+ test('should allow updating an existing workflow snapshot', async () => {
1022
+ const wfName = 'update-test-wf';
1023
+ const runId = `run-${randomUUID()}`;
1024
+
1025
+ // Create initial snapshot
1026
+ const { snapshot: initialSnapshot } = sampleWorkflowSnapshot(wfName, runId);
1027
+
1028
+ await expect(
1029
+ store.persistWorkflowSnapshot({
1030
+ workflowName: wfName,
1031
+ runId: runId,
1032
+ snapshot: initialSnapshot,
1033
+ }),
1034
+ ).resolves.not.toThrow();
1035
+
1036
+ // Create updated snapshot with different data
1037
+ const updatedSnapshot: WorkflowRunState = {
1038
+ ...initialSnapshot,
1039
+ value: { currentState: 'completed' },
1040
+ context: {
1041
+ step1: { status: 'success', output: { data: 'updated-test' } },
1042
+ step2: { status: 'success', output: { data: 'new-step' } },
1043
+ input: { source: 'updated-test' },
1044
+ } as unknown as WorkflowRunState['context'],
1045
+ timestamp: Date.now(),
1046
+ };
1047
+
1048
+ // This should succeed (update existing snapshot)
1049
+ await expect(
1050
+ store.persistWorkflowSnapshot({
1051
+ workflowName: wfName,
1052
+ runId: runId,
1053
+ snapshot: updatedSnapshot,
1054
+ }),
1055
+ ).resolves.not.toThrow();
1056
+
1057
+ // Verify the snapshot was updated
1058
+ const loadedSnapshot = await store.loadWorkflowSnapshot({
1059
+ workflowName: wfName,
1060
+ runId: runId,
1061
+ });
1062
+
1063
+ expect(loadedSnapshot?.runId).toEqual(updatedSnapshot.runId);
1064
+ expect(loadedSnapshot?.value).toEqual(updatedSnapshot.value);
1065
+ expect(loadedSnapshot?.context).toEqual(updatedSnapshot.context);
1066
+ });
1067
+
781
1068
  test('getWorkflowRunById should retrieve correct run', async () => {
782
1069
  const wfName = 'get-by-id-wf';
783
1070
  const runId1 = `run-${randomUUID()}`;
@@ -1009,6 +1296,32 @@ describe('DynamoDBStore Integration Tests', () => {
1009
1296
  }
1010
1297
  });
1011
1298
 
1299
+ test('insert() should handle Date objects for createdAt/updatedAt fields', async () => {
1300
+ // Test that individual insert method properly handles Date objects in date fields
1301
+ const now = new Date();
1302
+ const recordWithDates = {
1303
+ id: `thread-${randomUUID()}`,
1304
+ resourceId: `resource-${randomUUID()}`,
1305
+ title: 'Thread with Date Objects',
1306
+ // These are Date objects, not ISO strings - should be handled by preprocessing
1307
+ createdAt: now,
1308
+ updatedAt: now,
1309
+ metadata: JSON.stringify({ test: 'with-dates' }),
1310
+ };
1311
+
1312
+ // This should not throw a validation error due to Date object type
1313
+ await expect(genericStore.insert({ tableName: TABLE_THREADS, record: recordWithDates })).resolves.not.toThrow();
1314
+
1315
+ // Verify the record was saved correctly
1316
+ const loaded = await genericStore.load<StorageThreadType>({
1317
+ tableName: TABLE_THREADS,
1318
+ keys: { id: recordWithDates.id },
1319
+ });
1320
+ expect(loaded).not.toBeNull();
1321
+ expect(loaded?.id).toBe(recordWithDates.id);
1322
+ expect(loaded?.title).toBe('Thread with Date Objects');
1323
+ });
1324
+
1012
1325
  test('load() should return null for non-existent record', async () => {
1013
1326
  // Use the genericStore instance
1014
1327
  const loaded = await genericStore.load({ tableName: TABLE_THREADS, keys: { id: 'non-existent-generic' } });
@@ -1050,4 +1363,4 @@ describe('DynamoDBStore Integration Tests', () => {
1050
1363
  ).toBeNull();
1051
1364
  });
1052
1365
  }); // End Generic Storage Methods describe
1053
- }); // End Main Describe
1366
+ });