@mastra/dynamodb 0.10.1 → 0.10.3

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.
@@ -70,6 +70,11 @@ declare class DynamoDBStore extends MastraStorage {
70
70
  * Handles resetting the stored promise on failure to allow retries.
71
71
  */
72
72
  private _performInitializationAndStore;
73
+ /**
74
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
75
+ * This is necessary because ElectroDB validation happens before setters are applied
76
+ */
77
+ private preprocessRecord;
73
78
  /**
74
79
  * Clear all items from a logical "table" (entity type)
75
80
  */
@@ -70,6 +70,11 @@ declare class DynamoDBStore extends MastraStorage {
70
70
  * Handles resetting the stored promise on failure to allow retries.
71
71
  */
72
72
  private _performInitializationAndStore;
73
+ /**
74
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
75
+ * This is necessary because ElectroDB validation happens before setters are applied
76
+ */
77
+ private preprocessRecord;
73
78
  /**
74
79
  * Clear all items from a logical "table" (entity type)
75
80
  */
package/dist/index.cjs CHANGED
@@ -659,6 +659,23 @@ var DynamoDBStore = class extends storage.MastraStorage {
659
659
  throw err;
660
660
  });
661
661
  }
662
+ /**
663
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
664
+ * This is necessary because ElectroDB validation happens before setters are applied
665
+ */
666
+ preprocessRecord(record) {
667
+ const processed = { ...record };
668
+ if (processed.createdAt instanceof Date) {
669
+ processed.createdAt = processed.createdAt.toISOString();
670
+ }
671
+ if (processed.updatedAt instanceof Date) {
672
+ processed.updatedAt = processed.updatedAt.toISOString();
673
+ }
674
+ if (processed.created_at instanceof Date) {
675
+ processed.created_at = processed.created_at.toISOString();
676
+ }
677
+ return processed;
678
+ }
662
679
  /**
663
680
  * Clear all items from a logical "table" (entity type)
664
681
  */
@@ -728,7 +745,7 @@ var DynamoDBStore = class extends storage.MastraStorage {
728
745
  throw new Error(`No entity defined for ${tableName}`);
729
746
  }
