@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.
- package/dist/_tsup-dts-rollup.d.cts +11 -0
- package/dist/_tsup-dts-rollup.d.ts +11 -0
- package/dist/index.cjs +21 -2
- package/dist/index.js +21 -2
- package/package.json +14 -13
- package/src/storage/index.test.ts +91 -0
- package/src/storage/index.ts +42 -5
|
@@ -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.
|
|
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.
|
|
27
|
-
"@aws-sdk/lib-dynamodb": "^3.
|
|
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.
|
|
35
|
-
"@types/node": "^20.17.
|
|
36
|
-
"@vitest/coverage-v8": "3.
|
|
37
|
-
"@vitest/ui": "3.
|
|
38
|
-
"axios": "^1.
|
|
39
|
-
"eslint": "^9.
|
|
40
|
-
"tsup": "^8.
|
|
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.
|
|
43
|
-
"@internal/lint": "0.0.
|
|
44
|
-
"@
|
|
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' } });
|
package/src/storage/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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;
|