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