@mastra/dynamodb 0.14.3 → 0.14.6-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.
@@ -1,433 +0,0 @@
1
- import { DescribeTableCommand } from '@aws-sdk/client-dynamodb';
2
- import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
3
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
- import {
5
- StoreOperations,
6
- TABLE_EVALS,
7
- TABLE_MESSAGES,
8
- TABLE_RESOURCES,
9
- TABLE_SCORERS,
10
- TABLE_THREADS,
11
- TABLE_TRACES,
12
- TABLE_WORKFLOW_SNAPSHOT,
13
- } from '@mastra/core/storage';
14
- import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
15
- import type { Service } from 'electrodb';
16
-
17
- export class StoreOperationsDynamoDB extends StoreOperations {
18
- client: DynamoDBDocumentClient;
19
- tableName: string;
20
- service: Service<Record<string, any>>;
21
- constructor({
22
- service,
23
- tableName,
24
- client,
25
- }: {
26
- service: Service<Record<string, any>>;
27
- tableName: string;
28
- client: DynamoDBDocumentClient;
29
- }) {
30
- super();
31
- this.service = service;
32
- this.client = client;
33
- this.tableName = tableName;
34
- }
35
-
36
- async hasColumn(): Promise<boolean> {
37
- return true;
38
- }
39
-
40
- async dropTable(): Promise<void> {}
41
-
42
- // Helper methods for entity/table mapping
43
- private getEntityNameForTable(tableName: TABLE_NAMES): string | null {
44
- const mapping: Record<TABLE_NAMES, string> = {
45
- [TABLE_THREADS]: 'thread',
46
- [TABLE_MESSAGES]: 'message',
47
- [TABLE_WORKFLOW_SNAPSHOT]: 'workflow_snapshot',
48
- [TABLE_EVALS]: 'eval',
49
- [TABLE_SCORERS]: 'score',
50
- [TABLE_TRACES]: 'trace',
51
- [TABLE_RESOURCES]: 'resource',
52
- };
53
- return mapping[tableName] || null;
54
- }
55
-
56
- /**
57
- * Pre-processes a record to ensure Date objects are converted to ISO strings
58
- * This is necessary because ElectroDB validation happens before setters are applied
59
- */
60
- private preprocessRecord(record: Record<string, any>): Record<string, any> {
61
- const processed = { ...record };
62
-
63
- // Convert Date objects to ISO strings for date fields
64
- // This prevents ElectroDB validation errors that occur when Date objects are passed
65
- // to string-typed attributes, even when the attribute has a setter that converts dates
66
- if (processed.createdAt instanceof Date) {
67
- processed.createdAt = processed.createdAt.toISOString();
68
- }
69
- if (processed.updatedAt instanceof Date) {
70
- processed.updatedAt = processed.updatedAt.toISOString();
71
- }
72
- if (processed.created_at instanceof Date) {
73
- processed.created_at = processed.created_at.toISOString();
74
- }
75
-
76
- // Convert result field to JSON string if it's an object
77
- if (processed.result && typeof processed.result === 'object') {
78
- processed.result = JSON.stringify(processed.result);
79
- }
80
-
81
- // Convert test_info field to JSON string if it's an object, or remove if undefined/null
82
- if (processed.test_info && typeof processed.test_info === 'object') {
83
- processed.test_info = JSON.stringify(processed.test_info);
84
- } else if (processed.test_info === undefined || processed.test_info === null) {
85
- delete processed.test_info;
86
- }
87
-
88
- // Convert snapshot field to JSON string if it's an object
89
- if (processed.snapshot && typeof processed.snapshot === 'object') {
90
- processed.snapshot = JSON.stringify(processed.snapshot);
91
- }
92
-
93
- // Convert trace-specific fields to JSON strings if they're objects
94
- // These fields have set/get functions in the entity but validation happens before set
95
- if (processed.attributes && typeof processed.attributes === 'object') {
96
- processed.attributes = JSON.stringify(processed.attributes);
97
- }
98
-
99
- if (processed.status && typeof processed.status === 'object') {
100
- processed.status = JSON.stringify(processed.status);
101
- }
102
-
103
- if (processed.events && typeof processed.events === 'object') {
104
- processed.events = JSON.stringify(processed.events);
105
- }
106
-
107
- if (processed.links && typeof processed.links === 'object') {
108
- processed.links = JSON.stringify(processed.links);
109
- }
110
-
111
- return processed;
112
- }
113
-
114
- /**
115
- * Validates that the required DynamoDB table exists and is accessible.
116
- * This does not check the table structure - it assumes the table
117
- * was created with the correct structure via CDK/CloudFormation.
118
- */
119
- private async validateTableExists(): Promise<boolean> {
120
- try {
121
- const command = new DescribeTableCommand({
122
- TableName: this.tableName,
123
- });
124
-
125
- // If the table exists, this call will succeed
126
- // If the table doesn't exist, it will throw a ResourceNotFoundException
127
- await this.client.send(command);
128
- return true;
129
- } catch (error: any) {
130
- // If the table doesn't exist, DynamoDB returns a ResourceNotFoundException
131
- if (error.name === 'ResourceNotFoundException') {
132
- return false;
133
- }
134
-
135
- // For other errors (like permissions issues), we should throw
136
- throw new MastraError(
137
- {
138
- id: 'STORAGE_DYNAMODB_STORE_VALIDATE_TABLE_EXISTS_FAILED',
139
- domain: ErrorDomain.STORAGE,
140
- category: ErrorCategory.THIRD_PARTY,
141
- details: { tableName: this.tableName },
142
- },
143
- error,
144
- );
145
- }
146
- }
147
-
148
- /**
149
- * This method is modified for DynamoDB with ElectroDB single-table design.
150
- * It assumes the table is created and managed externally via CDK/CloudFormation.
151
- *
152
- * This implementation only validates that the required table exists and is accessible.
153
- * No table creation is attempted - we simply check if we can access the table.
154
- */
155
- async createTable({ tableName }: { tableName: TABLE_NAMES; schema: Record<string, any> }): Promise<void> {
156
- this.logger.debug('Validating access to externally managed table', { tableName, physicalTable: this.tableName });
157
-
158
- // For single-table design, we just need to verify the table exists and is accessible
159
- try {
160
- const tableExists = await this.validateTableExists();
161
-
162
- if (!tableExists) {
163
- this.logger.error(
164
- `Table ${this.tableName} does not exist or is not accessible. It should be created via CDK/CloudFormation.`,
165
- );
166
- throw new Error(
167
- `Table ${this.tableName} does not exist or is not accessible. Ensure it's created via CDK/CloudFormation before using this store.`,
168
- );
169
- }
170
-
171
- this.logger.debug(`Table ${this.tableName} exists and is accessible`);
172
- } catch (error) {
173
- this.logger.error('Error validating table access', { tableName: this.tableName, error });
174
- throw new MastraError(
175
- {
176
- id: 'STORAGE_DYNAMODB_STORE_VALIDATE_TABLE_ACCESS_FAILED',
177
- domain: ErrorDomain.STORAGE,
178
- category: ErrorCategory.THIRD_PARTY,
179
- details: { tableName: this.tableName },
180
- },
181
- error,
182
- );
183
- }
184
- }
185
-
186
- async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
187
- this.logger.debug('DynamoDB insert called', { tableName });
188
-
189
- const entityName = this.getEntityNameForTable(tableName);
190
- if (!entityName || !this.service.entities[entityName]) {
191
- throw new MastraError({
192
- id: 'STORAGE_DYNAMODB_STORE_INSERT_INVALID_ARGS',
193
- domain: ErrorDomain.STORAGE,
194
- category: ErrorCategory.USER,
195
- text: 'No entity defined for tableName',
196
- details: { tableName },
197
- });
198
- }
199
-
200
- try {
201
- // Add the entity type to the record and preprocess before creating
202
- const dataToSave = { entity: entityName, ...this.preprocessRecord(record) };
203
- await this.service.entities[entityName].create(dataToSave).go();
204
- } catch (error) {
205
- throw new MastraError(
206
- {
207
- id: 'STORAGE_DYNAMODB_STORE_INSERT_FAILED',
208
- domain: ErrorDomain.STORAGE,
209
- category: ErrorCategory.THIRD_PARTY,
210
- details: { tableName },
211
- },
212
- error,
213
- );
214
- }
215
- }
216
-
217
- async alterTable(_args: {
218
- tableName: TABLE_NAMES;
219
- schema: Record<string, StorageColumn>;
220
- ifNotExists: string[];
221
- }): Promise<void> {
222
- // Nothing to do here, DynamoDB has a flexible schema and handles new attributes automatically upon insertion/update.
223
- }
224
-
225
- /**
226
- * Clear all items from a logical "table" (entity type)
227
- */
228
- async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
229
- this.logger.debug('DynamoDB clearTable called', { tableName });
230
-
231
- const entityName = this.getEntityNameForTable(tableName)!;
232
-
233
- if (!entityName || !this.service.entities[entityName]) {
234
- throw new MastraError({
235
- id: 'STORAGE_DYNAMODB_STORE_CLEAR_TABLE_INVALID_ARGS',
236
- domain: ErrorDomain.STORAGE,
237
- category: ErrorCategory.USER,
238
- text: 'No entity defined for tableName',
239
- details: { tableName },
240
- });
241
- }
242
-
243
- try {
244
- // Scan requires no key, just uses the entity handler
245
- const result = await this.service.entities[entityName].scan.go({ pages: 'all' }); // Get all pages
246
-
247
- if (!result.data.length) {
248
- this.logger.debug(`No records found to clear for ${tableName}`);
249
- return;
250
- }
251
-
252
- this.logger.debug(`Found ${result.data.length} records to delete for ${tableName}`);
253
-
254
- // ElectroDB batch delete expects the key components for each item
255
- const keysToDelete = result.data.map((item: any) => {
256
- const key: { entity: string; [key: string]: any } = { entity: entityName };
257
-
258
- // Construct the key based on the specific entity's primary key structure
259
- switch (entityName) {
260
- case 'thread':
261
- if (!item.id) throw new Error(`Missing required key 'id' for entity 'thread'`);
262
- key.id = item.id;
263
- break;
264
- case 'message':
265
- if (!item.id) throw new Error(`Missing required key 'id' for entity 'message'`);
266
- key.id = item.id;
267
- break;
268
- case 'workflow_snapshot':
269
- if (!item.workflow_name)
270
- throw new Error(`Missing required key 'workflow_name' for entity 'workflow_snapshot'`);
271
- if (!item.run_id) throw new Error(`Missing required key 'run_id' for entity 'workflow_snapshot'`);
272
- key.workflow_name = item.workflow_name;
273
- key.run_id = item.run_id;
274
- break;
275
- case 'eval':
276
- // Assuming 'eval' uses 'run_id' or another unique identifier as part of its PK
277
- // Adjust based on the actual primary key defined in getElectroDbService
278
- if (!item.run_id) throw new Error(`Missing required key 'run_id' for entity 'eval'`);
279
- // Add other key components if necessary for 'eval' PK
280
- key.run_id = item.run_id;
281
- // Example: if global_run_id is also part of PK:
282
- // if (!item.global_run_id) throw new Error(`Missing required key 'global_run_id' for entity 'eval'`);
283
- // key.global_run_id = item.global_run_id;
284
- break;
285
- case 'trace':
286
- // Assuming 'trace' uses 'id' as its PK
287
- // Adjust based on the actual primary key defined in getElectroDbService
288
- if (!item.id) throw new Error(`Missing required key 'id' for entity 'trace'`);
289
- key.id = item.id;
290
- break;
291
- case 'score':
292
- // Score entity uses 'id' as its PK
293
- if (!item.id) throw new Error(`Missing required key 'id' for entity 'score'`);
294
- key.id = item.id;
295
- break;
296
- default:
297
- // Handle unknown entity types - log a warning or throw an error
298
- this.logger.warn(`Unknown entity type encountered during clearTable: ${entityName}`);
299
- // Optionally throw an error if strict handling is required
300
- throw new Error(`Cannot construct delete key for unknown entity type: ${entityName}`);
301
- }
302
-
303
- return key;
304
- });
305
-
306
- const batchSize = 25;
307
- for (let i = 0; i < keysToDelete.length; i += batchSize) {
308
- const batchKeys = keysToDelete.slice(i, i + batchSize);
309
- // Pass the array of key objects to delete
310
- await this.service.entities[entityName].delete(batchKeys).go();
311
- }
312
-
313
- this.logger.debug(`Successfully cleared all records for ${tableName}`);
314
- } catch (error) {
315
- throw new MastraError(
316
- {
317
- id: 'STORAGE_DYNAMODB_STORE_CLEAR_TABLE_FAILED',
318
- domain: ErrorDomain.STORAGE,
319
- category: ErrorCategory.THIRD_PARTY,
320
- details: { tableName },
321
- },
322
- error,
323
- );
324
- }
325
- }
326
-
327
- /**
328
- * Insert multiple records as a batch
329
- */
330
- async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
331
- this.logger.debug('DynamoDB batchInsert called', { tableName, count: records.length });
332
-
333
- const entityName = this.getEntityNameForTable(tableName);
334
- if (!entityName || !this.service.entities[entityName]) {
335
- throw new MastraError({
336
- id: 'STORAGE_DYNAMODB_STORE_BATCH_INSERT_INVALID_ARGS',
337
- domain: ErrorDomain.STORAGE,
338
- category: ErrorCategory.USER,
339
- text: 'No entity defined for tableName',
340
- details: { tableName },
341
- });
342
- }
343
-
344
- // Add entity type and preprocess each record
345
- const recordsToSave = records.map(rec => ({ entity: entityName, ...this.preprocessRecord(rec) }));
346
-
347
- // ElectroDB has batch limits of 25 items, so we need to chunk
348
- const batchSize = 25;
349
- const batches = [];
350
- for (let i = 0; i < recordsToSave.length; i += batchSize) {
351
- const batch = recordsToSave.slice(i, i + batchSize);
352
- batches.push(batch);
353
- }
354
-
355
- try {
356
- // Process each batch
357
- for (const batch of batches) {
358
- // Create each item individually within the batch
359
- for (const recordData of batch) {
360
- if (!recordData.entity) {
361
- this.logger.error('Missing entity property in record data for batchInsert', { recordData, tableName });
362
- throw new Error(`Internal error: Missing entity property during batchInsert for ${tableName}`);
363
- }
364
- // Log the object just before the create call
365
- this.logger.debug('Attempting to create record in batchInsert:', { entityName, recordData });
366
- await this.service.entities[entityName].create(recordData).go();
367
- }
368
- // Original batch call: await this.service.entities[entityName].create(batch).go();
369
- }
370
- } catch (error) {
371
- throw new MastraError(
372
- {
373
- id: 'STORAGE_DYNAMODB_STORE_BATCH_INSERT_FAILED',
374
- domain: ErrorDomain.STORAGE,
375
- category: ErrorCategory.THIRD_PARTY,
376
- details: { tableName },
377
- },
378
- error,
379
- );
380
- }
381
- }
382
-
383
- /**
384
- * Load a record by its keys
385
- */
386
- async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
387
- this.logger.debug('DynamoDB load called', { tableName, keys });
388
-
389
- const entityName = this.getEntityNameForTable(tableName);
390
- if (!entityName || !this.service.entities[entityName]) {
391
- throw new MastraError({
392
- id: 'STORAGE_DYNAMODB_STORE_LOAD_INVALID_ARGS',
393
- domain: ErrorDomain.STORAGE,
394
- category: ErrorCategory.USER,
395
- text: 'No entity defined for tableName',
396
- details: { tableName },
397
- });
398
- }
399
-
400
- try {
401
- // Add the entity type to the key object for the .get call
402
- const keyObject = { entity: entityName, ...keys };
403
- const result = await this.service.entities[entityName].get(keyObject).go();
404
-
405
- if (!result.data) {
406
- return null;
407
- }
408
-
409
- // Add parsing logic if necessary (e.g., for metadata)
410
- let data = result.data;
411
- if (data.metadata && typeof data.metadata === 'string') {
412
- try {
413
- // data.metadata = JSON.parse(data.metadata); // REMOVED by AI
414
- } catch {
415
- /* ignore parse error */
416
- }
417
- }
418
- // Add similar parsing for other JSON fields if needed based on entity type
419
-
420
- return data as R;
421
- } catch (error) {
422
- throw new MastraError(
423
- {
424
- id: 'STORAGE_DYNAMODB_STORE_LOAD_FAILED',
425
- domain: ErrorDomain.STORAGE,
426
- category: ErrorCategory.THIRD_PARTY,
427
- details: { tableName },
428
- },
429
- error,
430
- );
431
- }
432
- }
433
- }