730
747
  try {
731
- const dataToSave = { entity: entityName, ...record };
748
+ const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
732
749
  await this.service.entities[entityName].create(dataToSave).go();
733
750
  } catch (error) {
734
751
  this.logger.error("Failed to insert record", { tableName, error });
@@ -744,7 +761,7 @@ var DynamoDBStore = class extends storage.MastraStorage {
744
761
  if (!entityName || !this.service.entities[entityName]) {
745
762
  throw new Error(`No entity defined for ${tableName}`);
746
763
  }
747
- const recordsToSave = records.map((rec) => ({ entity: entityName, ...rec }));
764
+ const recordsToSave = records.map((rec) => ({ entity: entityName, ...this.preprocessRecord(rec) }));
748
765
  const batchSize = 25;
749
766
  const batches = [];
750
767
  for (let i = 0; i < recordsToSave.length; i += batchSize) {
@@ -904,7 +921,7 @@ var DynamoDBStore = class extends storage.MastraStorage {
904
921
  try {
905
922
  const query = this.service.entities.message.query.byThread({ entity: "message", threadId });
906
923
  if (selectBy?.last && typeof selectBy.last === "number") {
907
- const results2 = await query.go({ limit: selectBy.last, reverse: true });
924
+ const results2 = await query.go({ limit: selectBy.last, order: "desc" });
908
925
  const list2 = new agent.MessageList({ threadId, resourceId }).add(
909
926
  results2.data.map((data) => this.parseMessageData(data)),
910
927
  "memory"
package/dist/index.js CHANGED
@@ -657,6 +657,23 @@ var DynamoDBStore = class extends MastraStorage {
657
657
  throw err;
658
658
  });
659
659
  }
660
+ /**
661
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
662
+ * This is necessary because ElectroDB validation happens before setters are applied
663
+ */
664
+ preprocessRecord(record) {
665
+ const processed = { ...record };
666
+ if (processed.createdAt instanceof Date) {
667
+ processed.createdAt = processed.createdAt.toISOString();
668
+ }
669
+ if (processed.updatedAt instanceof Date) {
670
+ processed.updatedAt = processed.updatedAt.toISOString();
671
+ }
672
+ if (processed.created_at instanceof Date) {
673
+ processed.created_at = processed.created_at.toISOString();
674
+ }
675
+ return processed;
676
+ }
660
677
  /**
661
678
  * Clear all items from a logical "table" (entity type)
662
679
  */
@@ -726,7 +743,7 @@ var DynamoDBStore = class extends MastraStorage {
726
743
  throw new Error(`No entity defined for ${tableName}`);
727
744
  }
728
745
  try {
729
- const dataToSave = { entity: entityName, ...record };
746
+ const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
730
747
  await this.service.entities[entityName].create(dataToSave).go();
731
748
  } catch (error) {
732
749
  this.logger.error("Failed to insert record", { tableName, error });
@@ -742,7 +759,7 @@ var DynamoDBStore = class extends MastraStorage {
742
759
  if (!entityName || !this.service.entities[entityName]) {
743
760
  throw new Error(`No entity defined for ${tableName}`);
744
761
  }
745
- const recordsToSave = records.map((rec) => ({ entity: entityName, ...rec }));
762
+ const recordsToSave = records.map((rec) => ({ entity: entityName, ...this.preprocessRecord(rec) }));
746
763
  const batchSize = 25;
747
764
  const batches = [];
748
765
  for (let i = 0; i < recordsToSave.length; i += batchSize) {
@@ -902,7 +919,7 @@ var DynamoDBStore = class extends MastraStorage {
902
919
  try {
903
920
  const query = this.service.entities.message.query.byThread({ entity: "message", threadId });
904
921
  if (selectBy?.last && typeof selectBy.last === "number") {
905
- const results2 = await query.go({ limit: selectBy.last, reverse: true });
922
+ const results2 = await query.go({ limit: selectBy.last, order: "desc" });
906
923
  const list2 = new MessageList({ threadId, resourceId }).add(
907
924
  results2.data.map((data) => this.parseMessageData(data)),
908
925
  "memory"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/dynamodb",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "DynamoDB storage adapter for Mastra",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,8 +40,8 @@
40
40
  "tsup": "^8.4.0",
41
41
  "typescript": "^5.8.2",
42
42
  "vitest": "^3.0.9",
43
- "@internal/lint": "0.0.8",
44
- "@mastra/core": "0.10.2"
43
+ "@internal/lint": "0.0.10",
44
+ "@mastra/core": "0.10.3"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
@@ -387,6 +387,61 @@ describe('DynamoDBStore Integration Tests', () => {
387
387
  expect(retrieved?.title).toBe('Updated Thread 2');
388
388
  expect(retrieved?.metadata?.update).toBe(2);
389
389
  });
390
+
391
+ test('getMessages should return the N most recent messages [v2 storage]', async () => {
392
+ const threadId = 'last-selector-thread';
393
+ const start = Date.now();
394
+
395
+ // Insert 10 messages with increasing timestamps
396
+ const messages: MastraMessageV2[] = Array.from({ length: 10 }, (_, i) => ({
397
+ id: `m-${i}`,
398
+ threadId,
399
+ resourceId: 'r',
400
+ content: { format: 2, parts: [{ type: 'text', text: `msg-${i}` }] },
401
+ createdAt: new Date(start + i), // 0..9 ms apart
402
+ role: 'user',
403
+ type: 'text',
404
+ }));
405
+ await store.saveMessages({ messages, format: 'v2' });
406
+
407
+ const last3 = await store.getMessages({
408
+ format: 'v2',
409
+ threadId,
410
+ selectBy: { last: 3 },
411
+ });
412
+
413
+ expect(last3).toHaveLength(3);
414
+ expect(last3.map(m => (m.content.parts[0] as { type: string; text: string }).text)).toEqual([
415
+ 'msg-7',
416
+ 'msg-8',
417
+ 'msg-9',
418
+ ]);
419
+ });
420
+
421
+ test('getMessages should return the N most recent messages [v1 storage]', async () => {
422
+ const threadId = 'last-selector-thread';
423
+ const start = Date.now();
424
+
425
+ // Insert 10 messages with increasing timestamps
426
+ const messages: MastraMessageV1[] = Array.from({ length: 10 }, (_, i) => ({
427
+ id: `m-${i}`,
428
+ threadId,
429
+ resourceId: 'r',
430
+ content: `msg-${i}`,
431
+ createdAt: new Date(start + i), // 0..9 ms apart
432
+ role: 'user',
433
+ type: 'text',
434
+ }));
435
+ await store.saveMessages({ messages });
436
+
437
+ const last3 = await store.getMessages({
438
+ threadId,
439
+ selectBy: { last: 3 },
440
+ });
441
+
442
+ expect(last3).toHaveLength(3);
443
+ expect(last3.map(m => m.content)).toEqual(['msg-7', 'msg-8', 'msg-9']);
444
+ });
390
445
  });
391
446
 
392
447
  describe('Batch Operations', () => {
@@ -589,6 +644,37 @@ describe('DynamoDBStore Integration Tests', () => {
589
644
  expect(allTraces.length).toBe(3);
590
645
  });
591
646
 
647
+ test('should handle Date objects for createdAt/updatedAt fields in batchTraceInsert', async () => {
648
+ // This test specifically verifies the bug from the issue where Date objects
649
+ // were passed instead of ISO strings and ElectroDB validation failed
650
+ const now = new Date();
651
+ const traceWithDateObjects = {
652
+ id: `trace-${randomUUID()}`,
653
+ parentSpanId: `span-${randomUUID()}`,
654
+ traceId: `traceid-${randomUUID()}`,
655
+ name: 'test-trace-with-dates',
656
+ scope: 'default-tracer',
657
+ kind: 1,
658
+ startTime: now.getTime(),
659
+ endTime: now.getTime() + 100,
660
+ status: JSON.stringify({ code: 0 }),
661
+ attributes: JSON.stringify({ key: 'value' }),
662
+ events: JSON.stringify([]),
663
+ links: JSON.stringify([]),
664
+ // These are Date objects, not ISO strings - this should be handled by ElectroDB attribute setters
665
+ createdAt: now,
666
+ updatedAt: now,
667
+ };
668
+
669
+ // This should not throw a validation error due to Date object type
670
+ await expect(store.batchTraceInsert({ records: [traceWithDateObjects] })).resolves.not.toThrow();
671
+
672
+ // Verify the trace was saved correctly
673
+ const allTraces = await store.getTraces({ name: 'test-trace-with-dates', page: 1, perPage: 10 });
674
+ expect(allTraces.length).toBe(1);
675
+ expect(allTraces[0].name).toBe('test-trace-with-dates');
676
+ });
677
+
592
678
  test('should retrieve traces filtered by name using GSI', async () => {
593
679
  const trace1 = sampleTrace('trace-filter-name', 'scope-X');
594
680
  const trace2 = sampleTrace('trace-filter-name', 'scope-Y', Date.now() + 10);
@@ -670,6 +756,40 @@ describe('DynamoDBStore Integration Tests', () => {
670
756
  };
671
757
  };
672
758
 
759
+ test('should handle Date objects for createdAt/updatedAt fields in eval batchInsert', async () => {
760
+ // Test that eval entity properly handles Date objects in createdAt/updatedAt fields
761
+ const now = new Date();
762
+ const evalWithDateObjects = {
763
+ entity: 'eval',
764
+ agent_name: 'test-agent-dates',
765
+ input: 'Test input',
766
+ output: 'Test output',
767
+ result: JSON.stringify({ score: 0.95 }),
768
+ metric_name: 'test-metric',
769
+ instructions: 'Test instructions',
770
+ global_run_id: `global-${randomUUID()}`,
771
+ run_id: `run-${randomUUID()}`,
772
+ created_at: now, // Date object instead of ISO string
773
+ // These are Date objects, not ISO strings - should be handled by ElectroDB attribute setters
774
+ createdAt: now,
775
+ updatedAt: now,
776
+ metadata: JSON.stringify({ test: 'meta' }),
777
+ };
778
+
779
+ // This should not throw a validation error due to Date object type
780
+ await expect(
781
+ store.batchInsert({
782
+ tableName: TABLE_EVALS,
783
+ records: [evalWithDateObjects],
784
+ }),
785
+ ).resolves.not.toThrow();
786
+
787
+ // Verify the eval was saved correctly
788
+ const evals = await store.getEvalsByAgentName('test-agent-dates');
789
+ expect(evals.length).toBe(1);
790
+ expect(evals[0].agentName).toBe('test-agent-dates');
791
+ });
792
+
673
793
  test('should retrieve evals by agent name using GSI and filter by type', async () => {
674
794
  const agent1 = 'eval-agent-1';
675
795
  const agent2 = 'eval-agent-2';
@@ -1012,6 +1132,32 @@ describe('DynamoDBStore Integration Tests', () => {
1012
1132
  }
1013
1133
  });
1014
1134
 
1135
+ test('insert() should handle Date objects for createdAt/updatedAt fields', async () => {
1136
+ // Test that individual insert method properly handles Date objects in date fields
1137
+ const now = new Date();
1138
+ const recordWithDates = {
1139
+ id: `thread-${randomUUID()}`,
1140
+ resourceId: `resource-${randomUUID()}`,
1141
+ title: 'Thread with Date Objects',
1142
+ // These are Date objects, not ISO strings - should be handled by preprocessing
1143
+ createdAt: now,
1144
+ updatedAt: now,
1145
+ metadata: JSON.stringify({ test: 'with-dates' }),
1146
+ };
1147
+
1148
+ // This should not throw a validation error due to Date object type
1149
+ await expect(genericStore.insert({ tableName: TABLE_THREADS, record: recordWithDates })).resolves.not.toThrow();
1150
+
1151
+ // Verify the record was saved correctly
1152
+ const loaded = await genericStore.load<StorageThreadType>({
1153
+ tableName: TABLE_THREADS,
1154
+ keys: { id: recordWithDates.id },
1155
+ });
1156
+ expect(loaded).not.toBeNull();
1157
+ expect(loaded?.id).toBe(recordWithDates.id);
1158
+ expect(loaded?.title).toBe('Thread with Date Objects');
1159
+ });
1160
+
1015
1161
  test('load() should return null for non-existent record', async () => {
1016
1162
  // Use the genericStore instance
1017
1163
  const loaded = await genericStore.load({ tableName: TABLE_THREADS, keys: { id: 'non-existent-generic' } });
@@ -180,6 +180,29 @@ export class DynamoDBStore extends MastraStorage {
180
180
  });
181
181
  }
182
182
 
183
+ /**
184
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
185
+ * This is necessary because ElectroDB validation happens before setters are applied
186
+ */
187
+ private preprocessRecord(record: Record<string, any>): Record<string, any> {
188
+ const processed = { ...record };
189
+
190
+ // Convert Date objects to ISO strings for date fields
191
+ // This prevents ElectroDB validation errors that occur when Date objects are passed
192
+ // to string-typed attributes, even when the attribute has a setter that converts dates
193
+ if (processed.createdAt instanceof Date) {
194
+ processed.createdAt = processed.createdAt.toISOString();
195
+ }
196
+ if (processed.updatedAt instanceof Date) {
197
+ processed.updatedAt = processed.updatedAt.toISOString();
198
+ }
199
+ if (processed.created_at instanceof Date) {
200
+ processed.created_at = processed.created_at.toISOString();
201
+ }
202
+
203
+ return processed;
204
+ }
205
+
183
206
  /**
184
207
  * Clear all items from a logical "table" (entity type)
185
208
  */
@@ -275,8 +298,8 @@ export class DynamoDBStore extends MastraStorage {
275
298
  }
276
299
 
277
300
  try {
278
- // Add the entity type to the record before creating
279
- const dataToSave = { entity: entityName, ...record };
301
+ // Add the entity type to the record and preprocess before creating
302
+ const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
280
303
  await this.service.entities[entityName].create(dataToSave).go();
281
304
  } catch (error) {
282
305
  this.logger.error('Failed to insert record', { tableName, error });
@@ -295,8 +318,8 @@ export class DynamoDBStore extends MastraStorage {
295
318
  throw new Error(`No entity defined for ${tableName}`);
296
319
  }
297
320
 
298
- // Add entity type to each record
299
- const recordsToSave = records.map(rec => ({ entity: entityName, ...rec }));
321
+ // Add entity type and preprocess each record
322
+ const recordsToSave = records.map(rec => ({ entity: entityName, ...this.preprocessRecord(rec) }));
300
323
 
301
324
  // ElectroDB has batch limits of 25 items, so we need to chunk
302
325
  const batchSize = 25;
@@ -532,10 +555,10 @@ export class DynamoDBStore extends MastraStorage {
532
555
 
533
556
  // Apply the 'last' limit if provided
534
557
  if (selectBy?.last && typeof selectBy.last === 'number') {
535
- // Use ElectroDB's limit parameter (descending sort assumed on GSI SK)
536
- // Ensure GSI sk (createdAt) is sorted descending for 'last' to work correctly
537
- // Assuming default sort is ascending on SK, use reverse: true for descending
538
- const results = await query.go({ limit: selectBy.last, reverse: true });
558
+ // Use ElectroDB's limit parameter
559
+ // DDB GSIs are sorted in ascending order
560
+ // Use ElectroDB's order parameter to sort in descending order to retrieve 'latest' messages
561
+ const results = await query.go({ limit: selectBy.last, order: 'desc' });
539
562
  // Use arrow function in map to preserve 'this' context for parseMessageData
540
563
  const list = new MessageList({ threadId, resourceId }).add(
541
564
  results.data.map((data: any) => this.parseMessageData(data)),