@mastra/dynamodb 0.0.0-taofeeqInngest-20250603090617 → 0.0.0-tool-call-parts-20250630193309

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-taofeeqInngest-20250603090617",
3
+ "version": "0.0.0-tool-call-parts-20250630193309",
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-alpha.0"
31
+ "@mastra/core": "0.0.0-tool-call-parts-20250630193309"
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-taofeeqInngest-20250603090617",
31
- "@mastra/core": "0.0.0-taofeeqInngest-20250603090617"
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.3",
43
+ "@internal/lint": "0.0.0-tool-call-parts-20250630193309",
44
+ "@mastra/core": "0.0.0-tool-call-parts-20250630193309",
45
+ "@internal/storage-test-utils": "0.0.11"
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';
@@ -325,6 +326,7 @@ describe('DynamoDBStore Integration Tests', () => {
325
326
  suspendedPaths: { test: [1] },
326
327
  runId: 'test-run-large', // Use unique runId
327
328
  timestamp: now,
329
+ status: 'success',
328
330
  };
329
331
 
330
332
  await expect(
@@ -387,6 +389,98 @@ describe('DynamoDBStore Integration Tests', () => {
387
389
  expect(retrieved?.title).toBe('Updated Thread 2');
388
390
  expect(retrieved?.metadata?.update).toBe(2);
389
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
+ });
390
484
  });
391
485
 
392
486
  describe('Batch Operations', () => {
@@ -462,6 +556,82 @@ describe('DynamoDBStore Integration Tests', () => {
462
556
  expect(retrieved[0]?.content).toBe('Large Message 0');
463
557
  expect(retrieved[29]?.content).toBe('Large Message 29');
464
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
+ });
465
635
  });
466
636
 
467
637
  describe('Single-Table Design', () => {
@@ -492,6 +662,7 @@ describe('DynamoDBStore Integration Tests', () => {
492
662
  suspendedPaths: { test: [1] },
493
663
  runId: 'mixed-run',
494
664
  timestamp: Date.now(),
665
+ status: 'success',
495
666
  };
496
667
  await store.persistWorkflowSnapshot({ workflowName, runId: 'mixed-run', snapshot: workflowSnapshot });
497
668
 
@@ -589,6 +760,37 @@ describe('DynamoDBStore Integration Tests', () => {
589
760
  expect(allTraces.length).toBe(3);
590
761
  });
591
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
+
592
794
  test('should retrieve traces filtered by name using GSI', async () => {
593
795
  const trace1 = sampleTrace('trace-filter-name', 'scope-X');
594
796
  const trace2 = sampleTrace('trace-filter-name', 'scope-Y', Date.now() + 10);
@@ -670,6 +872,40 @@ describe('DynamoDBStore Integration Tests', () => {
670
872
  };
671
873
  };
672
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
+
673
909
  test('should retrieve evals by agent name using GSI and filter by type', async () => {
674
910
  const agent1 = 'eval-agent-1';
675
911
  const agent2 = 'eval-agent-2';
@@ -741,6 +977,7 @@ describe('DynamoDBStore Integration Tests', () => {
741
977
  suspendedPaths: {},
742
978
  runId: runId,
743
979
  timestamp: createdAt.getTime(),
980
+ status: 'success',
744
981
  ...(resourceId && { resourceId: resourceId }), // Conditionally add resourceId to snapshot
745
982
  };
746
983
  return {
@@ -781,6 +1018,53 @@ describe('DynamoDBStore Integration Tests', () => {
781
1018
  expect(loadedSnapshot?.context).toEqual(snapshot.context);
782
1019
  });
783
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
+
784
1068
  test('getWorkflowRunById should retrieve correct run', async () => {
785
1069
  const wfName = 'get-by-id-wf';
786
1070
  const runId1 = `run-${randomUUID()}`;
@@ -1012,6 +1296,32 @@ describe('DynamoDBStore Integration Tests', () => {
1012
1296
  }
1013
1297
  });
1014
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
+
1015
1325
  test('load() should return null for non-existent record', async () => {
1016
1326
  // Use the genericStore instance
1017
1327
  const loaded = await genericStore.load({ tableName: TABLE_THREADS, keys: { id: 'non-existent-generic' } });
@@ -1053,4 +1363,4 @@ describe('DynamoDBStore Integration Tests', () => {
1053
1363
  ).toBeNull();
1054
1364
  });
1055
1365
  }); // End Generic Storage Methods describe
1056
- }); // End Main Describe
1366
+ });