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