@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,987 +0,0 @@
1
- import { MessageList } from '@mastra/core/agent';
2
- import type { MastraMessageContentV2 } from '@mastra/core/agent';
3
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
- import type { StorageThreadType, MastraMessageV1, MastraMessageV2 } from '@mastra/core/memory';
5
- import { MemoryStorage, resolveMessageLimit } from '@mastra/core/storage';
6
- import type {
7
- PaginationInfo,
8
- StorageGetMessagesArg,
9
- StorageResourceType,
10
- ThreadSortOptions,
11
- } from '@mastra/core/storage';
12
- import type { Service } from 'electrodb';
13
-
14
- export class MemoryStorageDynamoDB extends MemoryStorage {
15
- private service: Service<Record<string, any>>;
16
- constructor({ service }: { service: Service<Record<string, any>> }) {
17
- super();
18
- this.service = service;
19
- }
20
-
21
- // Helper function to parse message data (handle JSON fields)
22
- private parseMessageData(data: any): MastraMessageV2 | MastraMessageV1 {
23
- // Removed try/catch and JSON.parse logic - now handled by entity 'get' attributes
24
- // This function now primarily ensures correct typing and Date conversion.
25
- return {
26
- ...data,
27
- // Ensure dates are Date objects if needed (ElectroDB might return strings)
28
- createdAt: data.createdAt ? new Date(data.createdAt) : undefined,
29
- updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
30
- // Other fields like content, toolCallArgs etc. are assumed to be correctly
31
- // transformed by the ElectroDB entity getters.
32
- };
33
- }
34
-
35
- // Helper function to transform and sort threads
36
- private transformAndSortThreads(rawThreads: any[], orderBy: string, sortDirection: string): StorageThreadType[] {
37
- return rawThreads
38
- .map((data: any) => ({
39
- ...data,
40
- // Convert date strings back to Date objects for consistency
41
- createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt,
42
- updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt,
43
- }))
44
- .sort((a: StorageThreadType, b: StorageThreadType) => {
45
- const fieldA = orderBy === 'createdAt' ? a.createdAt : a.updatedAt;
46
- const fieldB = orderBy === 'createdAt' ? b.createdAt : b.updatedAt;
47
-
48
- const comparison = fieldA.getTime() - fieldB.getTime();
49
- return sortDirection === 'DESC' ? -comparison : comparison;
50
- }) as StorageThreadType[];
51
- }
52
-
53
- async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
54
- this.logger.debug('Getting thread by ID', { threadId });
55
- try {
56
- const result = await this.service.entities.thread.get({ entity: 'thread', id: threadId }).go();
57
-
58
- if (!result.data) {
59
- return null;
60
- }
61
-
62
- // ElectroDB handles the transformation with attribute getters
63
- const data = result.data;
64
- return {
65
- ...data,
66
- // Convert date strings back to Date objects for consistency
67
- createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt,
68
- updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt,
69
- // metadata: data.metadata ? JSON.parse(data.metadata) : undefined, // REMOVED by AI
70
- // metadata is already transformed by the entity's getter
71
- } as StorageThreadType;
72
- } catch (error) {
73
- throw new MastraError(
74
- {
75
- id: 'STORAGE_DYNAMODB_STORE_GET_THREAD_BY_ID_FAILED',
76
- domain: ErrorDomain.STORAGE,
77
- category: ErrorCategory.THIRD_PARTY,
78
- details: { threadId },
79
- },
80
- error,
81
- );
82
- }
83
- }
84
-
85
- /**
86
- * @deprecated use getThreadsByResourceIdPaginated instead for paginated results.
87
- */
88
- public async getThreadsByResourceId(args: { resourceId: string } & ThreadSortOptions): Promise<StorageThreadType[]> {
89
- const resourceId = args.resourceId;
90
- const orderBy = this.castThreadOrderBy(args.orderBy);
91
- const sortDirection = this.castThreadSortDirection(args.sortDirection);
92
-
93
- this.logger.debug('Getting threads by resource ID', { resourceId, orderBy, sortDirection });
94
-
95
- try {
96
- const result = await this.service.entities.thread.query.byResource({ entity: 'thread', resourceId }).go();
97
-
98
- if (!result.data.length) {
99
- return [];
100
- }
101
-
102
- // Use shared helper method for transformation and sorting
103
- return this.transformAndSortThreads(result.data, orderBy, sortDirection);
104
- } catch (error) {
105
- throw new MastraError(
106
- {
107
- id: 'STORAGE_DYNAMODB_STORE_GET_THREADS_BY_RESOURCE_ID_FAILED',
108
- domain: ErrorDomain.STORAGE,
109
- category: ErrorCategory.THIRD_PARTY,
110
- details: { resourceId },
111
- },
112
- error,
113
- );
114
- }
115
- }
116
-
117
- async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
118
- this.logger.debug('Saving thread', { threadId: thread.id });
119
-
120
- const now = new Date();
121
-
122
- const threadData = {
123
- entity: 'thread',
124
- id: thread.id,
125
- resourceId: thread.resourceId,
126
- title: thread.title || `Thread ${thread.id}`,
127
- createdAt: thread.createdAt?.toISOString() || now.toISOString(),
128
- updatedAt: thread.updatedAt?.toISOString() || now.toISOString(),
129
- metadata: thread.metadata ? JSON.stringify(thread.metadata) : undefined,
130
- };
131
-
132
- try {
133
- await this.service.entities.thread.upsert(threadData).go();
134
-
135
- return {
136
- id: thread.id,
137
- resourceId: thread.resourceId,
138
- title: threadData.title,
139
- createdAt: thread.createdAt || now,
140
- updatedAt: now,
141
- metadata: thread.metadata,
142
- };
143
- } catch (error) {
144
- throw new MastraError(
145
- {
146
- id: 'STORAGE_DYNAMODB_STORE_SAVE_THREAD_FAILED',
147
- domain: ErrorDomain.STORAGE,
148
- category: ErrorCategory.THIRD_PARTY,
149
- details: { threadId: thread.id },
150
- },
151
- error,
152
- );
153
- }
154
- }
155
-
156
- async updateThread({
157
- id,
158
- title,
159
- metadata,
160
- }: {
161
- id: string;
162
- title: string;
163
- metadata: Record<string, unknown>;
164
- }): Promise<StorageThreadType> {
165
- this.logger.debug('Updating thread', { threadId: id });
166
-
167
- try {
168
- // First, get the existing thread to merge with updates
169
- const existingThread = await this.getThreadById({ threadId: id });
170
-
171
- if (!existingThread) {
172
- throw new Error(`Thread not found: ${id}`);
173
- }
174
-
175
- const now = new Date();
176
-
177
- // Prepare the update
178
- // Define type for only the fields we are actually updating
179
- type ThreadUpdatePayload = {
180
- updatedAt: string; // ISO String for DDB
181
- title?: string;
182
- metadata?: string; // Stringified JSON for DDB
183
- };
184
- const updateData: ThreadUpdatePayload = {
185
- updatedAt: now.toISOString(),
186
- };
187
-
188
- if (title) {
189
- updateData.title = title;
190
- }
191
-
192
- if (metadata) {
193
- // Merge with existing metadata instead of overwriting
194
- const existingMetadata = existingThread.metadata
195
- ? typeof existingThread.metadata === 'string'
196
- ? JSON.parse(existingThread.metadata)
197
- : existingThread.metadata
198
- : {};
199
- const mergedMetadata = { ...existingMetadata, ...metadata };
200
- updateData.metadata = JSON.stringify(mergedMetadata); // Stringify merged metadata for update
201
- }
202
-
203
- // Update the thread using the primary key
204
- await this.service.entities.thread.update({ entity: 'thread', id }).set(updateData).go();
205
-
206
- // Return the potentially updated thread object
207
- return {
208
- ...existingThread,
209
- title: title || existingThread.title,
210
- metadata: metadata ? { ...existingThread.metadata, ...metadata } : existingThread.metadata,
211
- updatedAt: now,
212
- };
213
- } catch (error) {
214
- throw new MastraError(
215
- {
216
- id: 'STORAGE_DYNAMODB_STORE_UPDATE_THREAD_FAILED',
217
- domain: ErrorDomain.STORAGE,
218
- category: ErrorCategory.THIRD_PARTY,
219
- details: { threadId: id },
220
- },
221
- error,
222
- );
223
- }
224
- }
225
-
226
- async deleteThread({ threadId }: { threadId: string }): Promise<void> {
227
- this.logger.debug('Deleting thread', { threadId });
228
-
229
- try {
230
- // First, delete all messages associated with this thread
231
- const messages = await this.getMessages({ threadId });
232
- if (messages.length > 0) {
233
- // Delete messages in batches
234
- const batchSize = 25; // DynamoDB batch limits
235
- for (let i = 0; i < messages.length; i += batchSize) {
236
- const batch = messages.slice(i, i + batchSize);
237
- await Promise.all(
238
- batch.map(message =>
239
- this.service.entities.message
240
- .delete({
241
- entity: 'message',
242
- id: message.id,
243
- threadId: message.threadId,
244
- })
245
- .go(),
246
- ),
247
- );
248
- }
249
- }
250
-
251
- // Then delete the thread using the primary key
252
- await this.service.entities.thread.delete({ entity: 'thread', id: threadId }).go();
253
- } catch (error) {
254
- throw new MastraError(
255
- {
256
- id: 'STORAGE_DYNAMODB_STORE_DELETE_THREAD_FAILED',
257
- domain: ErrorDomain.STORAGE,
258
- category: ErrorCategory.THIRD_PARTY,
259
- details: { threadId },
260
- },
261
- error,
262
- );
263
- }
264
- }
265
-
266
- public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
267
- public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
268
- public async getMessages({
269
- threadId,
270
- resourceId,
271
- selectBy,
272
- format,
273
- }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
274
- this.logger.debug('Getting messages', { threadId, selectBy });
275
-
276
- try {
277
- const messages: MastraMessageV2[] = [];
278
- const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
279
-
280
- // Handle included messages first (like libsql)
281
- if (selectBy?.include?.length) {
282
- const includeMessages = await this._getIncludedMessages(threadId, selectBy);
283
- if (includeMessages) {
284
- messages.push(...includeMessages);
285
- }
286
- }
287
-
288
- // Get remaining messages only if limit is not 0
289
- if (limit !== 0) {
290
- // Query messages by thread ID using the GSI
291
- const query = this.service.entities.message.query.byThread({ entity: 'message', threadId });
292
-
293
- // Get messages from the main thread
294
- let results;
295
- if (limit !== Number.MAX_SAFE_INTEGER && limit > 0) {
296
- // Use limit in query to get only the last N messages
297
- results = await query.go({ limit, order: 'desc' });
298
- // Reverse the results since we want ascending order
299
- results.data = results.data.reverse();
300
- } else {
301
- // Get all messages
302
- results = await query.go();
303
- }
304
-
305
- let allThreadMessages = results.data
306
- .map((data: any) => this.parseMessageData(data))
307
- .filter((msg: any): msg is MastraMessageV2 => 'content' in msg);
308
-
309
- // Sort by createdAt ASC to get proper order
310
- allThreadMessages.sort((a: MastraMessageV2, b: MastraMessageV2) => {
311
- const timeA = a.createdAt.getTime();
312
- const timeB = b.createdAt.getTime();
313
- if (timeA === timeB) {
314
- return a.id.localeCompare(b.id);
315
- }
316
- return timeA - timeB;
317
- });
318
-
319
- messages.push(...allThreadMessages);
320
- }
321
-
322
- // Sort by createdAt ASC to match libsql behavior, with ID tiebreaker for stable ordering
323
- messages.sort((a, b) => {
324
- const timeA = a.createdAt.getTime();
325
- const timeB = b.createdAt.getTime();
326
- if (timeA === timeB) {
327
- return a.id.localeCompare(b.id);
328
- }
329
- return timeA - timeB;
330
- });
331
-
332
- // Deduplicate messages by ID (like libsql)
333
- const uniqueMessages = messages.filter(
334
- (message, index, self) => index === self.findIndex(m => m.id === message.id),
335
- );
336
-
337
- const list = new MessageList({ threadId, resourceId }).add(uniqueMessages, 'memory');
338
- if (format === `v2`) return list.get.all.v2();
339
- return list.get.all.v1();
340
- } catch (error) {
341
- throw new MastraError(
342
- {
343
- id: 'STORAGE_DYNAMODB_STORE_GET_MESSAGES_FAILED',
344
- domain: ErrorDomain.STORAGE,
345
- category: ErrorCategory.THIRD_PARTY,
346
- details: { threadId },
347
- },
348
- error,
349
- );
350
- }
351
- }
352
-
353
- public async getMessagesById({
354
- messageIds,
355
- format,
356
- }: {
357
- messageIds: string[];
358
- format: 'v1';
359
- }): Promise<MastraMessageV1[]>;
360
- public async getMessagesById({
361
- messageIds,
362
- format,
363
- }: {
364
- messageIds: string[];
365
- format?: 'v2';
366
- }): Promise<MastraMessageV2[]>;
367
- public async getMessagesById({
368
- messageIds,
369
- format,
370
- }: {
371
- messageIds: string[];
372
- format?: 'v1' | 'v2';
373
- }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
374
- this.logger.debug('Getting messages by ID', { messageIds });
375
- if (messageIds.length === 0) return [];
376
-
377
- try {
378
- const results = await Promise.all(
379
- messageIds.map(id => this.service.entities.message.query.primary({ entity: 'message', id }).go()),
380
- );
381
-
382
- const data = results.map(result => result.data).flat(1);
383
-
384
- let parsedMessages = data
385
- .map((data: any) => this.parseMessageData(data))
386
- .filter((msg: any): msg is MastraMessageV2 => 'content' in msg);
387
-
388
- // Deduplicate messages by ID (like libsql)
389
- const uniqueMessages = parsedMessages.filter(
390
- (message, index, self) => index === self.findIndex(m => m.id === message.id),
391
- );
392
-
393
- const list = new MessageList().add(uniqueMessages, 'memory');
394
- if (format === `v1`) return list.get.all.v1();
395
- return list.get.all.v2();
396
- } catch (error) {
397
- throw new MastraError(
398
- {
399
- id: 'STORAGE_DYNAMODB_STORE_GET_MESSAGES_BY_ID_FAILED',
400
- domain: ErrorDomain.STORAGE,
401
- category: ErrorCategory.THIRD_PARTY,
402
- details: { messageIds: JSON.stringify(messageIds) },
403
- },
404
- error,
405
- );
406
- }
407
- }
408
-
409
- async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
410
- async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
411
- async saveMessages(
412
- args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
413
- ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
414
- const { messages, format = 'v1' } = args;
415
- this.logger.debug('Saving messages', { count: messages.length });
416
-
417
- if (!messages.length) {
418
- return [];
419
- }
420
-
421
- const threadId = messages[0]?.threadId;
422
- if (!threadId) {
423
- throw new Error('Thread ID is required');
424
- }
425
-
426
- // Ensure 'entity' is added and complex fields are handled
427
- const messagesToSave = messages.map(msg => {
428
- const now = new Date().toISOString();
429
- return {
430
- entity: 'message', // Add entity type
431
- id: msg.id,
432
- threadId: msg.threadId,
433
- role: msg.role,
434
- type: msg.type,
435
- resourceId: msg.resourceId,
436
- // Ensure complex fields are stringified if not handled by attribute setters
437
- content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
438
- toolCallArgs: `toolCallArgs` in msg && msg.toolCallArgs ? JSON.stringify(msg.toolCallArgs) : undefined,
439
- toolCallIds: `toolCallIds` in msg && msg.toolCallIds ? JSON.stringify(msg.toolCallIds) : undefined,
440
- toolNames: `toolNames` in msg && msg.toolNames ? JSON.stringify(msg.toolNames) : undefined,
441
- createdAt: msg.createdAt instanceof Date ? msg.createdAt.toISOString() : msg.createdAt || now,
442
- updatedAt: now, // Add updatedAt
443
- };
444
- });
445
-
446
- try {
447
- // Process messages sequentially to enable rollback on error
448
- const savedMessageIds: string[] = [];
449
-
450
- for (const messageData of messagesToSave) {
451
- // Ensure each item has the entity property before sending
452
- if (!messageData.entity) {
453
- this.logger.error('Missing entity property in message data for create', { messageData });
454
- throw new Error('Internal error: Missing entity property during saveMessages');
455
- }
456
-
457
- try {
458
- await this.service.entities.message.put(messageData).go();
459
- savedMessageIds.push(messageData.id);
460
- } catch (error) {
461
- // Rollback: delete all previously saved messages
462
- for (const savedId of savedMessageIds) {
463
- try {
464
- await this.service.entities.message.delete({ entity: 'message', id: savedId }).go();
465
- } catch (rollbackError) {
466
- this.logger.error('Failed to rollback message during save error', {
467
- messageId: savedId,
468
- error: rollbackError,
469
- });
470
- }
471
- }
472
- throw error;
473
- }
474
- }
475
-
476
- // Update thread's updatedAt timestamp
477
- await this.service.entities.thread
478
- .update({ entity: 'thread', id: threadId })
479
- .set({
480
- updatedAt: new Date().toISOString(),
481
- })
482
- .go();
483
-
484
- const list = new MessageList().add(messages, 'memory');
485
- if (format === `v1`) return list.get.all.v1();
486
- return list.get.all.v2();
487
- } catch (error) {
488
- throw new MastraError(
489
- {
490
- id: 'STORAGE_DYNAMODB_STORE_SAVE_MESSAGES_FAILED',
491
- domain: ErrorDomain.STORAGE,
492
- category: ErrorCategory.THIRD_PARTY,
493
- details: { count: messages.length },
494
- },
495
- error,
496
- );
497
- }
498
- }
499
-
500
- async getThreadsByResourceIdPaginated(
501
- args: {
502
- resourceId: string;
503
- page?: number;
504
- perPage?: number;
505
- } & ThreadSortOptions,
506
- ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
507
- const { resourceId, page = 0, perPage = 100 } = args;
508
- const orderBy = this.castThreadOrderBy(args.orderBy);
509
- const sortDirection = this.castThreadSortDirection(args.sortDirection);
510
-
511
- this.logger.debug('Getting threads by resource ID with pagination', {
512
- resourceId,
513
- page,
514
- perPage,
515
- orderBy,
516
- sortDirection,
517
- });
518
-
519
- try {
520
- // Query threads by resource ID using the GSI
521
- const query = this.service.entities.thread.query.byResource({ entity: 'thread', resourceId });
522
-
523
- // Get all threads for this resource ID (DynamoDB doesn't support OFFSET/LIMIT)
524
- const results = await query.go();
525
-
526
- // Use shared helper method for transformation and sorting
527
- const allThreads = this.transformAndSortThreads(results.data, orderBy, sortDirection);
528
-
529
- // Apply pagination in memory
530
- const startIndex = page * perPage;
531
- const endIndex = startIndex + perPage;
532
- const paginatedThreads = allThreads.slice(startIndex, endIndex);
533
-
534
- // Calculate pagination info
535
- const total = allThreads.length;
536
- const hasMore = endIndex < total;
537
-
538
- return {
539
- threads: paginatedThreads,
540
- total,
541
- page,
542
- perPage,
543
- hasMore,
544
- };
545
- } catch (error) {
546
- throw new MastraError(
547
- {
548
- id: 'STORAGE_DYNAMODB_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
549
- domain: ErrorDomain.STORAGE,
550
- category: ErrorCategory.THIRD_PARTY,
551
- details: { resourceId, page, perPage },
552
- },
553
- error,
554
- );
555
- }
556
- }
557
-
558
- async getMessagesPaginated(
559
- args: StorageGetMessagesArg & { format?: 'v1' | 'v2' },
560
- ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
561
- const { threadId, resourceId, selectBy, format = 'v1' } = args;
562
- const { page = 0, perPage = 40, dateRange } = selectBy?.pagination || {};
563
- const fromDate = dateRange?.start;
564
- const toDate = dateRange?.end;
565
- const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
566
-
567
- this.logger.debug('Getting messages with pagination', { threadId, page, perPage, fromDate, toDate, limit });
568
-
569
- try {
570
- let messages: MastraMessageV2[] = [];
571
-
572
- // Handle include messages first
573
- if (selectBy?.include?.length) {
574
- const includeMessages = await this._getIncludedMessages(threadId, selectBy);
575
- if (includeMessages) {
576
- messages.push(...includeMessages);
577
- }
578
- }
579
-
580
- // Get remaining messages only if limit is not 0
581
- if (limit !== 0) {
582
- // Query messages by thread ID using the GSI
583
- const query = this.service.entities.message.query.byThread({ entity: 'message', threadId });
584
-
585
- // Get messages from the main thread
586
- let results;
587
- if (limit !== Number.MAX_SAFE_INTEGER && limit > 0) {
588
- // Use limit in query to get only the last N messages
589
- results = await query.go({ limit, order: 'desc' });
590
- // Reverse the results since we want ascending order
591
- results.data = results.data.reverse();
592
- } else {
593
- // Get all messages
594
- results = await query.go();
595
- }
596
-
597
- let allThreadMessages = results.data
598
- .map((data: any) => this.parseMessageData(data))
599
- .filter((msg: any): msg is MastraMessageV2 => 'content' in msg);
600
-
601
- // Sort by createdAt ASC to get proper order
602
- allThreadMessages.sort((a: MastraMessageV2, b: MastraMessageV2) => {
603
- const timeA = a.createdAt.getTime();
604
- const timeB = b.createdAt.getTime();
605
- if (timeA === timeB) {
606
- return a.id.localeCompare(b.id);
607
- }
608
- return timeA - timeB;
609
- });
610
-
611
- // Exclude already included messages
612
- const excludeIds = messages.map(m => m.id);
613
- if (excludeIds.length > 0) {
614
- allThreadMessages = allThreadMessages.filter((msg: MastraMessageV2) => !excludeIds.includes(msg.id));
615
- }
616
-
617
- messages.push(...allThreadMessages);
618
- }
619
-
620
- // Sort all messages by createdAt (oldest first for final result)
621
- messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
622
-
623
- // Apply date filtering if needed
624
- if (fromDate || toDate) {
625
- messages = messages.filter(msg => {
626
- const createdAt = new Date(msg.createdAt).getTime();
627
- if (fromDate && createdAt < new Date(fromDate).getTime()) return false;
628
- if (toDate && createdAt > new Date(toDate).getTime()) return false;
629
- return true;
630
- });
631
- }
632
-
633
- // Save total before pagination
634
- const total = messages.length;
635
-
636
- // Apply offset-based pagination in memory
637
- const start = page * perPage;
638
- const end = start + perPage;
639
- const paginatedMessages = messages.slice(start, end);
640
- const hasMore = end < total;
641
-
642
- const list = new MessageList({ threadId, resourceId }).add(paginatedMessages as MastraMessageV2[], 'memory');
643
- const finalMessages = format === 'v2' ? list.get.all.v2() : list.get.all.v1();
644
-
645
- return {
646
- messages: finalMessages,
647
- total,
648
- page,
649
- perPage,
650
- hasMore,
651
- };
652
- } catch (error) {
653
- throw new MastraError(
654
- {
655
- id: 'STORAGE_DYNAMODB_STORE_GET_MESSAGES_PAGINATED_FAILED',
656
- domain: ErrorDomain.STORAGE,
657
- category: ErrorCategory.THIRD_PARTY,
658
- details: { threadId },
659
- },
660
- error,
661
- );
662
- }
663
- }
664
-
665
- // Helper method to get included messages with context
666
- private async _getIncludedMessages(threadId: string, selectBy: any): Promise<MastraMessageV2[]> {
667
- if (!selectBy?.include?.length) {
668
- return [];
669
- }
670
-
671
- const includeMessages: MastraMessageV2[] = [];
672
-
673
- for (const includeItem of selectBy.include) {
674
- try {
675
- const { id, threadId: targetThreadId, withPreviousMessages = 0, withNextMessages = 0 } = includeItem;
676
- const searchThreadId = targetThreadId || threadId;
677
-
678
- this.logger.debug('Getting included messages for', {
679
- id,
680
- targetThreadId,
681
- searchThreadId,
682
- withPreviousMessages,
683
- withNextMessages,
684
- });
685
-
686
- // Get all messages for the target thread
687
- const query = this.service.entities.message.query.byThread({ entity: 'message', threadId: searchThreadId });
688
- const results = await query.go();
689
- const allMessages = results.data
690
- .map((data: any) => this.parseMessageData(data))
691
- .filter((msg: any): msg is MastraMessageV2 => 'content' in msg && typeof msg.content === 'object');
692
-
693
- this.logger.debug('Found messages in thread', {
694
- threadId: searchThreadId,
695
- messageCount: allMessages.length,
696
- messageIds: allMessages.map((m: MastraMessageV2) => m.id),
697
- });
698
-
699
- // Sort by createdAt ASC to get proper order, with ID tiebreaker for stable ordering
700
- allMessages.sort((a: MastraMessageV2, b: MastraMessageV2) => {
701
- const timeA = a.createdAt.getTime();
702
- const timeB = b.createdAt.getTime();
703
- if (timeA === timeB) {
704
- return a.id.localeCompare(b.id);
705
- }
706
- return timeA - timeB;
707
- });
708
-
709
- // Find the target message
710
- const targetIndex = allMessages.findIndex((msg: MastraMessageV2) => msg.id === id);
711
- if (targetIndex === -1) {
712
- this.logger.warn('Target message not found', { id, threadId: searchThreadId });
713
- continue;
714
- }
715
-
716
- this.logger.debug('Found target message at index', { id, targetIndex, totalMessages: allMessages.length });
717
-
718
- // Get context messages (previous and next)
719
- const startIndex = Math.max(0, targetIndex - withPreviousMessages);
720
- const endIndex = Math.min(allMessages.length, targetIndex + withNextMessages + 1);
721
- const contextMessages = allMessages.slice(startIndex, endIndex);
722
-
723
- this.logger.debug('Context messages', {
724
- startIndex,
725
- endIndex,
726
- contextCount: contextMessages.length,
727
- contextIds: contextMessages.map((m: MastraMessageV2) => m.id),
728
- });
729
-
730
- includeMessages.push(...contextMessages);
731
- } catch (error) {
732
- this.logger.warn('Failed to get included message', { messageId: includeItem.id, error });
733
- }
734
- }
735
-
736
- this.logger.debug('Total included messages', {
737
- count: includeMessages.length,
738
- ids: includeMessages.map((m: MastraMessageV2) => m.id),
739
- });
740
-
741
- return includeMessages;
742
- }
743
-
744
- async updateMessages(args: {
745
- messages: Partial<Omit<MastraMessageV2, 'createdAt'>> &
746
- {
747
- id: string;
748
- content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
749
- }[];
750
- }): Promise<MastraMessageV2[]> {
751
- const { messages } = args;
752
- this.logger.debug('Updating messages', { count: messages.length });
753
-
754
- if (!messages.length) {
755
- return [];
756
- }
757
-
758
- const updatedMessages: MastraMessageV2[] = [];
759
- const affectedThreadIds = new Set<string>();
760
-
761
- try {
762
- for (const updateData of messages) {
763
- const { id, ...updates } = updateData;
764
-
765
- // Get the existing message
766
- const existingMessage = await this.service.entities.message.get({ entity: 'message', id }).go();
767
- if (!existingMessage.data) {
768
- this.logger.warn('Message not found for update', { id });
769
- continue;
770
- }
771
-
772
- const existingMsg = this.parseMessageData(existingMessage.data) as MastraMessageV2;
773
- const originalThreadId = existingMsg.threadId;
774
- affectedThreadIds.add(originalThreadId!);
775
-
776
- // Prepare the update payload
777
- const updatePayload: any = {
778
- updatedAt: new Date().toISOString(),
779
- };
780
-
781
- // Handle basic field updates
782
- if ('role' in updates && updates.role !== undefined) updatePayload.role = updates.role;
783
- if ('type' in updates && updates.type !== undefined) updatePayload.type = updates.type;
784
- if ('resourceId' in updates && updates.resourceId !== undefined) updatePayload.resourceId = updates.resourceId;
785
- if ('threadId' in updates && updates.threadId !== undefined && updates.threadId !== null) {
786
- updatePayload.threadId = updates.threadId;
787
- affectedThreadIds.add(updates.threadId as string);
788
- }
789
-
790
- // Handle content updates
791
- if (updates.content) {
792
- const existingContent = existingMsg.content;
793
- let newContent = { ...existingContent };
794
-
795
- // Deep merge metadata if provided
796
- if (updates.content.metadata !== undefined) {
797
- newContent.metadata = {
798
- ...(existingContent.metadata || {}),
799
- ...(updates.content.metadata || {}),
800
- };
801
- }
802
-
803
- // Update content string if provided
804
- if (updates.content.content !== undefined) {
805
- newContent.content = updates.content.content;
806
- }
807
-
808
- // Update parts if provided (only if it exists in the content type)
809
- if ('parts' in updates.content && updates.content.parts !== undefined) {
810
- (newContent as any).parts = updates.content.parts;
811
- }
812
-
813
- updatePayload.content = JSON.stringify(newContent);
814
- }
815
-
816
- // Update the message
817
- await this.service.entities.message.update({ entity: 'message', id }).set(updatePayload).go();
818
-
819
- // Get the updated message
820
- const updatedMessage = await this.service.entities.message.get({ entity: 'message', id }).go();
821
- if (updatedMessage.data) {
822
- updatedMessages.push(this.parseMessageData(updatedMessage.data) as MastraMessageV2);
823
- }
824
- }
825
-
826
- // Update timestamps for all affected threads
827
- for (const threadId of affectedThreadIds) {
828
- await this.service.entities.thread
829
- .update({ entity: 'thread', id: threadId })
830
- .set({
831
- updatedAt: new Date().toISOString(),
832
- })
833
- .go();
834
- }
835
-
836
- return updatedMessages;
837
- } catch (error) {
838
- throw new MastraError(
839
- {
840
- id: 'STORAGE_DYNAMODB_STORE_UPDATE_MESSAGES_FAILED',
841
- domain: ErrorDomain.STORAGE,
842
- category: ErrorCategory.THIRD_PARTY,
843
- details: { count: messages.length },
844
- },
845
- error,
846
- );
847
- }
848
- }
849
-
850
- async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
851
- this.logger.debug('Getting resource by ID', { resourceId });
852
- try {
853
- const result = await this.service.entities.resource.get({ entity: 'resource', id: resourceId }).go();
854
-
855
- if (!result.data) {
856
- return null;
857
- }
858
-
859
- // ElectroDB handles the transformation with attribute getters
860
- const data = result.data;
861
- return {
862
- ...data,
863
- // Convert date strings back to Date objects for consistency
864
- createdAt: typeof data.createdAt === 'string' ? new Date(data.createdAt) : data.createdAt,
865
- updatedAt: typeof data.updatedAt === 'string' ? new Date(data.updatedAt) : data.updatedAt,
866
- // Ensure workingMemory is always returned as a string, regardless of automatic parsing
867
- workingMemory: typeof data.workingMemory === 'object' ? JSON.stringify(data.workingMemory) : data.workingMemory,
868
- // metadata is already transformed by the entity's getter
869
- } as StorageResourceType;
870
- } catch (error) {
871
- throw new MastraError(
872
- {
873
- id: 'STORAGE_DYNAMODB_STORE_GET_RESOURCE_BY_ID_FAILED',
874
- domain: ErrorDomain.STORAGE,
875
- category: ErrorCategory.THIRD_PARTY,
876
- details: { resourceId },
877
- },
878
- error,
879
- );
880
- }
881
- }
882
-
883
- async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
884
- this.logger.debug('Saving resource', { resourceId: resource.id });
885
-
886
- const now = new Date();
887
-
888
- const resourceData = {
889
- entity: 'resource',
890
- id: resource.id,
891
- workingMemory: resource.workingMemory,
892
- metadata: resource.metadata ? JSON.stringify(resource.metadata) : undefined,
893
- createdAt: resource.createdAt?.toISOString() || now.toISOString(),
894
- updatedAt: now.toISOString(),
895
- };
896
-
897
- try {
898
- await this.service.entities.resource.upsert(resourceData).go();
899
-
900
- return {
901
- id: resource.id,
902
- workingMemory: resource.workingMemory,
903
- metadata: resource.metadata,
904
- createdAt: resource.createdAt || now,
905
- updatedAt: now,
906
- };
907
- } catch (error) {
908
- throw new MastraError(
909
- {
910
- id: 'STORAGE_DYNAMODB_STORE_SAVE_RESOURCE_FAILED',
911
- domain: ErrorDomain.STORAGE,
912
- category: ErrorCategory.THIRD_PARTY,
913
- details: { resourceId: resource.id },
914
- },
915
- error,
916
- );
917
- }
918
- }
919
-
920
- async updateResource({
921
- resourceId,
922
- workingMemory,
923
- metadata,
924
- }: {
925
- resourceId: string;
926
- workingMemory?: string;
927
- metadata?: Record<string, unknown>;
928
- }): Promise<StorageResourceType> {
929
- this.logger.debug('Updating resource', { resourceId });
930
-
931
- try {
932
- // First, get the existing resource to merge with updates
933
- const existingResource = await this.getResourceById({ resourceId });
934
-
935
- if (!existingResource) {
936
- // Create new resource if it doesn't exist
937
- const newResource: StorageResourceType = {
938
- id: resourceId,
939
- workingMemory,
940
- metadata: metadata || {},
941
- createdAt: new Date(),
942
- updatedAt: new Date(),
943
- };
944
- return this.saveResource({ resource: newResource });
945
- }
946
-
947
- const now = new Date();
948
-
949
- // Prepare the update
950
- const updateData: any = {
951
- updatedAt: now.toISOString(),
952
- };
953
-
954
- if (workingMemory !== undefined) {
955
- updateData.workingMemory = workingMemory;
956
- }
957
-
958
- if (metadata) {
959
- // Merge with existing metadata instead of overwriting
960
- const existingMetadata = existingResource.metadata || {};
961
- const mergedMetadata = { ...existingMetadata, ...metadata };
962
- updateData.metadata = JSON.stringify(mergedMetadata);
963
- }
964
-
965
- // Update the resource using the primary key
966
- await this.service.entities.resource.update({ entity: 'resource', id: resourceId }).set(updateData).go();
967
-
968
- // Return the updated resource object
969
- return {
970
- ...existingResource,
971
- workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
972
- metadata: metadata ? { ...existingResource.metadata, ...metadata } : existingResource.metadata,
973
- updatedAt: now,
974
- };
975
- } catch (error) {
976
- throw new MastraError(
977
- {
978
- id: 'STORAGE_DYNAMODB_STORE_UPDATE_RESOURCE_FAILED',
979
- domain: ErrorDomain.STORAGE,
980
- category: ErrorCategory.THIRD_PARTY,
981
- details: { resourceId },
982
- },
983
- error,
984
- );
985
- }
986
- }
987
- }