@mastra/libsql 0.13.7 → 0.13.8-alpha.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.
@@ -1,937 +0,0 @@
1
- import type { Client, InValue } from '@libsql/client';
2
- import type { MastraMessageContentV2 } from '@mastra/core/agent';
3
- import { MessageList } from '@mastra/core/agent';
4
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
5
- import type { MastraMessageV1, MastraMessageV2, StorageThreadType } from '@mastra/core/memory';
6
- import type {
7
- PaginationInfo,
8
- StorageGetMessagesArg,
9
- StorageResourceType,
10
- ThreadSortOptions,
11
- } from '@mastra/core/storage';
12
- import {
13
- MemoryStorage,
14
- resolveMessageLimit,
15
- TABLE_MESSAGES,
16
- TABLE_RESOURCES,
17
- TABLE_THREADS,
18
- } from '@mastra/core/storage';
19
- import { parseSqlIdentifier } from '@mastra/core/utils';
20
- import type { StoreOperationsLibSQL } from '../operations';
21
-
22
- export class MemoryLibSQL extends MemoryStorage {
23
- private client: Client;
24
- private operations: StoreOperationsLibSQL;
25
- constructor({ client, operations }: { client: Client; operations: StoreOperationsLibSQL }) {
26
- super();
27
- this.client = client;
28
- this.operations = operations;
29
- }
30
-
31
- private parseRow(row: any): MastraMessageV2 {
32
- let content = row.content;
33
- try {
34
- content = JSON.parse(row.content);
35
- } catch {
36
- // use content as is if it's not JSON
37
- }
38
- const result = {
39
- id: row.id,
40
- content,
41
- role: row.role,
42
- createdAt: new Date(row.createdAt as string),
43
- threadId: row.thread_id,
44
- resourceId: row.resourceId,
45
- } as MastraMessageV2;
46
- if (row.type && row.type !== `v2`) result.type = row.type;
47
- return result;
48
- }
49
-
50
- private async _getIncludedMessages({
51
- threadId,
52
- selectBy,
53
- }: {
54
- threadId: string;
55
- selectBy: StorageGetMessagesArg['selectBy'];
56
- }) {
57
- const include = selectBy?.include;
58
- if (!include) return null;
59
-
60
- const unionQueries: string[] = [];
61
- const params: any[] = [];
62
-
63
- for (const inc of include) {
64
- const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
65
- // if threadId is provided, use it, otherwise use threadId from args
66
- const searchId = inc.threadId || threadId;
67
- unionQueries.push(
68
- `
69
- SELECT * FROM (
70
- WITH numbered_messages AS (
71
- SELECT
72
- id, content, role, type, "createdAt", thread_id, "resourceId",
73
- ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
74
- FROM "${TABLE_MESSAGES}"
75
- WHERE thread_id = ?
76
- ),
77
- target_positions AS (
78
- SELECT row_num as target_pos
79
- FROM numbered_messages
80
- WHERE id = ?
81
- )
82
- SELECT DISTINCT m.*
83
- FROM numbered_messages m
84
- CROSS JOIN target_positions t
85
- WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
86
- )
87
- `, // Keep ASC for final sorting after fetching context
88
- );
89
- params.push(searchId, id, withPreviousMessages, withNextMessages);
90
- }
91
- const finalQuery = unionQueries.join(' UNION ALL ') + ' ORDER BY "createdAt" ASC';
92
- const includedResult = await this.client.execute({ sql: finalQuery, args: params });
93
- const includedRows = includedResult.rows?.map(row => this.parseRow(row));
94
- const seen = new Set<string>();
95
- const dedupedRows = includedRows.filter(row => {
96
- if (seen.has(row.id)) return false;
97
- seen.add(row.id);
98
- return true;
99
- });
100
- return dedupedRows;
101
- }
102
-
103
- /**
104
- * @deprecated use getMessagesPaginated instead for paginated results.
105
- */
106
- public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
107
- public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
108
- public async getMessages({
109
- threadId,
110
- selectBy,
111
- format,
112
- }: StorageGetMessagesArg & {
113
- format?: 'v1' | 'v2';
114
- }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
115
- try {
116
- const messages: MastraMessageV2[] = [];
117
- const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
118
- if (selectBy?.include?.length) {
119
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy });
120
- if (includeMessages) {
121
- messages.push(...includeMessages);
122
- }
123
- }
124
-
125
- const excludeIds = messages.map(m => m.id);
126
- const remainingSql = `
127
- SELECT
128
- id,
129
- content,
130
- role,
131
- type,
132
- "createdAt",
133
- thread_id,
134
- "resourceId"
135
- FROM "${TABLE_MESSAGES}"
136
- WHERE thread_id = ?
137
- ${excludeIds.length ? `AND id NOT IN (${excludeIds.map(() => '?').join(', ')})` : ''}
138
- ORDER BY "createdAt" DESC
139
- LIMIT ?
140
- `;
141
- const remainingArgs = [threadId, ...(excludeIds.length ? excludeIds : []), limit];
142
- const remainingResult = await this.client.execute({ sql: remainingSql, args: remainingArgs });
143
- if (remainingResult.rows) {
144
- messages.push(...remainingResult.rows.map((row: any) => this.parseRow(row)));
145
- }
146
- messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
147
- const list = new MessageList().add(messages, 'memory');
148
- if (format === `v2`) return list.get.all.v2();
149
- return list.get.all.v1();
150
- } catch (error) {
151
- throw new MastraError(
152
- {
153
- id: 'LIBSQL_STORE_GET_MESSAGES_FAILED',
154
- domain: ErrorDomain.STORAGE,
155
- category: ErrorCategory.THIRD_PARTY,
156
- details: { threadId },
157
- },
158
- error,
159
- );
160
- }
161
- }
162
-
163
- public async getMessagesById({
164
- messageIds,
165
- format,
166
- }: {
167
- messageIds: string[];
168
- format: 'v1';
169
- }): Promise<MastraMessageV1[]>;
170
- public async getMessagesById({
171
- messageIds,
172
- format,
173
- }: {
174
- messageIds: string[];
175
- format?: 'v2';
176
- }): Promise<MastraMessageV2[]>;
177
- public async getMessagesById({
178
- messageIds,
179
- format,
180
- }: {
181
- messageIds: string[];
182
- format?: 'v1' | 'v2';
183
- }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
184
- if (messageIds.length === 0) return [];
185
-
186
- try {
187
- const sql = `
188
- SELECT
189
- id,
190
- content,
191
- role,
192
- type,
193
- "createdAt",
194
- thread_id,
195
- "resourceId"
196
- FROM "${TABLE_MESSAGES}"
197
- WHERE id IN (${messageIds.map(() => '?').join(', ')})
198
- ORDER BY "createdAt" DESC
199
- `;
200
- const result = await this.client.execute({ sql, args: messageIds });
201
- if (!result.rows) return [];
202
-
203
- const list = new MessageList().add(result.rows.map(this.parseRow), 'memory');
204
- if (format === `v1`) return list.get.all.v1();
205
- return list.get.all.v2();
206
- } catch (error) {
207
- throw new MastraError(
208
- {
209
- id: 'LIBSQL_STORE_GET_MESSAGES_BY_ID_FAILED',
210
- domain: ErrorDomain.STORAGE,
211
- category: ErrorCategory.THIRD_PARTY,
212
- details: { messageIds: JSON.stringify(messageIds) },
213
- },
214
- error,
215
- );
216
- }
217
- }
218
-
219
- public async getMessagesPaginated(
220
- args: StorageGetMessagesArg & {
221
- format?: 'v1' | 'v2';
222
- },
223
- ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
224
- const { threadId, format, selectBy } = args;
225
- const { page = 0, perPage: perPageInput, dateRange } = selectBy?.pagination || {};
226
- const perPage =
227
- perPageInput !== undefined ? perPageInput : resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
228
- const fromDate = dateRange?.start;
229
- const toDate = dateRange?.end;
230
-
231
- const messages: MastraMessageV2[] = [];
232
-
233
- if (selectBy?.include?.length) {
234
- try {
235
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy });
236
- if (includeMessages) {
237
- messages.push(...includeMessages);
238
- }
239
- } catch (error) {
240
- throw new MastraError(
241
- {
242
- id: 'LIBSQL_STORE_GET_MESSAGES_PAGINATED_GET_INCLUDE_MESSAGES_FAILED',
243
- domain: ErrorDomain.STORAGE,
244
- category: ErrorCategory.THIRD_PARTY,
245
- details: { threadId },
246
- },
247
- error,
248
- );
249
- }
250
- }
251
-
252
- try {
253
- const currentOffset = page * perPage;
254
-
255
- const conditions: string[] = [`thread_id = ?`];
256
- const queryParams: InValue[] = [threadId];
257
-
258
- if (fromDate) {
259
- conditions.push(`"createdAt" >= ?`);
260
- queryParams.push(fromDate.toISOString());
261
- }
262
- if (toDate) {
263
- conditions.push(`"createdAt" <= ?`);
264
- queryParams.push(toDate.toISOString());
265
- }
266
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
267
-
268
- const countResult = await this.client.execute({
269
- sql: `SELECT COUNT(*) as count FROM ${TABLE_MESSAGES} ${whereClause}`,
270
- args: queryParams,
271
- });
272
- const total = Number(countResult.rows?.[0]?.count ?? 0);
273
-
274
- if (total === 0 && messages.length === 0) {
275
- return {
276
- messages: [],
277
- total: 0,
278
- page,
279
- perPage,
280
- hasMore: false,
281
- };
282
- }
283
-
284
- const excludeIds = messages.map(m => m.id);
285
- const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + queryParams.length + 1}`).join(', ');
286
-
287
- const dataResult = await this.client.execute({
288
- sql: `SELECT id, content, role, type, "createdAt", "resourceId", "thread_id" FROM ${TABLE_MESSAGES} ${whereClause} ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ''} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`,
289
- args: [...queryParams, ...excludeIds, perPage, currentOffset],
290
- });
291
-
292
- messages.push(...(dataResult.rows || []).map((row: any) => this.parseRow(row)));
293
-
294
- const messagesToReturn =
295
- format === 'v1'
296
- ? new MessageList().add(messages, 'memory').get.all.v1()
297
- : new MessageList().add(messages, 'memory').get.all.v2();
298
-
299
- return {
300
- messages: messagesToReturn,
301
- total,
302
- page,
303
- perPage,
304
- hasMore: currentOffset + messages.length < total,
305
- };
306
- } catch (error) {
307
- const mastraError = new MastraError(
308
- {
309
- id: 'LIBSQL_STORE_GET_MESSAGES_PAGINATED_FAILED',
310
- domain: ErrorDomain.STORAGE,
311
- category: ErrorCategory.THIRD_PARTY,
312
- details: { threadId },
313
- },
314
- error,
315
- );
316
- this.logger?.trackException?.(mastraError);
317
- this.logger?.error?.(mastraError.toString());
318
- return { messages: [], total: 0, page, perPage, hasMore: false };
319
- }
320
- }
321
-
322
- async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
323
- async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
324
- async saveMessages({
325
- messages,
326
- format,
327
- }:
328
- | { messages: MastraMessageV1[]; format?: undefined | 'v1' }
329
- | { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[] | MastraMessageV1[]> {
330
- if (messages.length === 0) return messages;
331
-
332
- try {
333
- const threadId = messages[0]?.threadId;
334
- if (!threadId) {
335
- throw new Error('Thread ID is required');
336
- }
337
-
338
- // Prepare batch statements for all messages
339
- const batchStatements = messages.map(message => {
340
- const time = message.createdAt || new Date();
341
- if (!message.threadId) {
342
- throw new Error(
343
- `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`,
344
- );
345
- }
346
- if (!message.resourceId) {
347
- throw new Error(
348
- `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`,
349
- );
350
- }
351
- return {
352
- sql: `INSERT INTO "${TABLE_MESSAGES}" (id, thread_id, content, role, type, "createdAt", "resourceId")
353
- VALUES (?, ?, ?, ?, ?, ?, ?)
354
- ON CONFLICT(id) DO UPDATE SET
355
- thread_id=excluded.thread_id,
356
- content=excluded.content,
357
- role=excluded.role,
358
- type=excluded.type,
359
- "resourceId"=excluded."resourceId"
360
- `,
361
- args: [
362
- message.id,
363
- message.threadId!,
364
- typeof message.content === 'object' ? JSON.stringify(message.content) : message.content,
365
- message.role,
366
- message.type || 'v2',
367
- time instanceof Date ? time.toISOString() : time,
368
- message.resourceId,
369
- ],
370
- };
371
- });
372
-
373
- const now = new Date().toISOString();
374
- batchStatements.push({
375
- sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
376
- args: [now, threadId],
377
- });
378
-
379
- // Execute in batches to avoid potential limitations
380
- const BATCH_SIZE = 50; // Safe batch size for libsql
381
-
382
- // Separate message statements from thread update
383
- const messageStatements = batchStatements.slice(0, -1);
384
- const threadUpdateStatement = batchStatements[batchStatements.length - 1];
385
-
386
- // Process message statements in batches
387
- for (let i = 0; i < messageStatements.length; i += BATCH_SIZE) {
388
- const batch = messageStatements.slice(i, i + BATCH_SIZE);
389
- if (batch.length > 0) {
390
- await this.client.batch(batch, 'write');
391
- }
392
- }
393
-
394
- // Execute thread update separately
395
- if (threadUpdateStatement) {
396
- await this.client.execute(threadUpdateStatement);
397
- }
398
-
399
- const list = new MessageList().add(messages, 'memory');
400
- if (format === `v2`) return list.get.all.v2();
401
- return list.get.all.v1();
402
- } catch (error) {
403
- throw new MastraError(
404
- {
405
- id: 'LIBSQL_STORE_SAVE_MESSAGES_FAILED',
406
- domain: ErrorDomain.STORAGE,
407
- category: ErrorCategory.THIRD_PARTY,
408
- },
409
- error,
410
- );
411
- }
412
- }
413
-
414
- async updateMessages({
415
- messages,
416
- }: {
417
- messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
418
- id: string;
419
- content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
420
- })[];
421
- }): Promise<MastraMessageV2[]> {
422
- if (messages.length === 0) {
423
- return [];
424
- }
425
-
426
- const messageIds = messages.map(m => m.id);
427
- const placeholders = messageIds.map(() => '?').join(',');
428
-
429
- const selectSql = `SELECT * FROM ${TABLE_MESSAGES} WHERE id IN (${placeholders})`;
430
- const existingResult = await this.client.execute({ sql: selectSql, args: messageIds });
431
- const existingMessages: MastraMessageV2[] = existingResult.rows.map(row => this.parseRow(row));
432
-
433
- if (existingMessages.length === 0) {
434
- return [];
435
- }
436
-
437
- const batchStatements = [];
438
- const threadIdsToUpdate = new Set<string>();
439
- const columnMapping: Record<string, string> = {
440
- threadId: 'thread_id',
441
- };
442
-
443
- for (const existingMessage of existingMessages) {
444
- const updatePayload = messages.find(m => m.id === existingMessage.id);
445
- if (!updatePayload) continue;
446
-
447
- const { id, ...fieldsToUpdate } = updatePayload;
448
- if (Object.keys(fieldsToUpdate).length === 0) continue;
449
-
450
- threadIdsToUpdate.add(existingMessage.threadId!);
451
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
452
- threadIdsToUpdate.add(updatePayload.threadId);
453
- }
454
-
455
- const setClauses = [];
456
- const args: InValue[] = [];
457
- const updatableFields = { ...fieldsToUpdate };
458
-
459
- // Special handling for the 'content' field to merge instead of overwrite
460
- if (updatableFields.content) {
461
- const newContent = {
462
- ...existingMessage.content,
463
- ...updatableFields.content,
464
- // Deep merge metadata if it exists on both
465
- ...(existingMessage.content?.metadata && updatableFields.content.metadata
466
- ? {
467
- metadata: {
468
- ...existingMessage.content.metadata,
469
- ...updatableFields.content.metadata,
470
- },
471
- }
472
- : {}),
473
- };
474
- setClauses.push(`${parseSqlIdentifier('content', 'column name')} = ?`);
475
- args.push(JSON.stringify(newContent));
476
- delete updatableFields.content;
477
- }
478
-
479
- for (const key in updatableFields) {
480
- if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
481
- const dbKey = columnMapping[key] || key;
482
- setClauses.push(`${parseSqlIdentifier(dbKey, 'column name')} = ?`);
483
- let value = updatableFields[key as keyof typeof updatableFields];
484
-
485
- if (typeof value === 'object' && value !== null) {
486
- value = JSON.stringify(value);
487
- }
488
- args.push(value as InValue);
489
- }
490
- }
491
-
492
- if (setClauses.length === 0) continue;
493
-
494
- args.push(id);
495
-
496
- const sql = `UPDATE ${TABLE_MESSAGES} SET ${setClauses.join(', ')} WHERE id = ?`;
497
- batchStatements.push({ sql, args });
498
- }
499
-
500
- if (batchStatements.length === 0) {
501
- return existingMessages;
502
- }
503
-
504
- const now = new Date().toISOString();
505
- for (const threadId of threadIdsToUpdate) {
506
- if (threadId) {
507
- batchStatements.push({
508
- sql: `UPDATE ${TABLE_THREADS} SET updatedAt = ? WHERE id = ?`,
509
- args: [now, threadId],
510
- });
511
- }
512
- }
513
-
514
- await this.client.batch(batchStatements, 'write');
515
-
516
- const updatedResult = await this.client.execute({ sql: selectSql, args: messageIds });
517
- return updatedResult.rows.map(row => this.parseRow(row));
518
- }
519
-
520
- async deleteMessages(messageIds: string[]): Promise<void> {
521
- if (!messageIds || messageIds.length === 0) {
522
- return;
523
- }
524
-
525
- try {
526
- // Process in batches to avoid SQL parameter limits
527
- const BATCH_SIZE = 100;
528
- const threadIds = new Set<string>();
529
-
530
- // Use a transaction to ensure consistency
531
- const tx = await this.client.transaction('write');
532
-
533
- try {
534
- for (let i = 0; i < messageIds.length; i += BATCH_SIZE) {
535
- const batch = messageIds.slice(i, i + BATCH_SIZE);
536
- const placeholders = batch.map(() => '?').join(',');
537
-
538
- // Get thread IDs for this batch
539
- const result = await tx.execute({
540
- sql: `SELECT DISTINCT thread_id FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
541
- args: batch,
542
- });
543
-
544
- result.rows?.forEach(row => {
545
- if (row.thread_id) threadIds.add(row.thread_id as string);
546
- });
547
-
548
- // Delete messages in this batch
549
- await tx.execute({
550
- sql: `DELETE FROM "${TABLE_MESSAGES}" WHERE id IN (${placeholders})`,
551
- args: batch,
552
- });
553
- }
554
-
555
- // Update thread timestamps within the transaction
556
- if (threadIds.size > 0) {
557
- const now = new Date().toISOString();
558
- for (const threadId of threadIds) {
559
- await tx.execute({
560
- sql: `UPDATE "${TABLE_THREADS}" SET "updatedAt" = ? WHERE id = ?`,
561
- args: [now, threadId],
562
- });
563
- }
564
- }
565
-
566
- // Commit the transaction
567
- await tx.commit();
568
- } catch (error) {
569
- // Rollback on error
570
- await tx.rollback();
571
- throw error;
572
- }
573
-
574
- // TODO: Delete from vector store if semantic recall is enabled
575
- } catch (error) {
576
- throw new MastraError(
577
- {
578
- id: 'LIBSQL_STORE_DELETE_MESSAGES_FAILED',
579
- domain: ErrorDomain.STORAGE,
580
- category: ErrorCategory.THIRD_PARTY,
581
- details: { messageIds: messageIds.join(', ') },
582
- },
583
- error,
584
- );
585
- }
586
- }
587
-
588
- async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
589
- const result = await this.operations.load<StorageResourceType>({
590
- tableName: TABLE_RESOURCES,
591
- keys: { id: resourceId },
592
- });
593
-
594
- if (!result) {
595
- return null;
596
- }
597
-
598
- return {
599
- ...result,
600
- // Ensure workingMemory is always returned as a string, even if auto-parsed as JSON
601
- workingMemory:
602
- result.workingMemory && typeof result.workingMemory === 'object'
603
- ? JSON.stringify(result.workingMemory)
604
- : result.workingMemory,
605
- metadata: typeof result.metadata === 'string' ? JSON.parse(result.metadata) : result.metadata,
606
- createdAt: new Date(result.createdAt),
607
- updatedAt: new Date(result.updatedAt),
608
- };
609
- }
610
-
611
- async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
612
- await this.operations.insert({
613
- tableName: TABLE_RESOURCES,
614
- record: {
615
- ...resource,
616
- metadata: JSON.stringify(resource.metadata),
617
- },
618
- });
619
-
620
- return resource;
621
- }
622
-
623
- async updateResource({
624
- resourceId,
625
- workingMemory,
626
- metadata,
627
- }: {
628
- resourceId: string;
629
- workingMemory?: string;
630
- metadata?: Record<string, unknown>;
631
- }): Promise<StorageResourceType> {
632
- const existingResource = await this.getResourceById({ resourceId });
633
-
634
- if (!existingResource) {
635
- // Create new resource if it doesn't exist
636
- const newResource: StorageResourceType = {
637
- id: resourceId,
638
- workingMemory,
639
- metadata: metadata || {},
640
- createdAt: new Date(),
641
- updatedAt: new Date(),
642
- };
643
- return this.saveResource({ resource: newResource });
644
- }
645
-
646
- const updatedResource = {
647
- ...existingResource,
648
- workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
649
- metadata: {
650
- ...existingResource.metadata,
651
- ...metadata,
652
- },
653
- updatedAt: new Date(),
654
- };
655
-
656
- const updates: string[] = [];
657
- const values: InValue[] = [];
658
-
659
- if (workingMemory !== undefined) {
660
- updates.push('workingMemory = ?');
661
- values.push(workingMemory);
662
- }
663
-
664
- if (metadata) {
665
- updates.push('metadata = ?');
666
- values.push(JSON.stringify(updatedResource.metadata));
667
- }
668
-
669
- updates.push('updatedAt = ?');
670
- values.push(updatedResource.updatedAt.toISOString());
671
-
672
- values.push(resourceId);
673
-
674
- await this.client.execute({
675
- sql: `UPDATE ${TABLE_RESOURCES} SET ${updates.join(', ')} WHERE id = ?`,
676
- args: values,
677
- });
678
-
679
- return updatedResource;
680
- }
681
-
682
- async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
683
- try {
684
- const result = await this.operations.load<
685
- Omit<StorageThreadType, 'createdAt' | 'updatedAt'> & { createdAt: string; updatedAt: string }
686
- >({
687
- tableName: TABLE_THREADS,
688
- keys: { id: threadId },
689
- });
690
-
691
- if (!result) {
692
- return null;
693
- }
694
-
695
- return {
696
- ...result,
697
- metadata: typeof result.metadata === 'string' ? JSON.parse(result.metadata) : result.metadata,
698
- createdAt: new Date(result.createdAt),
699
- updatedAt: new Date(result.updatedAt),
700
- };
701
- } catch (error) {
702
- throw new MastraError(
703
- {
704
- id: 'LIBSQL_STORE_GET_THREAD_BY_ID_FAILED',
705
- domain: ErrorDomain.STORAGE,
706
- category: ErrorCategory.THIRD_PARTY,
707
- details: { threadId },
708
- },
709
- error,
710
- );
711
- }
712
- }
713
-
714
- /**
715
- * @deprecated use getThreadsByResourceIdPaginated instead for paginated results.
716
- */
717
- public async getThreadsByResourceId(args: { resourceId: string } & ThreadSortOptions): Promise<StorageThreadType[]> {
718
- const resourceId = args.resourceId;
719
- const orderBy = this.castThreadOrderBy(args.orderBy);
720
- const sortDirection = this.castThreadSortDirection(args.sortDirection);
721
-
722
- try {
723
- const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
724
- const queryParams: InValue[] = [resourceId];
725
-
726
- const mapRowToStorageThreadType = (row: any): StorageThreadType => ({
727
- id: row.id as string,
728
- resourceId: row.resourceId as string,
729
- title: row.title as string,
730
- createdAt: new Date(row.createdAt as string), // Convert string to Date
731
- updatedAt: new Date(row.updatedAt as string), // Convert string to Date
732
- metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
733
- });
734
-
735
- // Non-paginated path
736
- const result = await this.client.execute({
737
- sql: `SELECT * ${baseQuery} ORDER BY ${orderBy} ${sortDirection}`,
738
- args: queryParams,
739
- });
740
-
741
- if (!result.rows) {
742
- return [];
743
- }
744
- return result.rows.map(mapRowToStorageThreadType);
745
- } catch (error) {
746
- const mastraError = new MastraError(
747
- {
748
- id: 'LIBSQL_STORE_GET_THREADS_BY_RESOURCE_ID_FAILED',
749
- domain: ErrorDomain.STORAGE,
750
- category: ErrorCategory.THIRD_PARTY,
751
- details: { resourceId },
752
- },
753
- error,
754
- );
755
- this.logger?.trackException?.(mastraError);
756
- this.logger?.error?.(mastraError.toString());
757
- return [];
758
- }
759
- }
760
-
761
- public async getThreadsByResourceIdPaginated(
762
- args: {
763
- resourceId: string;
764
- page: number;
765
- perPage: number;
766
- } & ThreadSortOptions,
767
- ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
768
- const { resourceId, page = 0, perPage = 100 } = args;
769
- const orderBy = this.castThreadOrderBy(args.orderBy);
770
- const sortDirection = this.castThreadSortDirection(args.sortDirection);
771
-
772
- try {
773
- const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
774
- const queryParams: InValue[] = [resourceId];
775
-
776
- const mapRowToStorageThreadType = (row: any): StorageThreadType => ({
777
- id: row.id as string,
778
- resourceId: row.resourceId as string,
779
- title: row.title as string,
780
- createdAt: new Date(row.createdAt as string), // Convert string to Date
781
- updatedAt: new Date(row.updatedAt as string), // Convert string to Date
782
- metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
783
- });
784
-
785
- const currentOffset = page * perPage;
786
-
787
- const countResult = await this.client.execute({
788
- sql: `SELECT COUNT(*) as count ${baseQuery}`,
789
- args: queryParams,
790
- });
791
- const total = Number(countResult.rows?.[0]?.count ?? 0);
792
-
793
- if (total === 0) {
794
- return {
795
- threads: [],
796
- total: 0,
797
- page,
798
- perPage,
799
- hasMore: false,
800
- };
801
- }
802
-
803
- const dataResult = await this.client.execute({
804
- sql: `SELECT * ${baseQuery} ORDER BY ${orderBy} ${sortDirection} LIMIT ? OFFSET ?`,
805
- args: [...queryParams, perPage, currentOffset],
806
- });
807
-
808
- const threads = (dataResult.rows || []).map(mapRowToStorageThreadType);
809
-
810
- return {
811
- threads,
812
- total,
813
- page,
814
- perPage,
815
- hasMore: currentOffset + threads.length < total,
816
- };
817
- } catch (error) {
818
- const mastraError = new MastraError(
819
- {
820
- id: 'LIBSQL_STORE_GET_THREADS_BY_RESOURCE_ID_FAILED',
821
- domain: ErrorDomain.STORAGE,
822
- category: ErrorCategory.THIRD_PARTY,
823
- details: { resourceId },
824
- },
825
- error,
826
- );
827
- this.logger?.trackException?.(mastraError);
828
- this.logger?.error?.(mastraError.toString());
829
- return { threads: [], total: 0, page, perPage, hasMore: false };
830
- }
831
- }
832
-
833
- async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
834
- try {
835
- await this.operations.insert({
836
- tableName: TABLE_THREADS,
837
- record: {
838
- ...thread,
839
- metadata: JSON.stringify(thread.metadata),
840
- },
841
- });
842
-
843
- return thread;
844
- } catch (error) {
845
- const mastraError = new MastraError(
846
- {
847
- id: 'LIBSQL_STORE_SAVE_THREAD_FAILED',
848
- domain: ErrorDomain.STORAGE,
849
- category: ErrorCategory.THIRD_PARTY,
850
- details: { threadId: thread.id },
851
- },
852
- error,
853
- );
854
- this.logger?.trackException?.(mastraError);
855
- this.logger?.error?.(mastraError.toString());
856
- throw mastraError;
857
- }
858
- }
859
-
860
- async updateThread({
861
- id,
862
- title,
863
- metadata,
864
- }: {
865
- id: string;
866
- title: string;
867
- metadata: Record<string, unknown>;
868
- }): Promise<StorageThreadType> {
869
- const thread = await this.getThreadById({ threadId: id });
870
- if (!thread) {
871
- throw new MastraError({
872
- id: 'LIBSQL_STORE_UPDATE_THREAD_FAILED_THREAD_NOT_FOUND',
873
- domain: ErrorDomain.STORAGE,
874
- category: ErrorCategory.USER,
875
- text: `Thread ${id} not found`,
876
- details: {
877
- status: 404,
878
- threadId: id,
879
- },
880
- });
881
- }
882
-
883
- const updatedThread = {
884
- ...thread,
885
- title,
886
- metadata: {
887
- ...thread.metadata,
888
- ...metadata,
889
- },
890
- };
891
-
892
- try {
893
- await this.client.execute({
894
- sql: `UPDATE ${TABLE_THREADS} SET title = ?, metadata = ? WHERE id = ?`,
895
- args: [title, JSON.stringify(updatedThread.metadata), id],
896
- });
897
-
898
- return updatedThread;
899
- } catch (error) {
900
- throw new MastraError(
901
- {
902
- id: 'LIBSQL_STORE_UPDATE_THREAD_FAILED',
903
- domain: ErrorDomain.STORAGE,
904
- category: ErrorCategory.THIRD_PARTY,
905
- text: `Failed to update thread ${id}`,
906
- details: { threadId: id },
907
- },
908
- error,
909
- );
910
- }
911
- }
912
-
913
- async deleteThread({ threadId }: { threadId: string }): Promise<void> {
914
- // Delete messages for this thread (manual step)
915
- try {
916
- await this.client.execute({
917
- sql: `DELETE FROM ${TABLE_MESSAGES} WHERE thread_id = ?`,
918
- args: [threadId],
919
- });
920
- await this.client.execute({
921
- sql: `DELETE FROM ${TABLE_THREADS} WHERE id = ?`,
922
- args: [threadId],
923
- });
924
- } catch (error) {
925
- throw new MastraError(
926
- {
927
- id: 'LIBSQL_STORE_DELETE_THREAD_FAILED',
928
- domain: ErrorDomain.STORAGE,
929
- category: ErrorCategory.THIRD_PARTY,
930
- details: { threadId },
931
- },
932
- error,
933
- );
934
- }
935
- // TODO: Need to check if CASCADE is enabled so that messages will be automatically deleted due to CASCADE constraint
936
- }
937
- }