@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,297 @@
1
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
2
+ import { WorkflowsStorage } from '@mastra/core/storage';
3
+ import type { WorkflowRun, WorkflowRuns } from '@mastra/core/storage';
4
+ import type { WorkflowRunState } from '@mastra/core/workflows';
5
+ import type { Service } from 'electrodb';
6
+
7
+ // Define the structure for workflow snapshot items retrieved from DynamoDB
8
+ interface WorkflowSnapshotDBItem {
9
+ entity: string; // Typically 'workflow_snapshot'
10
+ workflow_name: string;
11
+ run_id: string;
12
+ snapshot: WorkflowRunState; // Should be WorkflowRunState after ElectroDB get attribute processing
13
+ createdAt: string; // ISO Date string
14
+ updatedAt: string; // ISO Date string
15
+ resourceId?: string;
16
+ }
17
+
18
+ function formatWorkflowRun(snapshotData: WorkflowSnapshotDBItem): WorkflowRun {
19
+ return {
20
+ workflowName: snapshotData.workflow_name,
21
+ runId: snapshotData.run_id,
22
+ snapshot: snapshotData.snapshot as WorkflowRunState,
23
+ createdAt: new Date(snapshotData.createdAt),
24
+ updatedAt: new Date(snapshotData.updatedAt),
25
+ resourceId: snapshotData.resourceId,
26
+ };
27
+ }
28
+
29
+ export class WorkflowStorageDynamoDB extends WorkflowsStorage {
30
+ private service: Service<Record<string, any>>;
31
+ constructor({ service }: { service: Service<Record<string, any>> }) {
32
+ super();
33
+
34
+ this.service = service;
35
+ }
36
+
37
+ // Workflow operations
38
+ async persistWorkflowSnapshot({
39
+ workflowName,
40
+ runId,
41
+ snapshot,
42
+ }: {
43
+ workflowName: string;
44
+ runId: string;
45
+ snapshot: WorkflowRunState;
46
+ }): Promise<void> {
47
+ this.logger.debug('Persisting workflow snapshot', { workflowName, runId });
48
+
49
+ try {
50
+ const resourceId = 'resourceId' in snapshot ? snapshot.resourceId : undefined;
51
+ const now = new Date().toISOString();
52
+ // Prepare data including the 'entity' type
53
+ const data = {
54
+ entity: 'workflow_snapshot', // Add entity type
55
+ workflow_name: workflowName,
56
+ run_id: runId,
57
+ snapshot: JSON.stringify(snapshot), // Stringify the snapshot object
58
+ createdAt: now,
59
+ updatedAt: now,
60
+ resourceId,
61
+ };
62
+ // Use upsert instead of create to handle both create and update cases
63
+ await this.service.entities.workflow_snapshot.upsert(data).go();
64
+ } catch (error) {
65
+ throw new MastraError(
66
+ {
67
+ id: 'STORAGE_DYNAMODB_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
68
+ domain: ErrorDomain.STORAGE,
69
+ category: ErrorCategory.THIRD_PARTY,
70
+ details: { workflowName, runId },
71
+ },
72
+ error,
73
+ );
74
+ }
75
+ }
76
+
77
+ async loadWorkflowSnapshot({
78
+ workflowName,
79
+ runId,
80
+ }: {
81
+ workflowName: string;
82
+ runId: string;
83
+ }): Promise<WorkflowRunState | null> {
84
+ this.logger.debug('Loading workflow snapshot', { workflowName, runId });
85
+
86
+ try {
87
+ // Provide *all* composite key components for the primary index ('entity', 'workflow_name', 'run_id')
88
+ const result = await this.service.entities.workflow_snapshot
89
+ .get({
90
+ entity: 'workflow_snapshot', // Add entity type
91
+ workflow_name: workflowName,
92
+ run_id: runId,
93
+ })
94
+ .go();
95
+
96
+ if (!result.data?.snapshot) {
97
+ // Check snapshot exists
98
+ return null;
99
+ }
100
+
101
+ // Parse the snapshot string
102
+ return result.data.snapshot as WorkflowRunState;
103
+ } catch (error) {
104
+ throw new MastraError(
105
+ {
106
+ id: 'STORAGE_DYNAMODB_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
107
+ domain: ErrorDomain.STORAGE,
108
+ category: ErrorCategory.THIRD_PARTY,
109
+ details: { workflowName, runId },
110
+ },
111
+ error,
112
+ );
113
+ }
114
+ }
115
+
116
+ async getWorkflowRuns(args?: {
117
+ workflowName?: string;
118
+ fromDate?: Date;
119
+ toDate?: Date;
120
+ limit?: number;
121
+ offset?: number;
122
+ resourceId?: string;
123
+ }): Promise<WorkflowRuns> {
124
+ this.logger.debug('Getting workflow runs', { args });
125
+
126
+ try {
127
+ // Default values
128
+ const limit = args?.limit || 10;
129
+ const offset = args?.offset || 0;
130
+
131
+ let query;
132
+
133
+ if (args?.workflowName) {
134
+ // Query by workflow name using the primary index
135
+ // Provide *all* composite key components for the PK ('entity', 'workflow_name')
136
+ query = this.service.entities.workflow_snapshot.query.primary({
137
+ entity: 'workflow_snapshot', // Add entity type
138
+ workflow_name: args.workflowName,
139
+ });
140
+ } else {
141
+ // If no workflow name, we need to scan
142
+ // This is not ideal for production with large datasets
143
+ this.logger.warn('Performing a scan operation on workflow snapshots - consider using a more specific query');
144
+ query = this.service.entities.workflow_snapshot.scan; // Scan still uses the service entity
145
+ }
146
+
147
+ const allMatchingSnapshots: WorkflowSnapshotDBItem[] = [];
148
+ let cursor: string | null = null;
149
+ const DYNAMODB_PAGE_SIZE = 100; // Sensible page size for fetching
150
+
151
+ do {
152
+ const pageResults: { data: WorkflowSnapshotDBItem[]; cursor: string | null } = await query.go({
153
+ limit: DYNAMODB_PAGE_SIZE,
154
+ cursor,
155
+ });
156
+
157
+ if (pageResults.data && pageResults.data.length > 0) {
158
+ let pageFilteredData: WorkflowSnapshotDBItem[] = pageResults.data;
159
+
160
+ // Apply date filters if specified
161
+ if (args?.fromDate || args?.toDate) {
162
+ pageFilteredData = pageFilteredData.filter((snapshot: WorkflowSnapshotDBItem) => {
163
+ const createdAt = new Date(snapshot.createdAt);
164
+ if (args.fromDate && createdAt < args.fromDate) {
165
+ return false;
166
+ }
167
+ if (args.toDate && createdAt > args.toDate) {
168
+ return false;
169
+ }
170
+ return true;
171
+ });
172
+ }
173
+
174
+ // Filter by resourceId if specified
175
+ if (args?.resourceId) {
176
+ pageFilteredData = pageFilteredData.filter((snapshot: WorkflowSnapshotDBItem) => {
177
+ return snapshot.resourceId === args.resourceId;
178
+ });
179
+ }
180
+ allMatchingSnapshots.push(...pageFilteredData);
181
+ }
182
+
183
+ cursor = pageResults.cursor;
184
+ } while (cursor);
185
+
186
+ if (!allMatchingSnapshots.length) {
187
+ return { runs: [], total: 0 };
188
+ }
189
+
190
+ // Apply offset and limit to the accumulated filtered results
191
+ const total = allMatchingSnapshots.length;
192
+ const paginatedData = allMatchingSnapshots.slice(offset, offset + limit);
193
+
194
+ // Format and return the results
195
+ const runs = paginatedData.map((snapshot: WorkflowSnapshotDBItem) => formatWorkflowRun(snapshot));
196
+
197
+ return {
198
+ runs,
199
+ total,
200
+ };
201
+ } catch (error) {
202
+ throw new MastraError(
203
+ {
204
+ id: 'STORAGE_DYNAMODB_STORE_GET_WORKFLOW_RUNS_FAILED',
205
+ domain: ErrorDomain.STORAGE,
206
+ category: ErrorCategory.THIRD_PARTY,
207
+ details: { workflowName: args?.workflowName || '', resourceId: args?.resourceId || '' },
208
+ },
209
+ error,
210
+ );
211
+ }
212
+ }
213
+
214
+ async getWorkflowRunById(args: { runId: string; workflowName?: string }): Promise<WorkflowRun | null> {
215
+ const { runId, workflowName } = args;
216
+ this.logger.debug('Getting workflow run by ID', { runId, workflowName });
217
+
218
+ console.log('workflowName', workflowName);
219
+ console.log('runId', runId);
220
+
221
+ try {
222
+ // If we have a workflowName, we can do a direct get using the primary key
223
+ if (workflowName) {
224
+ this.logger.debug('WorkflowName provided, using direct GET operation.');
225
+ const result = await this.service.entities.workflow_snapshot
226
+ .get({
227
+ entity: 'workflow_snapshot', // Entity type for PK
228
+ workflow_name: workflowName,
229
+ run_id: runId,
230
+ })
231
+ .go();
232
+
233
+ console.log('result', result);
234
+
235
+ if (!result.data) {
236
+ return null;
237
+ }
238
+
239
+ const snapshot = result.data.snapshot;
240
+ return {
241
+ workflowName: result.data.workflow_name,
242
+ runId: result.data.run_id,
243
+ snapshot,
244
+ createdAt: new Date(result.data.createdAt),
245
+ updatedAt: new Date(result.data.updatedAt),
246
+ resourceId: result.data.resourceId,
247
+ };
248
+ }
249
+
250
+ // Otherwise, if workflowName is not provided, use the GSI on runId.
251
+ // This is more efficient than a full table scan.
252
+ this.logger.debug(
253
+ 'WorkflowName not provided. Attempting to find workflow run by runId using GSI. Ensure GSI (e.g., "byRunId") is defined on the workflowSnapshot entity with run_id as its key and provisioned in DynamoDB.',
254
+ );
255
+
256
+ // IMPORTANT: This assumes a GSI (e.g., named 'byRunId') exists on the workflowSnapshot entity
257
+ // with 'run_id' as its partition key. This GSI must be:
258
+ // 1. Defined in your ElectroDB model (e.g., in stores/dynamodb/src/entities/index.ts).
259
+ // 2. Provisioned in the actual DynamoDB table (e.g., via CDK/CloudFormation).
260
+ // The query key object includes 'entity' as it's good practice with ElectroDB and single-table design,
261
+ // aligning with how other GSIs are queried in this file.
262
+ const result = await this.service.entities.workflow_snapshot.query
263
+ .gsi2({ entity: 'workflow_snapshot', run_id: runId }) // Replace 'byRunId' with your actual GSI name
264
+ .go();
265
+
266
+ // If the GSI query returns multiple items (e.g., if run_id is not globally unique across all snapshots),
267
+ // this will take the first one. The original scan logic also effectively took the first match found.
268
+ // If run_id is guaranteed unique, result.data should contain at most one item.
269
+ const matchingRunDbItem: WorkflowSnapshotDBItem | null =
270
+ result.data && result.data.length > 0 ? result.data[0] : null;
271
+
272
+ if (!matchingRunDbItem) {
273
+ return null;
274
+ }
275
+
276
+ const snapshot = matchingRunDbItem.snapshot;
277
+ return {
278
+ workflowName: matchingRunDbItem.workflow_name,
279
+ runId: matchingRunDbItem.run_id,
280
+ snapshot,
281
+ createdAt: new Date(matchingRunDbItem.createdAt),
282
+ updatedAt: new Date(matchingRunDbItem.updatedAt),
283
+ resourceId: matchingRunDbItem.resourceId,
284
+ };
285
+ } catch (error) {
286
+ throw new MastraError(
287
+ {
288
+ id: 'STORAGE_DYNAMODB_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED',
289
+ domain: ErrorDomain.STORAGE,
290
+ category: ErrorCategory.THIRD_PARTY,
291
+ details: { runId, workflowName: args?.workflowName || '' },
292
+ },
293
+ error,
294
+ );
295
+ }
296
+ }
297
+ }