@mastra/dynamodb 0.13.0 → 0.13.1

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.
@@ -0,0 +1,285 @@
1
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
2
+ import type { ScoreRowData } from '@mastra/core/scores';
3
+ import { ScoresStorage } from '@mastra/core/storage';
4
+ import type { PaginationInfo, StoragePagination } from '@mastra/core/storage';
5
+ import type { Service } from 'electrodb';
6
+
7
+ export class ScoresStorageDynamoDB extends ScoresStorage {
8
+ private service: Service<Record<string, any>>;
9
+ constructor({ service }: { service: Service<Record<string, any>> }) {
10
+ super();
11
+ this.service = service;
12
+ }
13
+
14
+ // Helper function to parse score data (handle JSON fields)
15
+ private parseScoreData(data: any): ScoreRowData {
16
+ return {
17
+ ...data,
18
+ // Convert date strings back to Date objects for consistency
19
+ createdAt: data.createdAt ? new Date(data.createdAt) : new Date(),
20
+ updatedAt: data.updatedAt ? new Date(data.updatedAt) : new Date(),
21
+ // JSON fields are already transformed by the entity's getters
22
+ } as ScoreRowData;
23
+ }
24
+
25
+ async getScoreById({ id }: { id: string }): Promise<ScoreRowData | null> {
26
+ this.logger.debug('Getting score by ID', { id });
27
+ try {
28
+ const result = await this.service.entities.score.get({ entity: 'score', id }).go();
29
+
30
+ if (!result.data) {
31
+ return null;
32
+ }
33
+
34
+ return this.parseScoreData(result.data);
35
+ } catch (error) {
36
+ throw new MastraError(
37
+ {
38
+ id: 'STORAGE_DYNAMODB_STORE_GET_SCORE_BY_ID_FAILED',
39
+ domain: ErrorDomain.STORAGE,
40
+ category: ErrorCategory.THIRD_PARTY,
41
+ details: { id },
42
+ },
43
+ error,
44
+ );
45
+ }
46
+ }
47
+
48
+ async saveScore(score: Omit<ScoreRowData, 'id' | 'createdAt' | 'updatedAt'>): Promise<{ score: ScoreRowData }> {
49
+ this.logger.debug('Saving score', { scorerId: score.scorerId, runId: score.runId });
50
+
51
+ const now = new Date();
52
+ const scoreId = `score-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
53
+
54
+ const scoreData = {
55
+ entity: 'score',
56
+ id: scoreId,
57
+ scorerId: score.scorerId,
58
+ traceId: score.traceId || '',
59
+ runId: score.runId,
60
+ scorer: typeof score.scorer === 'string' ? score.scorer : JSON.stringify(score.scorer),
61
+ extractStepResult:
62
+ typeof score.extractStepResult === 'string' ? score.extractStepResult : JSON.stringify(score.extractStepResult),
63
+ analyzeStepResult:
64
+ typeof score.analyzeStepResult === 'string' ? score.analyzeStepResult : JSON.stringify(score.analyzeStepResult),
65
+ score: score.score,
66
+ reason: score.reason,
67
+ extractPrompt: score.extractPrompt,
68
+ analyzePrompt: score.analyzePrompt,
69
+ reasonPrompt: score.reasonPrompt,
70
+ input: typeof score.input === 'string' ? score.input : JSON.stringify(score.input),
71
+ output: typeof score.output === 'string' ? score.output : JSON.stringify(score.output),
72
+ additionalContext:
73
+ typeof score.additionalContext === 'string' ? score.additionalContext : JSON.stringify(score.additionalContext),
74
+ runtimeContext:
75
+ typeof score.runtimeContext === 'string' ? score.runtimeContext : JSON.stringify(score.runtimeContext),
76
+ entityType: score.entityType,
77
+ entityData: typeof score.entity === 'string' ? score.entity : JSON.stringify(score.entity),
78
+ entityId: score.entityId,
79
+ source: score.source,
80
+ resourceId: score.resourceId || '',
81
+ threadId: score.threadId || '',
82
+ createdAt: now.toISOString(),
83
+ updatedAt: now.toISOString(),
84
+ };
85
+
86
+ try {
87
+ await this.service.entities.score.upsert(scoreData).go();
88
+
89
+ const savedScore: ScoreRowData = {
90
+ ...score,
91
+ id: scoreId,
92
+ createdAt: now,
93
+ updatedAt: now,
94
+ };
95
+
96
+ return { score: savedScore };
97
+ } catch (error) {
98
+ throw new MastraError(
99
+ {
100
+ id: 'STORAGE_DYNAMODB_STORE_SAVE_SCORE_FAILED',
101
+ domain: ErrorDomain.STORAGE,
102
+ category: ErrorCategory.THIRD_PARTY,
103
+ details: { scorerId: score.scorerId, runId: score.runId },
104
+ },
105
+ error,
106
+ );
107
+ }
108
+ }
109
+
110
+ async getScoresByScorerId({
111
+ scorerId,
112
+ pagination,
113
+ entityId,
114
+ entityType,
115
+ }: {
116
+ scorerId: string;
117
+ pagination: StoragePagination;
118
+ entityId?: string;
119
+ entityType?: string;
120
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
121
+ this.logger.debug('Getting scores by scorer ID', { scorerId, pagination, entityId, entityType });
122
+
123
+ try {
124
+ // Query scores by scorer ID using the GSI
125
+ const query = this.service.entities.score.query.byScorer({ entity: 'score', scorerId });
126
+
127
+ // Get all scores for this scorer ID (DynamoDB doesn't support OFFSET/LIMIT)
128
+ const results = await query.go();
129
+ let allScores = results.data.map((data: any) => this.parseScoreData(data));
130
+
131
+ // Apply additional filters if provided
132
+ if (entityId) {
133
+ allScores = allScores.filter((score: ScoreRowData) => score.entityId === entityId);
134
+ }
135
+ if (entityType) {
136
+ allScores = allScores.filter((score: ScoreRowData) => score.entityType === entityType);
137
+ }
138
+
139
+ // Sort by createdAt DESC (newest first)
140
+ allScores.sort((a: ScoreRowData, b: ScoreRowData) => b.createdAt.getTime() - a.createdAt.getTime());
141
+
142
+ // Apply pagination in memory
143
+ const startIndex = pagination.page * pagination.perPage;
144
+ const endIndex = startIndex + pagination.perPage;
145
+ const paginatedScores = allScores.slice(startIndex, endIndex);
146
+
147
+ // Calculate pagination info
148
+ const total = allScores.length;
149
+ const hasMore = endIndex < total;
150
+
151
+ return {
152
+ scores: paginatedScores,
153
+ pagination: {
154
+ total,
155
+ page: pagination.page,
156
+ perPage: pagination.perPage,
157
+ hasMore,
158
+ },
159
+ };
160
+ } catch (error) {
161
+ throw new MastraError(
162
+ {
163
+ id: 'STORAGE_DYNAMODB_STORE_GET_SCORES_BY_SCORER_ID_FAILED',
164
+ domain: ErrorDomain.STORAGE,
165
+ category: ErrorCategory.THIRD_PARTY,
166
+ details: {
167
+ scorerId: scorerId || '',
168
+ entityId: entityId || '',
169
+ entityType: entityType || '',
170
+ page: pagination.page,
171
+ perPage: pagination.perPage,
172
+ },
173
+ },
174
+ error,
175
+ );
176
+ }
177
+ }
178
+
179
+ async getScoresByRunId({
180
+ runId,
181
+ pagination,
182
+ }: {
183
+ runId: string;
184
+ pagination: StoragePagination;
185
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
186
+ this.logger.debug('Getting scores by run ID', { runId, pagination });
187
+
188
+ try {
189
+ // Query scores by run ID using the GSI
190
+ const query = this.service.entities.score.query.byRun({ entity: 'score', runId });
191
+
192
+ // Get all scores for this run ID
193
+ const results = await query.go();
194
+ const allScores = results.data.map((data: any) => this.parseScoreData(data));
195
+
196
+ // Sort by createdAt DESC (newest first)
197
+ allScores.sort((a: ScoreRowData, b: ScoreRowData) => b.createdAt.getTime() - a.createdAt.getTime());
198
+
199
+ // Apply pagination in memory
200
+ const startIndex = pagination.page * pagination.perPage;
201
+ const endIndex = startIndex + pagination.perPage;
202
+ const paginatedScores = allScores.slice(startIndex, endIndex);
203
+
204
+ // Calculate pagination info
205
+ const total = allScores.length;
206
+ const hasMore = endIndex < total;
207
+
208
+ return {
209
+ scores: paginatedScores,
210
+ pagination: {
211
+ total,
212
+ page: pagination.page,
213
+ perPage: pagination.perPage,
214
+ hasMore,
215
+ },
216
+ };
217
+ } catch (error) {
218
+ throw new MastraError(
219
+ {
220
+ id: 'STORAGE_DYNAMODB_STORE_GET_SCORES_BY_RUN_ID_FAILED',
221
+ domain: ErrorDomain.STORAGE,
222
+ category: ErrorCategory.THIRD_PARTY,
223
+ details: { runId, page: pagination.page, perPage: pagination.perPage },
224
+ },
225
+ error,
226
+ );
227
+ }
228
+ }
229
+
230
+ async getScoresByEntityId({
231
+ entityId,
232
+ entityType,
233
+ pagination,
234
+ }: {
235
+ entityId: string;
236
+ entityType: string;
237
+ pagination: StoragePagination;
238
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
239
+ this.logger.debug('Getting scores by entity ID', { entityId, entityType, pagination });
240
+
241
+ try {
242
+ // Use the byEntityData index which uses entityId as the primary key
243
+ const query = this.service.entities.score.query.byEntityData({ entity: 'score', entityId });
244
+
245
+ // Get all scores for this entity ID
246
+ const results = await query.go();
247
+ let allScores = results.data.map((data: any) => this.parseScoreData(data));
248
+
249
+ // Filter by entityType since the index only uses entityId
250
+ allScores = allScores.filter((score: ScoreRowData) => score.entityType === entityType);
251
+
252
+ // Sort by createdAt DESC (newest first)
253
+ allScores.sort((a: ScoreRowData, b: ScoreRowData) => b.createdAt.getTime() - a.createdAt.getTime());
254
+
255
+ // Apply pagination in memory
256
+ const startIndex = pagination.page * pagination.perPage;
257
+ const endIndex = startIndex + pagination.perPage;
258
+ const paginatedScores = allScores.slice(startIndex, endIndex);
259
+
260
+ // Calculate pagination info
261
+ const total = allScores.length;
262
+ const hasMore = endIndex < total;
263
+
264
+ return {
265
+ scores: paginatedScores,
266
+ pagination: {
267
+ total,
268
+ page: pagination.page,
269
+ perPage: pagination.perPage,
270
+ hasMore,
271
+ },
272
+ };
273
+ } catch (error) {
274
+ throw new MastraError(
275
+ {
276
+ id: 'STORAGE_DYNAMODB_STORE_GET_SCORES_BY_ENTITY_ID_FAILED',
277
+ domain: ErrorDomain.STORAGE,
278
+ category: ErrorCategory.THIRD_PARTY,
279
+ details: { entityId, entityType, page: pagination.page, perPage: pagination.perPage },
280
+ },
281
+ error,
282
+ );
283
+ }
284
+ }
285
+ }
@@ -0,0 +1,286 @@
1
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
2
+ import { TABLE_TRACES, TracesStorage } from '@mastra/core/storage';
3
+ import type { PaginationInfo, StorageGetTracesPaginatedArg } from '@mastra/core/storage';
4
+ import type { Trace } from '@mastra/core/telemetry';
5
+ import type { Service } from 'electrodb';
6
+ import type { StoreOperationsDynamoDB } from '../operations';
7
+
8
+ export class TracesStorageDynamoDB extends TracesStorage {
9
+ private service: Service<Record<string, any>>;
10
+ private operations: StoreOperationsDynamoDB;
11
+ constructor({ service, operations }: { service: Service<Record<string, any>>; operations: StoreOperationsDynamoDB }) {
12
+ super();
13
+
14
+ this.service = service;
15
+ this.operations = operations;
16
+ }
17
+
18
+ // Trace operations
19
+ async getTraces(args: {
20
+ name?: string;
21
+ scope?: string;
22
+ page: number;
23
+ perPage: number;
24
+ attributes?: Record<string, string>;
25
+ filters?: Record<string, any>;
26
+ }): Promise<any[]> {
27
+ const { name, scope, page, perPage } = args;
28
+ this.logger.debug('Getting traces', { name, scope, page, perPage });
29
+
30
+ try {
31
+ let query;
32
+
33
+ // Determine which index to use based on the provided filters
34
+ // Provide *all* composite key components for the relevant index
35
+ if (name) {
36
+ query = this.service.entities.trace.query.byName({ entity: 'trace', name });
37
+ } else if (scope) {
38
+ query = this.service.entities.trace.query.byScope({ entity: 'trace', scope });
39
+ } else {
40
+ this.logger.warn('Performing a scan operation on traces - consider using a more specific query');
41
+ query = this.service.entities.trace.scan;
42
+ }
43
+
44
+ let items: any[] = [];
45
+ let cursor = null;
46
+ let pagesFetched = 0;
47
+ const startPage = page > 0 ? page : 1;
48
+
49
+ do {
50
+ const results: { data: any[]; cursor: string | null } = await query.go({ cursor, limit: perPage });
51
+ pagesFetched++;
52
+ if (pagesFetched === startPage) {
53
+ items = results.data;
54
+ break;
55
+ }
56
+ cursor = results.cursor;
57
+ if (!cursor && results.data.length > 0 && pagesFetched < startPage) {
58
+ break;
59
+ }
60
+ } while (cursor && pagesFetched < startPage);
61
+
62
+ return items;
63
+ } catch (error) {
64
+ throw new MastraError(
65
+ {
66
+ id: 'STORAGE_DYNAMODB_STORE_GET_TRACES_FAILED',
67
+ domain: ErrorDomain.STORAGE,
68
+ category: ErrorCategory.THIRD_PARTY,
69
+ },
70
+ error,
71
+ );
72
+ }
73
+ }
74
+
75
+ async batchTraceInsert({ records }: { records: Record<string, any>[] }): Promise<void> {
76
+ this.logger.debug('Batch inserting traces', { count: records.length });
77
+
78
+ if (!records.length) {
79
+ return;
80
+ }
81
+
82
+ try {
83
+ // Add 'entity' type to each record before passing to generic batchInsert
84
+ const recordsToSave = records.map(rec => ({ entity: 'trace', ...rec }));
85
+ await this.operations.batchInsert({
86
+ tableName: TABLE_TRACES,
87
+ records: recordsToSave, // Pass records with 'entity' included
88
+ });
89
+ } catch (error) {
90
+ throw new MastraError(
91
+ {
92
+ id: 'STORAGE_DYNAMODB_STORE_BATCH_TRACE_INSERT_FAILED',
93
+ domain: ErrorDomain.STORAGE,
94
+ category: ErrorCategory.THIRD_PARTY,
95
+ details: { count: records.length },
96
+ },
97
+ error,
98
+ );
99
+ }
100
+ }
101
+
102
+ async getTracesPaginated(args: StorageGetTracesPaginatedArg): Promise<PaginationInfo & { traces: Trace[] }> {
103
+ const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
104
+ this.logger.debug('Getting traces with pagination', { name, scope, page, perPage, attributes, filters, dateRange });
105
+
106
+ try {
107
+ let query;
108
+
109
+ // Determine which index to use based on the provided filters
110
+ if (name) {
111
+ query = this.service.entities.trace.query.byName({ entity: 'trace', name });
112
+ } else if (scope) {
113
+ query = this.service.entities.trace.query.byScope({ entity: 'trace', scope });
114
+ } else {
115
+ this.logger.warn('Performing a scan operation on traces - consider using a more specific query');
116
+ query = this.service.entities.trace.scan;
117
+ }
118
+
119
+ // For DynamoDB, we need to fetch all data and apply pagination in memory
120
+ // since DynamoDB doesn't support traditional offset-based pagination
121
+ const results = await query.go({
122
+ order: 'desc',
123
+ pages: 'all', // Get all pages to apply filtering and pagination
124
+ });
125
+
126
+ if (!results.data.length) {
127
+ return {
128
+ traces: [],
129
+ total: 0,
130
+ page,
131
+ perPage,
132
+ hasMore: false,
133
+ };
134
+ }
135
+
136
+ // Apply filters in memory
137
+ let filteredData = results.data;
138
+
139
+ // Filter by attributes if provided
140
+ if (attributes) {
141
+ filteredData = filteredData.filter((item: Record<string, any>) => {
142
+ try {
143
+ // Handle the case where attributes might be stored as "[object Object]" or JSON string
144
+ let itemAttributes: Record<string, any> = {};
145
+
146
+ if (item.attributes) {
147
+ if (typeof item.attributes === 'string') {
148
+ if (item.attributes === '[object Object]') {
149
+ // This means the object was stringified incorrectly
150
+ itemAttributes = {};
151
+ } else {
152
+ try {
153
+ itemAttributes = JSON.parse(item.attributes);
154
+ } catch {
155
+ itemAttributes = {};
156
+ }
157
+ }
158
+ } else if (typeof item.attributes === 'object') {
159
+ itemAttributes = item.attributes;
160
+ }
161
+ }
162
+
163
+ return Object.entries(attributes).every(([key, value]) => itemAttributes[key] === value);
164
+ } catch (e) {
165
+ this.logger.warn('Failed to parse attributes during filtering', { item, error: e });
166
+ return false;
167
+ }
168
+ });
169
+ }
170
+
171
+ // Filter by date range if provided
172
+ if (dateRange?.start) {
173
+ filteredData = filteredData.filter((item: Record<string, any>) => {
174
+ const itemDate = new Date(item.createdAt);
175
+ return itemDate >= dateRange.start!;
176
+ });
177
+ }
178
+
179
+ if (dateRange?.end) {
180
+ filteredData = filteredData.filter((item: Record<string, any>) => {
181
+ const itemDate = new Date(item.createdAt);
182
+ return itemDate <= dateRange.end!;
183
+ });
184
+ }
185
+
186
+ // Apply pagination
187
+ const total = filteredData.length;
188
+ const start = page * perPage;
189
+ const end = start + perPage;
190
+ const paginatedData = filteredData.slice(start, end);
191
+
192
+ const traces = paginatedData.map((item: any) => {
193
+ // Handle the case where attributes might be stored as "[object Object]" or JSON string
194
+ let attributes: Record<string, any> | undefined;
195
+ if (item.attributes) {
196
+ if (typeof item.attributes === 'string') {
197
+ if (item.attributes === '[object Object]') {
198
+ attributes = undefined;
199
+ } else {
200
+ try {
201
+ attributes = JSON.parse(item.attributes);
202
+ } catch {
203
+ attributes = undefined;
204
+ }
205
+ }
206
+ } else if (typeof item.attributes === 'object') {
207
+ attributes = item.attributes;
208
+ }
209
+ }
210
+
211
+ let status: Record<string, any> | undefined;
212
+ if (item.status) {
213
+ if (typeof item.status === 'string') {
214
+ try {
215
+ status = JSON.parse(item.status);
216
+ } catch {
217
+ status = undefined;
218
+ }
219
+ } else if (typeof item.status === 'object') {
220
+ status = item.status;
221
+ }
222
+ }
223
+
224
+ let events: any[] | undefined;
225
+ if (item.events) {
226
+ if (typeof item.events === 'string') {
227
+ try {
228
+ events = JSON.parse(item.events);
229
+ } catch {
230
+ events = undefined;
231
+ }
232
+ } else if (Array.isArray(item.events)) {
233
+ events = item.events;
234
+ }
235
+ }
236
+
237
+ let links: any[] | undefined;
238
+ if (item.links) {
239
+ if (typeof item.links === 'string') {
240
+ try {
241
+ links = JSON.parse(item.links);
242
+ } catch {
243
+ links = undefined;
244
+ }
245
+ } else if (Array.isArray(item.links)) {
246
+ links = item.links;
247
+ }
248
+ }
249
+
250
+ return {
251
+ id: item.id,
252
+ parentSpanId: item.parentSpanId,
253
+ name: item.name,
254
+ traceId: item.traceId,
255
+ scope: item.scope,
256
+ kind: item.kind,
257
+ attributes,
258
+ status,
259
+ events,
260
+ links,
261
+ other: item.other,
262
+ startTime: item.startTime,
263
+ endTime: item.endTime,
264
+ createdAt: item.createdAt,
265
+ };
266
+ });
267
+
268
+ return {
269
+ traces,
270
+ total,
271
+ page,
272
+ perPage,
273
+ hasMore: end < total,
274
+ };
275
+ } catch (error) {
276
+ throw new MastraError(
277
+ {
278
+ id: 'STORAGE_DYNAMODB_STORE_GET_TRACES_PAGINATED_FAILED',
279
+ domain: ErrorDomain.STORAGE,
280
+ category: ErrorCategory.THIRD_PARTY,
281
+ },
282
+ error,
283
+ );
284
+ }
285
+ }
286
+ }