@mastra/dynamodb 0.10.2 → 0.10.4-alpha.0

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.
@@ -5,6 +5,7 @@ import type { MastraMessageV1 } from '@mastra/core';
5
5
  import type { MastraMessageV2 } from '@mastra/core';
6
6
  import { MastraStorage } from '@mastra/core/storage';
7
7
  import { Service } from 'electrodb';
8
+ import type { StorageColumn } from '@mastra/core';
8
9
  import type { StorageGetMessagesArg } from '@mastra/core/storage';
9
10
  import type { StorageThreadType } from '@mastra/core';
10
11
  import type { TABLE_NAMES } from '@mastra/core/storage';
@@ -70,6 +71,16 @@ declare class DynamoDBStore extends MastraStorage {
70
71
  * Handles resetting the stored promise on failure to allow retries.
71
72
  */
72
73
  private _performInitializationAndStore;
74
+ /**
75
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
76
+ * This is necessary because ElectroDB validation happens before setters are applied
77
+ */
78
+ private preprocessRecord;
79
+ alterTable(_args: {
80
+ tableName: TABLE_NAMES;
81
+ schema: Record<string, StorageColumn>;
82
+ ifNotExists: string[];
83
+ }): Promise<void>;
73
84
  /**
74
85
  * Clear all items from a logical "table" (entity type)
75
86
  */
@@ -5,6 +5,7 @@ import type { MastraMessageV1 } from '@mastra/core';
5
5
  import type { MastraMessageV2 } from '@mastra/core';
6
6
  import { MastraStorage } from '@mastra/core/storage';
7
7
  import { Service } from 'electrodb';
8
+ import type { StorageColumn } from '@mastra/core';
8
9
  import type { StorageGetMessagesArg } from '@mastra/core/storage';
9
10
  import type { StorageThreadType } from '@mastra/core';
10
11
  import type { TABLE_NAMES } from '@mastra/core/storage';
@@ -70,6 +71,16 @@ declare class DynamoDBStore extends MastraStorage {
70
71
  * Handles resetting the stored promise on failure to allow retries.
71
72
  */
72
73
  private _performInitializationAndStore;
74
+ /**
75
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
76
+ * This is necessary because ElectroDB validation happens before setters are applied
77
+ */
78
+ private preprocessRecord;
79
+ alterTable(_args: {
80
+ tableName: TABLE_NAMES;
81
+ schema: Record<string, StorageColumn>;
82
+ ifNotExists: string[];
83
+ }): Promise<void>;
73
84
  /**
74
85
  * Clear all items from a logical "table" (entity type)
75
86
  */
package/dist/index.cjs CHANGED
@@ -659,6 +659,25 @@ 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
+ }
679
+ async alterTable(_args) {
680
+ }
662
681
  /**
663
682
  * Clear all items from a logical "table" (entity type)
664
683
  */
@@ -728,7 +747,7 @@ var DynamoDBStore = class extends storage.MastraStorage {
728
747
  throw new Error(`No entity defined for ${tableName}`);
729
748
  }
730
749
  try {
731
- const dataToSave = { entity: entityName, ...record };
750
+ const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
732
751
  await this.service.entities[entityName].create(dataToSave).go();
733
752
  } catch (error) {
734
753
  this.logger.error("Failed to insert record", { tableName, error });
@@ -744,7 +763,7 @@ var DynamoDBStore = class extends storage.MastraStorage {
744
763
  if (!entityName || !this.service.entities[entityName]) {
745
764
  throw new Error(`No entity defined for ${tableName}`);
746
765
  }
747
- const recordsToSave = records.map((rec) => ({ entity: entityName, ...rec }));
766
+ const recordsToSave = records.map((rec) => ({ entity: entityName, ...this.preprocessRecord(rec) }));
748
767
  const batchSize = 25;
749
768
  const batches = [];
750
769
  for (let i = 0; i < recordsToSave.length; i += batchSize) {
package/dist/index.js CHANGED
@@ -657,6 +657,25 @@ 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
+ }
677
+ async alterTable(_args) {
678
+ }
660
679
  /**
661
680
  * Clear all items from a logical "table" (entity type)
662
681
  */
@@ -726,7 +745,7 @@ var DynamoDBStore = class extends MastraStorage {
726
745
  throw new Error(`No entity defined for ${tableName}`);
727
746
  }
728
747
  try {
729
- const dataToSave = { entity: entityName, ...record };
748
+ const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
730
749
  await this.service.entities[entityName].create(dataToSave).go();
731
750
  } catch (error) {
732
751
  this.logger.error("Failed to insert record", { tableName, error });
@@ -742,7 +761,7 @@ var DynamoDBStore = class extends MastraStorage {
742
761
  if (!entityName || !this.service.entities[entityName]) {
743
762
  throw new Error(`No entity defined for ${tableName}`);
744
763
  }
745
- const recordsToSave = records.map((rec) => ({ entity: entityName, ...rec }));
764
+ const recordsToSave = records.map((rec) => ({ entity: entityName, ...this.preprocessRecord(rec) }));
746
765
  const batchSize = 25;
747
766
  const batches = [];
748
767
  for (let i = 0; i < recordsToSave.length; i += batchSize) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/dynamodb",
3
- "version": "0.10.2",
3
+ "version": "0.10.4-alpha.0",
4
4
  "description": "DynamoDB storage adapter for Mastra",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,25 +23,26 @@
23
23
  "src"
24
24
  ],
25
25
  "dependencies": {
26
- "@aws-sdk/client-dynamodb": "^3.0.0",
27
- "@aws-sdk/lib-dynamodb": "^3.0.0",
26
+ "@aws-sdk/client-dynamodb": "^3.823.0",
27
+ "@aws-sdk/lib-dynamodb": "^3.823.0",
28
28
  "electrodb": "^3.4.1"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@mastra/core": "^0.10.2-alpha.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@microsoft/api-extractor": "^7.52.1",
35
- "@types/node": "^20.17.27",
36
- "@vitest/coverage-v8": "3.0.9",
37
- "@vitest/ui": "3.0.9",
38
- "axios": "^1.8.4",
39
- "eslint": "^9.23.0",
40
- "tsup": "^8.4.0",
34
+ "@microsoft/api-extractor": "^7.52.8",
35
+ "@types/node": "^20.17.57",
36
+ "@vitest/coverage-v8": "3.2.2",
37
+ "@vitest/ui": "3.2.2",
38
+ "axios": "^1.9.0",
39
+ "eslint": "^9.28.0",
40
+ "tsup": "^8.5.0",
41
41
  "typescript": "^5.8.2",
42
- "vitest": "^3.0.9",
43
- "@internal/lint": "0.0.9",
44
- "@mastra/core": "0.10.2"
42
+ "vitest": "^3.2.2",
43
+ "@internal/lint": "0.0.10",
44
+ "@internal/storage-test-utils": "0.0.6",
45
+ "@mastra/core": "0.10.4-alpha.1"
45
46
  },
46
47
  "scripts": {
47
48
  "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
@@ -644,6 +644,37 @@ describe('DynamoDBStore Integration Tests', () => {
644
644
  expect(allTraces.length).toBe(3);
645
645
  });
646
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
+
647
678
  test('should retrieve traces filtered by name using GSI', async () => {
648
679
  const trace1 = sampleTrace('trace-filter-name', 'scope-X');
649
680
  const trace2 = sampleTrace('trace-filter-name', 'scope-Y', Date.now() + 10);
@@ -725,6 +756,40 @@ describe('DynamoDBStore Integration Tests', () => {
725
756
  };
726
757
  };
727
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
+
728
793
  test('should retrieve evals by agent name using GSI and filter by type', async () => {
729
794
  const agent1 = 'eval-agent-1';
730
795
  const agent2 = 'eval-agent-2';
@@ -1067,6 +1132,32 @@ describe('DynamoDBStore Integration Tests', () => {
1067
1132
  }
1068
1133
  });
1069
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
+
1070
1161
  test('load() should return null for non-existent record', async () => {
1071
1162
  // Use the genericStore instance
1072
1163
  const loaded = await genericStore.load({ tableName: TABLE_THREADS, keys: { id: 'non-existent-generic' } });
@@ -1,6 +1,12 @@
1
1
  import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
2
2
  import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
3
- import type { StorageThreadType, WorkflowRunState, MastraMessageV1, MastraMessageV2 } from '@mastra/core';
3
+ import type {
4
+ StorageThreadType,
5
+ WorkflowRunState,
6
+ MastraMessageV1,
7
+ MastraMessageV2,
8
+ StorageColumn,
9
+ } from '@mastra/core';
4
10
  import { MessageList } from '@mastra/core/agent';
5
11
  import {
6
12
  MastraStorage,
@@ -180,6 +186,37 @@ export class DynamoDBStore extends MastraStorage {
180
186
  });
181
187
  }
182
188
 
189
+ /**
190
+ * Pre-processes a record to ensure Date objects are converted to ISO strings
191
+ * This is necessary because ElectroDB validation happens before setters are applied
192
+ */
193
+ private preprocessRecord(record: Record<string, any>): Record<string, any> {
194
+ const processed = { ...record };
195
+
196
+ // Convert Date objects to ISO strings for date fields
197
+ // This prevents ElectroDB validation errors that occur when Date objects are passed
198
+ // to string-typed attributes, even when the attribute has a setter that converts dates
199
+ if (processed.createdAt instanceof Date) {
200
+ processed.createdAt = processed.createdAt.toISOString();
201
+ }
202
+ if (processed.updatedAt instanceof Date) {
203
+ processed.updatedAt = processed.updatedAt.toISOString();
204
+ }
205
+ if (processed.created_at instanceof Date) {
206
+ processed.created_at = processed.created_at.toISOString();
207
+ }
208
+
209
+ return processed;
210
+ }
211
+
212
+ async alterTable(_args: {
213
+ tableName: TABLE_NAMES;
214
+ schema: Record<string, StorageColumn>;
215
+ ifNotExists: string[];
216
+ }): Promise<void> {
217
+ // Nothing to do here, DynamoDB has a flexible schema and handles new attributes automatically upon insertion/update.
218
+ }
219
+
183
220
  /**
184
221
  * Clear all items from a logical "table" (entity type)
185
222
  */
@@ -275,8 +312,8 @@ export class DynamoDBStore extends MastraStorage {
275
312
  }
276
313
 
277
314
  try {
278
- // Add the entity type to the record before creating
279
- const dataToSave = { entity: entityName, ...record };
315
+ // Add the entity type to the record and preprocess before creating
316
+ const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
280
317
  await this.service.entities[entityName].create(dataToSave).go();
281
318
  } catch (error) {
282
319
  this.logger.error('Failed to insert record', { tableName, error });
@@ -295,8 +332,8 @@ export class DynamoDBStore extends MastraStorage {
295
332
  throw new Error(`No entity defined for ${tableName}`);
296
333
  }
297
334
 
298
- // Add entity type to each record
299
- const recordsToSave = records.map(rec => ({ entity: entityName, ...rec }));
335
+ // Add entity type and preprocess each record
336
+ const recordsToSave = records.map(rec => ({ entity: entityName, ...this.preprocessRecord(rec) }));
300
337
 
301
338
  // ElectroDB has batch limits of 25 items, so we need to chunk
302
339
  const batchSize = 25;