@mastra/libsql 0.10.1 → 0.10.2-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.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +27 -0
- package/dist/_tsup-dts-rollup.d.cts +53 -3
- package/dist/_tsup-dts-rollup.d.ts +53 -3
- package/dist/index.cjs +347 -129
- package/dist/index.js +347 -129
- package/package.json +10 -10
- package/src/storage/index.test.ts +356 -6
- package/src/storage/index.ts +437 -157
|
@@ -1,14 +1,364 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
createSampleEval,
|
|
4
|
+
createSampleTraceForDB,
|
|
5
|
+
createSampleThread,
|
|
6
|
+
createTestSuite,
|
|
7
|
+
createSampleMessageV1,
|
|
8
|
+
resetRole,
|
|
9
|
+
} from '@internal/storage-test-utils';
|
|
10
|
+
import type { MastraMessageV1, StorageThreadType } from '@mastra/core';
|
|
2
11
|
import { Mastra } from '@mastra/core/mastra';
|
|
12
|
+
import { TABLE_EVALS, TABLE_TRACES, TABLE_MESSAGES, TABLE_THREADS } from '@mastra/core/storage';
|
|
13
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
14
|
+
|
|
3
15
|
import { LibSQLStore } from './index';
|
|
4
16
|
|
|
5
|
-
|
|
6
|
-
const TEST_DB_URL = 'file::memory:?cache=shared'; // Use in-memory SQLite for tests
|
|
17
|
+
const TEST_DB_URL = 'file::memory:?cache=shared';
|
|
7
18
|
|
|
19
|
+
const libsql = new LibSQLStore({
|
|
20
|
+
url: TEST_DB_URL,
|
|
21
|
+
});
|
|
8
22
|
const mastra = new Mastra({
|
|
9
|
-
storage:
|
|
10
|
-
url: TEST_DB_URL,
|
|
11
|
-
}),
|
|
23
|
+
storage: libsql,
|
|
12
24
|
});
|
|
13
25
|
|
|
14
26
|
createTestSuite(mastra.getStorage()!);
|
|
27
|
+
|
|
28
|
+
describe('LibSQLStore Pagination Features', () => {
|
|
29
|
+
let store: LibSQLStore;
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
store = libsql;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
await store.clearTable({ tableName: TABLE_EVALS });
|
|
37
|
+
await store.clearTable({ tableName: TABLE_TRACES });
|
|
38
|
+
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
39
|
+
await store.clearTable({ tableName: TABLE_THREADS });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getEvals with pagination', () => {
|
|
43
|
+
it('should return paginated evals with total count (page/perPage)', async () => {
|
|
44
|
+
const agentName = 'libsql-pagination-agent-evals';
|
|
45
|
+
const evalRecords = Array.from({ length: 25 }, (_, i) => createSampleEval(agentName, i % 2 === 0));
|
|
46
|
+
await store.batchInsert({ tableName: TABLE_EVALS, records: evalRecords.map(r => r as any) });
|
|
47
|
+
|
|
48
|
+
const page1 = await store.getEvals({ agentName, page: 0, perPage: 10 });
|
|
49
|
+
expect(page1.evals).toHaveLength(10);
|
|
50
|
+
expect(page1.total).toBe(25);
|
|
51
|
+
expect(page1.page).toBe(0);
|
|
52
|
+
expect(page1.perPage).toBe(10);
|
|
53
|
+
expect(page1.hasMore).toBe(true);
|
|
54
|
+
|
|
55
|
+
const page3 = await store.getEvals({ agentName, page: 2, perPage: 10 });
|
|
56
|
+
expect(page3.evals).toHaveLength(5);
|
|
57
|
+
expect(page3.total).toBe(25);
|
|
58
|
+
expect(page3.page).toBe(2);
|
|
59
|
+
expect(page3.hasMore).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should support limit/offset pagination for getEvals', async () => {
|
|
63
|
+
const agentName = 'libsql-pagination-lo-evals';
|
|
64
|
+
const evalRecords = Array.from({ length: 15 }, () => createSampleEval(agentName));
|
|
65
|
+
await store.batchInsert({ tableName: TABLE_EVALS, records: evalRecords.map(r => r as any) });
|
|
66
|
+
|
|
67
|
+
const result = await store.getEvals({ agentName, page: 2, perPage: 5 });
|
|
68
|
+
expect(result.evals).toHaveLength(5);
|
|
69
|
+
expect(result.total).toBe(15);
|
|
70
|
+
expect(result.page).toBe(2);
|
|
71
|
+
expect(result.perPage).toBe(5);
|
|
72
|
+
expect(result.hasMore).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should filter by type with pagination for getEvals', async () => {
|
|
76
|
+
const agentName = 'libsql-pagination-type-evals';
|
|
77
|
+
const testEvals = Array.from({ length: 10 }, () => createSampleEval(agentName, true));
|
|
78
|
+
const liveEvals = Array.from({ length: 8 }, () => createSampleEval(agentName, false));
|
|
79
|
+
await store.batchInsert({ tableName: TABLE_EVALS, records: [...testEvals, ...liveEvals].map(r => r as any) });
|
|
80
|
+
|
|
81
|
+
const testResults = await store.getEvals({ agentName, type: 'test', page: 0, perPage: 5 });
|
|
82
|
+
expect(testResults.evals).toHaveLength(5);
|
|
83
|
+
expect(testResults.total).toBe(10);
|
|
84
|
+
|
|
85
|
+
const liveResults = await store.getEvals({ agentName, type: 'live', page: 1, perPage: 3 });
|
|
86
|
+
expect(liveResults.evals).toHaveLength(3);
|
|
87
|
+
expect(liveResults.total).toBe(8);
|
|
88
|
+
expect(liveResults.hasMore).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should filter by date with pagination for getEvals', async () => {
|
|
92
|
+
const agentName = 'libsql-pagination-date-evals';
|
|
93
|
+
const now = new Date();
|
|
94
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
95
|
+
const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
96
|
+
|
|
97
|
+
const recordsToInsert = [
|
|
98
|
+
createSampleEval(agentName, false, dayBeforeYesterday),
|
|
99
|
+
createSampleEval(agentName, false, dayBeforeYesterday),
|
|
100
|
+
createSampleEval(agentName, false, yesterday),
|
|
101
|
+
createSampleEval(agentName, false, yesterday),
|
|
102
|
+
createSampleEval(agentName, false, now),
|
|
103
|
+
createSampleEval(agentName, false, now),
|
|
104
|
+
];
|
|
105
|
+
await store.batchInsert({ tableName: TABLE_EVALS, records: recordsToInsert.map(r => r as any) });
|
|
106
|
+
|
|
107
|
+
const fromYesterday = await store.getEvals({ agentName, dateRange: { start: yesterday }, page: 0, perPage: 3 });
|
|
108
|
+
expect(fromYesterday.total).toBe(4);
|
|
109
|
+
expect(fromYesterday.evals).toHaveLength(3); // Should get 2 from 'now', 1 from 'yesterday' due to DESC order and limit 3
|
|
110
|
+
fromYesterday.evals.forEach(e =>
|
|
111
|
+
expect(new Date(e.createdAt).getTime()).toBeGreaterThanOrEqual(new Date(yesterday.toISOString()).getTime()),
|
|
112
|
+
);
|
|
113
|
+
// Check if the first item is from 'now' if possible (because of DESC order)
|
|
114
|
+
if (fromYesterday.evals.length > 0) {
|
|
115
|
+
expect(new Date(fromYesterday.evals[0].createdAt).toISOString().slice(0, 10)).toEqual(
|
|
116
|
+
now.toISOString().slice(0, 10),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const onlyDayBefore = await store.getEvals({
|
|
121
|
+
agentName,
|
|
122
|
+
dateRange: { end: new Date(yesterday.getTime() - 1) },
|
|
123
|
+
page: 0,
|
|
124
|
+
perPage: 5,
|
|
125
|
+
});
|
|
126
|
+
expect(onlyDayBefore.total).toBe(2);
|
|
127
|
+
expect(onlyDayBefore.evals).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('getTraces with pagination', () => {
|
|
132
|
+
it('should return paginated traces with total count when returnPaginationResults is true', async () => {
|
|
133
|
+
const scope = 'libsql-test-scope-traces';
|
|
134
|
+
const traceRecords = Array.from({ length: 18 }, (_, i) => createSampleTraceForDB(`test-trace-${i}`, scope));
|
|
135
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: traceRecords.map(r => r as any) });
|
|
136
|
+
|
|
137
|
+
const page1 = await store.getTracesPaginated({
|
|
138
|
+
scope,
|
|
139
|
+
page: 0,
|
|
140
|
+
perPage: 8,
|
|
141
|
+
});
|
|
142
|
+
expect(page1.traces).toHaveLength(8);
|
|
143
|
+
expect(page1.total).toBe(18);
|
|
144
|
+
expect(page1.page).toBe(0);
|
|
145
|
+
expect(page1.perPage).toBe(8);
|
|
146
|
+
expect(page1.hasMore).toBe(true);
|
|
147
|
+
|
|
148
|
+
const page3 = await store.getTracesPaginated({
|
|
149
|
+
scope,
|
|
150
|
+
page: 2,
|
|
151
|
+
perPage: 8,
|
|
152
|
+
});
|
|
153
|
+
expect(page3.traces).toHaveLength(2);
|
|
154
|
+
expect(page3.total).toBe(18);
|
|
155
|
+
expect(page3.hasMore).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return an array of traces when returnPaginationResults is undefined', async () => {
|
|
159
|
+
const scope = 'libsql-array-traces';
|
|
160
|
+
const traceRecords = [createSampleTraceForDB('trace-arr-1', scope), createSampleTraceForDB('trace-arr-2', scope)];
|
|
161
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: traceRecords.map(r => r as any) });
|
|
162
|
+
|
|
163
|
+
const tracesUndefined = await store.getTraces({
|
|
164
|
+
scope,
|
|
165
|
+
page: 0,
|
|
166
|
+
perPage: 5,
|
|
167
|
+
});
|
|
168
|
+
expect(Array.isArray(tracesUndefined)).toBe(true);
|
|
169
|
+
expect(tracesUndefined.length).toBe(2);
|
|
170
|
+
// @ts-expect-error
|
|
171
|
+
expect(tracesUndefined.total).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should filter by attributes with pagination for getTraces', async () => {
|
|
175
|
+
const scope = 'libsql-attr-traces';
|
|
176
|
+
const tracesWithAttr = Array.from({ length: 8 }, (_, i) =>
|
|
177
|
+
createSampleTraceForDB(`trace-prod-${i}`, scope, { environment: 'prod' }),
|
|
178
|
+
);
|
|
179
|
+
const tracesWithoutAttr = Array.from({ length: 5 }, (_, i) =>
|
|
180
|
+
createSampleTraceForDB(`trace-dev-${i}`, scope, { environment: 'dev' }),
|
|
181
|
+
);
|
|
182
|
+
await store.batchInsert({
|
|
183
|
+
tableName: TABLE_TRACES,
|
|
184
|
+
records: [...tracesWithAttr, ...tracesWithoutAttr].map(r => r as any),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const prodTraces = await store.getTracesPaginated({
|
|
188
|
+
scope,
|
|
189
|
+
attributes: { environment: 'prod' },
|
|
190
|
+
page: 0,
|
|
191
|
+
perPage: 5,
|
|
192
|
+
});
|
|
193
|
+
expect(prodTraces.traces).toHaveLength(5);
|
|
194
|
+
expect(prodTraces.total).toBe(8);
|
|
195
|
+
expect(prodTraces.hasMore).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should filter by date with pagination for getTraces', async () => {
|
|
199
|
+
const scope = 'libsql-date-traces';
|
|
200
|
+
const now = new Date();
|
|
201
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
202
|
+
const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
203
|
+
|
|
204
|
+
const recordsToInsert = [
|
|
205
|
+
createSampleTraceForDB('t_dbf1', scope, undefined, dayBeforeYesterday),
|
|
206
|
+
createSampleTraceForDB('t_dbf2', scope, undefined, dayBeforeYesterday),
|
|
207
|
+
createSampleTraceForDB('t_y1', scope, undefined, yesterday),
|
|
208
|
+
createSampleTraceForDB('t_y3', scope, undefined, yesterday),
|
|
209
|
+
createSampleTraceForDB('t_n1', scope, undefined, now),
|
|
210
|
+
createSampleTraceForDB('t_n2', scope, undefined, now),
|
|
211
|
+
];
|
|
212
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: recordsToInsert.map(r => r as any) });
|
|
213
|
+
|
|
214
|
+
const fromYesterday = await store.getTracesPaginated({
|
|
215
|
+
scope,
|
|
216
|
+
dateRange: { start: yesterday },
|
|
217
|
+
page: 0,
|
|
218
|
+
perPage: 3,
|
|
219
|
+
});
|
|
220
|
+
expect(fromYesterday.total).toBe(4);
|
|
221
|
+
expect(fromYesterday.traces).toHaveLength(3);
|
|
222
|
+
fromYesterday.traces.forEach(t =>
|
|
223
|
+
expect(new Date(t.createdAt).getTime()).toBeGreaterThanOrEqual(new Date(yesterday.toISOString()).getTime()),
|
|
224
|
+
);
|
|
225
|
+
if (fromYesterday.traces.length > 0 && fromYesterday.traces[0].createdAt === now.toISOString()) {
|
|
226
|
+
expect(new Date(fromYesterday.traces[0].createdAt).toISOString().slice(0, 10)).toEqual(
|
|
227
|
+
now.toISOString().slice(0, 10),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const onlyNow = await store.getTracesPaginated({
|
|
232
|
+
scope,
|
|
233
|
+
dateRange: { start: now, end: now },
|
|
234
|
+
page: 0,
|
|
235
|
+
perPage: 5,
|
|
236
|
+
});
|
|
237
|
+
expect(onlyNow.total).toBe(2);
|
|
238
|
+
expect(onlyNow.traces).toHaveLength(2);
|
|
239
|
+
onlyNow.traces.forEach(t =>
|
|
240
|
+
expect(new Date(t.createdAt).toISOString().slice(0, 10)).toEqual(now.toISOString().slice(0, 10)),
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('getMessages with pagination', () => {
|
|
246
|
+
it('should return paginated messages with total count', async () => {
|
|
247
|
+
resetRole();
|
|
248
|
+
const threadData = createSampleThread();
|
|
249
|
+
threadData.resourceId = 'resource-msg-pagination';
|
|
250
|
+
const thread = await store.saveThread({ thread: threadData as StorageThreadType });
|
|
251
|
+
|
|
252
|
+
const messageRecords: MastraMessageV1[] = [];
|
|
253
|
+
for (let i = 0; i < 15; i++) {
|
|
254
|
+
messageRecords.push(createSampleMessageV1({ threadId: thread.id, content: `Message ${i + 1}` }));
|
|
255
|
+
}
|
|
256
|
+
await store.saveMessages({ messages: messageRecords });
|
|
257
|
+
|
|
258
|
+
const page1 = await store.getMessagesPaginated({
|
|
259
|
+
threadId: thread.id,
|
|
260
|
+
selectBy: { pagination: { page: 0, perPage: 5 } },
|
|
261
|
+
format: 'v1',
|
|
262
|
+
});
|
|
263
|
+
expect(page1.messages).toHaveLength(5);
|
|
264
|
+
expect(page1.total).toBe(15);
|
|
265
|
+
expect(page1.page).toBe(0);
|
|
266
|
+
expect(page1.perPage).toBe(5);
|
|
267
|
+
expect(page1.hasMore).toBe(true);
|
|
268
|
+
|
|
269
|
+
const page3 = await store.getMessagesPaginated({
|
|
270
|
+
threadId: thread.id,
|
|
271
|
+
selectBy: { pagination: { page: 2, perPage: 5 } },
|
|
272
|
+
format: 'v1',
|
|
273
|
+
});
|
|
274
|
+
expect(page3.messages).toHaveLength(5);
|
|
275
|
+
expect(page3.total).toBe(15);
|
|
276
|
+
expect(page3.hasMore).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should filter by date with pagination for getMessages', async () => {
|
|
280
|
+
const threadData = createSampleThread();
|
|
281
|
+
const thread = await store.saveThread({ thread: threadData as StorageThreadType });
|
|
282
|
+
const now = new Date();
|
|
283
|
+
const yesterday = new Date(
|
|
284
|
+
now.getFullYear(),
|
|
285
|
+
now.getMonth(),
|
|
286
|
+
now.getDate() - 1,
|
|
287
|
+
now.getHours(),
|
|
288
|
+
now.getMinutes(),
|
|
289
|
+
now.getSeconds(),
|
|
290
|
+
);
|
|
291
|
+
const dayBeforeYesterday = new Date(
|
|
292
|
+
now.getFullYear(),
|
|
293
|
+
now.getMonth(),
|
|
294
|
+
now.getDate() - 2,
|
|
295
|
+
now.getHours(),
|
|
296
|
+
now.getMinutes(),
|
|
297
|
+
now.getSeconds(),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Ensure timestamps are distinct for reliable sorting by creating them with a slight delay for testing clarity
|
|
301
|
+
const messagesToSave: MastraMessageV1[] = [];
|
|
302
|
+
messagesToSave.push(
|
|
303
|
+
createSampleMessageV1({ threadId: thread.id, content: 'Message 1', createdAt: dayBeforeYesterday }),
|
|
304
|
+
);
|
|
305
|
+
await new Promise(r => setTimeout(r, 5));
|
|
306
|
+
messagesToSave.push(
|
|
307
|
+
createSampleMessageV1({ threadId: thread.id, content: 'Message 2', createdAt: dayBeforeYesterday }),
|
|
308
|
+
);
|
|
309
|
+
await new Promise(r => setTimeout(r, 5));
|
|
310
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, content: 'Message 3', createdAt: yesterday }));
|
|
311
|
+
await new Promise(r => setTimeout(r, 5));
|
|
312
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, content: 'Message 4', createdAt: yesterday }));
|
|
313
|
+
await new Promise(r => setTimeout(r, 5));
|
|
314
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, content: 'Message 5', createdAt: now }));
|
|
315
|
+
await new Promise(r => setTimeout(r, 5));
|
|
316
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, content: 'Message 6', createdAt: now }));
|
|
317
|
+
|
|
318
|
+
await store.saveMessages({ messages: messagesToSave, format: 'v1' });
|
|
319
|
+
// Total 6 messages: 2 now, 2 yesterday, 2 dayBeforeYesterday (oldest to newest)
|
|
320
|
+
|
|
321
|
+
const fromYesterday = await store.getMessagesPaginated({
|
|
322
|
+
threadId: thread.id,
|
|
323
|
+
selectBy: { pagination: { page: 0, perPage: 3, dateRange: { start: yesterday } } },
|
|
324
|
+
format: 'v2',
|
|
325
|
+
});
|
|
326
|
+
expect(fromYesterday.total).toBe(4);
|
|
327
|
+
expect(fromYesterday.messages).toHaveLength(3);
|
|
328
|
+
const firstMessageTime = new Date((fromYesterday.messages[0] as MastraMessageV1).createdAt).getTime();
|
|
329
|
+
expect(firstMessageTime).toBeGreaterThanOrEqual(new Date(yesterday.toISOString()).getTime());
|
|
330
|
+
if (fromYesterday.messages.length > 0) {
|
|
331
|
+
expect(new Date((fromYesterday.messages[0] as MastraMessageV1).createdAt).toISOString().slice(0, 10)).toEqual(
|
|
332
|
+
yesterday.toISOString().slice(0, 10),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('getThreadsByResourceId with pagination', () => {
|
|
339
|
+
it('should return paginated threads with total count', async () => {
|
|
340
|
+
const resourceId = `libsql-paginated-resource-${randomUUID()}`;
|
|
341
|
+
const threadRecords: StorageThreadType[] = [];
|
|
342
|
+
for (let i = 0; i < 17; i++) {
|
|
343
|
+
const threadData = createSampleThread();
|
|
344
|
+
threadData.resourceId = resourceId;
|
|
345
|
+
threadRecords.push(threadData as StorageThreadType);
|
|
346
|
+
}
|
|
347
|
+
for (const tr of threadRecords) {
|
|
348
|
+
await store.saveThread({ thread: tr });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const page1 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 0, perPage: 7 });
|
|
352
|
+
expect(page1.threads).toHaveLength(7);
|
|
353
|
+
expect(page1.total).toBe(17);
|
|
354
|
+
expect(page1.page).toBe(0);
|
|
355
|
+
expect(page1.perPage).toBe(7);
|
|
356
|
+
expect(page1.hasMore).toBe(true);
|
|
357
|
+
|
|
358
|
+
const page3 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 2, perPage: 7 });
|
|
359
|
+
expect(page3.threads).toHaveLength(3);
|
|
360
|
+
expect(page3.total).toBe(17);
|
|
361
|
+
expect(page3.hasMore).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|