@mastra/upstash 0.10.3-alpha.1 → 0.10.3-alpha.3

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,5 +1,10 @@
1
1
  import { randomUUID } from 'crypto';
2
- import { createSampleMessageV2, createSampleThread, createSampleWorkflowSnapshot } from '@internal/storage-test-utils';
2
+ import {
3
+ checkWorkflowSnapshot,
4
+ createSampleMessageV2,
5
+ createSampleThread,
6
+ createSampleWorkflowSnapshot,
7
+ } from '@internal/storage-test-utils';
3
8
  import type { MastraMessageV2 } from '@mastra/core';
4
9
  import type { TABLE_NAMES } from '@mastra/core/storage';
5
10
  import {
@@ -17,7 +22,12 @@ import { UpstashStore } from './index';
17
22
  // Increase timeout for all tests in this file to 30 seconds
18
23
  vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
19
24
 
20
- const createSampleTrace = (name: string, scope?: string, attributes?: Record<string, string>) => ({
25
+ const createSampleTrace = (
26
+ name: string,
27
+ scope?: string,
28
+ attributes?: Record<string, string>,
29
+ createdAt: Date = new Date(),
30
+ ) => ({
21
31
  id: `trace-${randomUUID()}`,
22
32
  parentSpanId: `span-${randomUUID()}`,
23
33
  traceId: `trace-${randomUUID()}`,
@@ -25,16 +35,16 @@ const createSampleTrace = (name: string, scope?: string, attributes?: Record<str
25
35
  scope,
26
36
  kind: 'internal',
27
37
  status: JSON.stringify({ code: 'success' }),
28
- events: JSON.stringify([{ name: 'start', timestamp: Date.now() }]),
38
+ events: JSON.stringify([{ name: 'start', timestamp: createdAt.getTime() }]),
29
39
  links: JSON.stringify([]),
30
40
  attributes: attributes ? JSON.stringify(attributes) : undefined,
31
- startTime: new Date().toISOString(),
32
- endTime: new Date().toISOString(),
41
+ startTime: createdAt.toISOString(),
42
+ endTime: new Date(createdAt.getTime() + 1000).toISOString(),
33
43
  other: JSON.stringify({ custom: 'data' }),
34
- createdAt: new Date().toISOString(),
44
+ createdAt: createdAt.toISOString(),
35
45
  });
36
46
 
37
- const createSampleEval = (agentName: string, isTest = false) => {
47
+ const createSampleEval = (agentName: string, isTest = false, createdAt: Date = new Date()) => {
38
48
  const testInfo = isTest ? { testPath: 'test/path.ts', testName: 'Test Name' } : undefined;
39
49
 
40
50
  return {
@@ -47,17 +57,10 @@ const createSampleEval = (agentName: string, isTest = false) => {
47
57
  test_info: testInfo ? JSON.stringify(testInfo) : undefined,
48
58
  global_run_id: `global-${randomUUID()}`,
49
59
  run_id: `run-${randomUUID()}`,
50
- created_at: new Date().toISOString(),
60
+ created_at: createdAt.toISOString(),
51
61
  };
52
62
  };
53
63
 
54
- const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
55
- if (typeof snapshot === 'string') {
56
- throw new Error('Expected WorkflowRunState, got string');
57
- }
58
- expect(snapshot.context?.[stepId]?.status).toBe(status);
59
- };
60
-
61
64
  describe('UpstashStore', () => {
62
65
  let store: UpstashStore;
63
66
  const testTableName = 'test_table';
@@ -154,7 +157,7 @@ describe('UpstashStore', () => {
154
157
 
155
158
  it('should get threads by resource ID', async () => {
156
159
  const thread1 = createSampleThread();
157
- const thread2 = { ...createSampleThread(), resourceId: thread1.resourceId };
160
+ const thread2 = createSampleThread({ resourceId: thread1.resourceId });
158
161
  const threads = [thread1, thread2];
159
162
 
160
163
  const resourceId = threads[0].resourceId;
@@ -187,7 +190,7 @@ describe('UpstashStore', () => {
187
190
  it('should fetch >100000 threads by resource ID', async () => {
188
191
  const resourceId = `resource-${randomUUID()}`;
189
192
  const total = 100_000;
190
- const threads = Array.from({ length: total }, () => ({ ...createSampleThread(), resourceId }));
193
+ const threads = Array.from({ length: total }, () => createSampleThread({ resourceId }));
191
194
 
192
195
  await store.batchInsert({ tableName: TABLE_THREADS, records: threads });
193
196
 
@@ -300,13 +303,117 @@ describe('UpstashStore', () => {
300
303
  createSampleMessageV2({ threadId, content: 'Third' }),
301
304
  ];
302
305
 
303
- await store.saveMessages({ messages: messages, format: 'v2' });
306
+ await store.saveMessages({ messages, format: 'v2' });
304
307
 
305
308
  const retrievedMessages = await store.getMessages({ threadId, format: 'v2' });
306
309
  expect(retrievedMessages).toHaveLength(3);
307
310
  expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
308
311
  });
309
312
 
313
+ it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
314
+ const thread = createSampleThread({ id: 'thread-one' });
315
+ await store.saveThread({ thread });
316
+
317
+ const thread2 = createSampleThread({ id: 'thread-two' });
318
+ await store.saveThread({ thread: thread2 });
319
+
320
+ const thread3 = createSampleThread({ id: 'thread-three' });
321
+ await store.saveThread({ thread: thread3 });
322
+
323
+ const messages: MastraMessageV2[] = [
324
+ createSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
325
+ createSampleMessageV2({ threadId: 'thread-one', content: 'Second', resourceId: 'cross-thread-resource' }),
326
+ createSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
327
+
328
+ createSampleMessageV2({ threadId: 'thread-two', content: 'Fourth', resourceId: 'cross-thread-resource' }),
329
+ createSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
330
+ createSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
331
+
332
+ createSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
333
+ createSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
334
+ ];
335
+
336
+ await store.saveMessages({ messages: messages, format: 'v2' });
337
+
338
+ const retrievedMessages = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
339
+ expect(retrievedMessages).toHaveLength(3);
340
+ expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
341
+
342
+ const retrievedMessages2 = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
343
+ expect(retrievedMessages2).toHaveLength(3);
344
+ expect(retrievedMessages2.map((m: any) => m.content.parts[0].text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
345
+
346
+ const retrievedMessages3 = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
347
+ expect(retrievedMessages3).toHaveLength(2);
348
+ expect(retrievedMessages3.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
349
+
350
+ const crossThreadMessages = await store.getMessages({
351
+ threadId: 'thread-doesnt-exist',
352
+ format: 'v2',
353
+ selectBy: {
354
+ last: 0,
355
+ include: [
356
+ {
357
+ id: messages[1].id,
358
+ threadId: 'thread-one',
359
+ withNextMessages: 2,
360
+ withPreviousMessages: 2,
361
+ },
362
+ {
363
+ id: messages[4].id,
364
+ threadId: 'thread-two',
365
+ withPreviousMessages: 2,
366
+ withNextMessages: 2,
367
+ },
368
+ ],
369
+ },
370
+ });
371
+
372
+ expect(crossThreadMessages).toHaveLength(6);
373
+ expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
374
+ expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
375
+
376
+ const crossThreadMessages2 = await store.getMessages({
377
+ threadId: 'thread-one',
378
+ format: 'v2',
379
+ selectBy: {
380
+ last: 0,
381
+ include: [
382
+ {
383
+ id: messages[4].id,
384
+ threadId: 'thread-two',
385
+ withPreviousMessages: 1,
386
+ withNextMessages: 1,
387
+ },
388
+ ],
389
+ },
390
+ });
391
+
392
+ expect(crossThreadMessages2).toHaveLength(3);
393
+ expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
394
+ expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
395
+
396
+ const crossThreadMessages3 = await store.getMessages({
397
+ threadId: 'thread-two',
398
+ format: 'v2',
399
+ selectBy: {
400
+ last: 0,
401
+ include: [
402
+ {
403
+ id: messages[1].id,
404
+ threadId: 'thread-one',
405
+ withNextMessages: 1,
406
+ withPreviousMessages: 1,
407
+ },
408
+ ],
409
+ },
410
+ });
411
+
412
+ expect(crossThreadMessages3).toHaveLength(3);
413
+ expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
414
+ expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
415
+ });
416
+
310
417
  it('should handle empty message array', async () => {
311
418
  const result = await store.saveMessages({ messages: [] });
312
419
  expect(result).toEqual([]);
@@ -336,7 +443,7 @@ describe('UpstashStore', () => {
336
443
  expect(retrievedMessages[0].content).toEqual(messages[0].content);
337
444
  });
338
445
 
339
- describe('getPaginatedMessages', () => {
446
+ describe('getMessagesPaginated', () => {
340
447
  it('should return paginated messages with total count', async () => {
341
448
  const thread = createSampleThread();
342
449
  await store.saveThread({ thread });
@@ -347,10 +454,9 @@ describe('UpstashStore', () => {
347
454
 
348
455
  await store.saveMessages({ messages, format: 'v2' });
349
456
 
350
- const page1 = await store.getMessages({
457
+ const page1 = await store.getMessagesPaginated({
351
458
  threadId: thread.id,
352
- page: 0,
353
- perPage: 5,
459
+ selectBy: { pagination: { page: 0, perPage: 5 } },
354
460
  format: 'v2',
355
461
  });
356
462
  expect(page1.messages).toHaveLength(5);
@@ -359,20 +465,18 @@ describe('UpstashStore', () => {
359
465
  expect(page1.perPage).toBe(5);
360
466
  expect(page1.hasMore).toBe(true);
361
467
 
362
- const page3 = await store.getMessages({
468
+ const page3 = await store.getMessagesPaginated({
363
469
  threadId: thread.id,
364
- page: 2,
365
- perPage: 5,
470
+ selectBy: { pagination: { page: 2, perPage: 5 } },
366
471
  format: 'v2',
367
472
  });
368
473
  expect(page3.messages).toHaveLength(5);
369
474
  expect(page3.total).toBe(15);
370
475
  expect(page3.hasMore).toBe(false);
371
476
 
372
- const page4 = await store.getMessages({
477
+ const page4 = await store.getMessagesPaginated({
373
478
  threadId: thread.id,
374
- page: 3,
375
- perPage: 5,
479
+ selectBy: { pagination: { page: 3, perPage: 5 } },
376
480
  format: 'v2',
377
481
  });
378
482
  expect(page4.messages).toHaveLength(0);
@@ -393,10 +497,9 @@ describe('UpstashStore', () => {
393
497
 
394
498
  await store.saveMessages({ messages, format: 'v2' });
395
499
 
396
- const page1 = await store.getMessages({
500
+ const page1 = await store.getMessagesPaginated({
397
501
  threadId: thread.id,
398
- page: 0,
399
- perPage: 3,
502
+ selectBy: { pagination: { page: 0, perPage: 3 } },
400
503
  format: 'v2',
401
504
  });
402
505
 
@@ -410,32 +513,6 @@ describe('UpstashStore', () => {
410
513
  }
411
514
  });
412
515
 
413
- it('should maintain backward compatibility when no pagination params provided', async () => {
414
- const thread = createSampleThread();
415
- await store.saveThread({ thread });
416
-
417
- const messages = Array.from({ length: 5 }, (_, i) =>
418
- createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
419
- );
420
-
421
- await store.saveMessages({ messages, format: 'v2' });
422
-
423
- // Test original format without pagination - should return array
424
- const messagesV1 = await store.getMessages({
425
- threadId: thread.id,
426
- format: 'v1',
427
- });
428
- expect(Array.isArray(messagesV1)).toBe(true);
429
- expect(messagesV1).toHaveLength(5);
430
-
431
- const messagesV2 = await store.getMessages({
432
- threadId: thread.id,
433
- format: 'v2',
434
- });
435
- expect(Array.isArray(messagesV2)).toBe(true);
436
- expect(messagesV2).toHaveLength(5);
437
- });
438
-
439
516
  it('should support date filtering with pagination', async () => {
440
517
  const thread = createSampleThread();
441
518
  await store.saveThread({ thread });
@@ -458,11 +535,15 @@ describe('UpstashStore', () => {
458
535
 
459
536
  await store.saveMessages({ messages: [...oldMessages, ...newMessages], format: 'v2' });
460
537
 
461
- const recentMessages = await store.getMessages({
538
+ const recentMessages = await store.getMessagesPaginated({
462
539
  threadId: thread.id,
463
- page: 0,
464
- perPage: 10,
465
- fromDate: now,
540
+ selectBy: {
541
+ pagination: {
542
+ page: 0,
543
+ perPage: 10,
544
+ dateRange: { start: now },
545
+ },
546
+ },
466
547
  format: 'v2',
467
548
  });
468
549
  expect(recentMessages.messages).toHaveLength(4);
@@ -1098,7 +1179,7 @@ describe('UpstashStore', () => {
1098
1179
  expect(page3.hasMore).toBe(false);
1099
1180
  });
1100
1181
 
1101
- it('should support limit/offset pagination', async () => {
1182
+ it('should support page/perPage pagination', async () => {
1102
1183
  const agentName = 'test-agent-2';
1103
1184
  const evals = Array.from({ length: 15 }, () => createSampleEval(agentName));
1104
1185
 
@@ -1110,12 +1191,12 @@ describe('UpstashStore', () => {
1110
1191
  }
1111
1192
 
1112
1193
  // Test offset-based pagination
1113
- const result1 = await store.getEvals({ agentName, limit: 5, offset: 0 });
1194
+ const result1 = await store.getEvals({ agentName, page: 0, perPage: 5 });
1114
1195
  expect(result1.evals).toHaveLength(5);
1115
1196
  expect(result1.total).toBe(15);
1116
1197
  expect(result1.hasMore).toBe(true);
1117
1198
 
1118
- const result2 = await store.getEvals({ agentName, limit: 5, offset: 10 });
1199
+ const result2 = await store.getEvals({ agentName, page: 2, perPage: 5 });
1119
1200
  expect(result2.evals).toHaveLength(5);
1120
1201
  expect(result2.total).toBe(15);
1121
1202
  expect(result2.hasMore).toBe(false);
@@ -1141,9 +1222,30 @@ describe('UpstashStore', () => {
1141
1222
  expect(liveResults.evals).toHaveLength(5);
1142
1223
  expect(liveResults.total).toBe(8);
1143
1224
  });
1225
+
1226
+ it('should filter by date with pagination', async () => {
1227
+ const agentName = 'test-agent-date';
1228
+ const now = new Date();
1229
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1230
+ const evals = [createSampleEval(agentName, false, now), createSampleEval(agentName, false, yesterday)];
1231
+ for (const evalRecord of evals) {
1232
+ await store.insert({
1233
+ tableName: TABLE_EVALS,
1234
+ record: evalRecord,
1235
+ });
1236
+ }
1237
+ const result = await store.getEvals({
1238
+ agentName,
1239
+ page: 0,
1240
+ perPage: 10,
1241
+ dateRange: { start: now },
1242
+ });
1243
+ expect(result.evals).toHaveLength(1);
1244
+ expect(result.total).toBe(1);
1245
+ });
1144
1246
  });
1145
1247
 
1146
- describe('getTracesPaginated', () => {
1248
+ describe('getTraces with pagination', () => {
1147
1249
  it('should return paginated traces with total count', async () => {
1148
1250
  const traces = Array.from({ length: 18 }, (_, i) => createSampleTrace(`test-trace-${i}`, 'test-scope'));
1149
1251
 
@@ -1154,11 +1256,10 @@ describe('UpstashStore', () => {
1154
1256
  });
1155
1257
  }
1156
1258
 
1157
- const page1 = await store.getTraces({
1259
+ const page1 = await store.getTracesPaginated({
1158
1260
  scope: 'test-scope',
1159
1261
  page: 0,
1160
1262
  perPage: 8,
1161
- returnPaginationResults: true,
1162
1263
  });
1163
1264
  expect(page1.traces).toHaveLength(8);
1164
1265
  expect(page1.total).toBe(18);
@@ -1166,75 +1267,62 @@ describe('UpstashStore', () => {
1166
1267
  expect(page1.perPage).toBe(8);
1167
1268
  expect(page1.hasMore).toBe(true);
1168
1269
 
1169
- const page3 = await store.getTraces({
1270
+ const page3 = await store.getTracesPaginated({
1170
1271
  scope: 'test-scope',
1171
1272
  page: 2,
1172
1273
  perPage: 8,
1173
- returnPaginationResults: true,
1174
1274
  });
1175
1275
  expect(page3.traces).toHaveLength(2);
1176
1276
  expect(page3.total).toBe(18);
1177
1277
  expect(page3.hasMore).toBe(false);
1178
1278
  });
1179
1279
 
1180
- it('should filter by attributes with pagination', async () => {
1181
- const tracesWithAttr = Array.from({ length: 8 }, (_, i) =>
1182
- createSampleTrace(`trace-${i}`, 'test-scope', { environment: 'prod' }),
1183
- );
1184
- const tracesWithoutAttr = Array.from({ length: 5 }, (_, i) =>
1185
- createSampleTrace(`trace-other-${i}`, 'test-scope', { environment: 'dev' }),
1186
- );
1280
+ it('should filter by date with pagination', async () => {
1281
+ const scope = 'test-scope-date';
1282
+ const now = new Date();
1283
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1284
+
1285
+ const traces = [
1286
+ createSampleTrace(`test-trace-now`, scope, undefined, now),
1287
+ createSampleTrace(`test-trace-yesterday`, scope, undefined, yesterday),
1288
+ ];
1187
1289
 
1188
- for (const trace of [...tracesWithAttr, ...tracesWithoutAttr]) {
1290
+ for (const trace of traces) {
1189
1291
  await store.insert({
1190
1292
  tableName: TABLE_TRACES,
1191
1293
  record: trace,
1192
1294
  });
1193
1295
  }
1194
1296
 
1195
- const prodTraces = await store.getTraces({
1196
- scope: 'test-scope',
1197
- attributes: { environment: 'prod' },
1198
- page: 0,
1199
- perPage: 5,
1200
- returnPaginationResults: true,
1201
- });
1202
- expect(prodTraces.traces).toHaveLength(5);
1203
- expect(prodTraces.total).toBe(8);
1204
- expect(prodTraces.hasMore).toBe(true);
1205
-
1206
- const devTraces = await store.getTraces({
1207
- scope: 'test-scope',
1208
- attributes: { environment: 'dev' },
1297
+ const result = await store.getTracesPaginated({
1298
+ scope,
1209
1299
  page: 0,
1210
1300
  perPage: 10,
1211
- returnPaginationResults: true,
1301
+ dateRange: { start: now },
1212
1302
  });
1213
- expect(devTraces.traces).toHaveLength(5);
1214
- expect(devTraces.total).toBe(5);
1215
- expect(devTraces.hasMore).toBe(false);
1303
+
1304
+ expect(result.traces).toHaveLength(1);
1305
+ expect(result.traces[0].name).toBe('test-trace-now');
1306
+ expect(result.total).toBe(1);
1216
1307
  });
1217
1308
  });
1218
1309
 
1219
1310
  describe('Enhanced existing methods with pagination', () => {
1220
1311
  it('should support pagination in getThreadsByResourceId', async () => {
1221
1312
  const resourceId = 'enhanced-resource';
1222
- const threads = Array.from({ length: 17 }, () => ({
1223
- ...createSampleThread(),
1224
- resourceId,
1225
- }));
1313
+ const threads = Array.from({ length: 17 }, () => createSampleThread({ resourceId }));
1226
1314
 
1227
1315
  for (const thread of threads) {
1228
1316
  await store.saveThread({ thread });
1229
1317
  }
1230
1318
 
1231
- const page1 = await store.getThreadsByResourceId({ resourceId, page: 0, perPage: 7 });
1319
+ const page1 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 0, perPage: 7 });
1232
1320
  expect(page1.threads).toHaveLength(7);
1233
1321
 
1234
- const page3 = await store.getThreadsByResourceId({ resourceId, page: 2, perPage: 7 });
1322
+ const page3 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 2, perPage: 7 });
1235
1323
  expect(page3.threads).toHaveLength(3);
1236
1324
 
1237
- const limited = await store.getThreadsByResourceId({ resourceId, page: 1, perPage: 5 });
1325
+ const limited = await store.getThreadsByResourceIdPaginated({ resourceId, page: 1, perPage: 5 });
1238
1326
  expect(limited.threads).toHaveLength(5);
1239
1327
  });
1240
1328
  });