@mastra/upstash 0.10.3-alpha.0 → 0.10.3-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/upstash",
3
- "version": "0.10.3-alpha.0",
3
+ "version": "0.10.3-alpha.1",
4
4
  "description": "Upstash provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,9 +30,10 @@
30
30
  "eslint": "^9.28.0",
31
31
  "tsup": "^8.5.0",
32
32
  "typescript": "^5.8.2",
33
- "vitest": "^3.1.2",
33
+ "vitest": "^3.2.2",
34
+ "@internal/storage-test-utils": "0.0.6",
34
35
  "@internal/lint": "0.0.10",
35
- "@mastra/core": "0.10.4-alpha.0"
36
+ "@mastra/core": "0.10.4-alpha.1"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@mastra/core": "^0.10.2-alpha.0"
@@ -93,17 +93,6 @@ export class UpstashStore extends MastraStorage {
93
93
  return `${tableName}:${keyParts.join(':')}`;
94
94
  }
95
95
 
96
- private ensureDate(date: Date | string | undefined): Date | undefined {
97
- if (!date) return undefined;
98
- return date instanceof Date ? date : new Date(date);
99
- }
100
-
101
- private serializeDate(date: Date | string | undefined): string | undefined {
102
- if (!date) return undefined;
103
- const dateObj = this.ensureDate(date);
104
- return dateObj?.toISOString();
105
- }
106
-
107
96
  /**
108
97
  * Scans for keys matching the given pattern using SCAN and returns them as an array.
109
98
  * @param pattern Redis key pattern, e.g. "table:*"
@@ -454,6 +443,20 @@ export class UpstashStore extends MastraStorage {
454
443
  await this.redis.set(`schema:${tableName}`, schema);
455
444
  }
456
445
 
446
+ /**
447
+ * No-op: This backend is schemaless and does not require schema changes.
448
+ * @param tableName Name of the table
449
+ * @param schema Schema of the table
450
+ * @param ifNotExists Array of column names to add if they don't exist
451
+ */
452
+ async alterTable(_args: {
453
+ tableName: TABLE_NAMES;
454
+ schema: Record<string, StorageColumn>;
455
+ ifNotExists: string[];
456
+ }): Promise<void> {
457
+ // Nothing to do here, Redis is schemaless
458
+ }
459
+
457
460
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
458
461
  const pattern = `${tableName}:*`;
459
462
  await this.scanAndDelete(pattern);
@@ -632,8 +635,25 @@ export class UpstashStore extends MastraStorage {
632
635
  }
633
636
 
634
637
  async deleteThread({ threadId }: { threadId: string }): Promise<void> {
635
- const key = this.getKey(TABLE_THREADS, { id: threadId });
636
- await this.redis.del(key);
638
+ // Delete thread metadata and sorted set
639
+ const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
640
+ const threadMessagesKey = this.getThreadMessagesKey(threadId);
641
+ const messageIds: string[] = await this.redis.zrange(threadMessagesKey, 0, -1);
642
+
643
+ const pipeline = this.redis.pipeline();
644
+ pipeline.del(threadKey);
645
+ pipeline.del(threadMessagesKey);
646
+
647
+ for (let i = 0; i < messageIds.length; i++) {
648
+ const messageId = messageIds[i];
649
+ const messageKey = this.getMessageKey(threadId, messageId as string);
650
+ pipeline.del(messageKey);
651
+ }
652
+
653
+ await pipeline.exec();
654
+
655
+ // Bulk delete all message keys for this thread if any remain
656
+ await this.scanAndDelete(this.getMessageKey(threadId, '*'));
637
657
  }
638
658
 
639
659
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'crypto';
2
+ import { createSampleMessageV2, createSampleThread, createSampleWorkflowSnapshot } from '@internal/storage-test-utils';
2
3
  import type { MastraMessageV2 } from '@mastra/core';
3
4
  import type { TABLE_NAMES } from '@mastra/core/storage';
4
5
  import {
@@ -9,56 +10,13 @@ import {
9
10
  TABLE_TRACES,
10
11
  } from '@mastra/core/storage';
11
12
  import type { WorkflowRunState } from '@mastra/core/workflows';
12
- import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest';
13
+ import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
13
14
 
14
15
  import { UpstashStore } from './index';
15
16
 
16
17
  // Increase timeout for all tests in this file to 30 seconds
17
18
  vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
18
19
 
19
- const createSampleThread = (date?: Date) => ({
20
- id: `thread-${randomUUID()}`,
21
- resourceId: `resource-${randomUUID()}`,
22
- title: 'Test Thread',
23
- createdAt: date || new Date(),
24
- updatedAt: date || new Date(),
25
- metadata: { key: 'value' },
26
- });
27
-
28
- const createSampleMessage = (threadId: string, content: string = 'Hello'): MastraMessageV2 => ({
29
- id: `msg-${randomUUID()}`,
30
- role: 'user',
31
- threadId,
32
- content: { format: 2, parts: [{ type: 'text', text: content }] },
33
- createdAt: new Date(),
34
- resourceId: `resource-${randomUUID()}`,
35
- });
36
-
37
- const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
38
- const runId = `run-${randomUUID()}`;
39
- const stepId = `step-${randomUUID()}`;
40
- const timestamp = createdAt || new Date();
41
- const snapshot: WorkflowRunState = {
42
- value: {},
43
- context: {
44
- [stepId]: {
45
- status: status,
46
- payload: {},
47
- error: undefined,
48
- startedAt: timestamp.getTime(),
49
- endedAt: new Date(timestamp.getTime() + 15000).getTime(),
50
- },
51
- input: {},
52
- } as WorkflowRunState['context'],
53
- serializedStepGraph: [],
54
- activePaths: [],
55
- suspendedPaths: {},
56
- runId,
57
- timestamp: timestamp.getTime(),
58
- };
59
- return { snapshot, runId, stepId };
60
- };
61
-
62
20
  const createSampleTrace = (name: string, scope?: string, attributes?: Record<string, string>) => ({
63
21
  id: `trace-${randomUUID()}`,
64
22
  parentSpanId: `span-${randomUUID()}`,
@@ -176,7 +134,7 @@ describe('UpstashStore', () => {
176
134
 
177
135
  it('should create and retrieve a thread', async () => {
178
136
  const now = new Date();
179
- const thread = createSampleThread(now);
137
+ const thread = createSampleThread({ date: now });
180
138
 
181
139
  const savedThread = await store.saveThread({ thread });
182
140
  expect(savedThread).toEqual(thread);
@@ -236,6 +194,23 @@ describe('UpstashStore', () => {
236
194
  const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
237
195
  expect(retrievedThreads).toHaveLength(total);
238
196
  });
197
+ it('should delete thread and its messages', async () => {
198
+ const thread = createSampleThread();
199
+ await store.saveThread({ thread });
200
+
201
+ // Add some messages
202
+ const messages = [createSampleMessageV2({ threadId: thread.id }), createSampleMessageV2({ threadId: thread.id })];
203
+ await store.saveMessages({ messages, format: 'v2' });
204
+
205
+ await store.deleteThread({ threadId: thread.id });
206
+
207
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
208
+ expect(retrievedThread).toBeNull();
209
+
210
+ // Verify messages were also deleted
211
+ const retrievedMessages = await store.getMessages({ threadId: thread.id });
212
+ expect(retrievedMessages).toHaveLength(0);
213
+ });
239
214
  });
240
215
 
241
216
  describe('Date Handling', () => {
@@ -245,7 +220,7 @@ describe('UpstashStore', () => {
245
220
 
246
221
  it('should handle Date objects in thread operations', async () => {
247
222
  const now = new Date();
248
- const thread = createSampleThread(now);
223
+ const thread = createSampleThread({ date: now });
249
224
 
250
225
  await store.saveThread({ thread });
251
226
  const retrievedThread = await store.getThreadById({ threadId: thread.id });
@@ -257,7 +232,7 @@ describe('UpstashStore', () => {
257
232
 
258
233
  it('should handle ISO string dates in thread operations', async () => {
259
234
  const now = new Date();
260
- const thread = createSampleThread(now);
235
+ const thread = createSampleThread({ date: now });
261
236
 
262
237
  await store.saveThread({ thread });
263
238
  const retrievedThread = await store.getThreadById({ threadId: thread.id });
@@ -269,7 +244,7 @@ describe('UpstashStore', () => {
269
244
 
270
245
  it('should handle mixed date formats in thread operations', async () => {
271
246
  const now = new Date();
272
- const thread = createSampleThread(now);
247
+ const thread = createSampleThread({ date: now });
273
248
 
274
249
  await store.saveThread({ thread });
275
250
  const retrievedThread = await store.getThreadById({ threadId: thread.id });
@@ -281,8 +256,8 @@ describe('UpstashStore', () => {
281
256
 
282
257
  it('should handle date serialization in getThreadsByResourceId', async () => {
283
258
  const now = new Date();
284
- const thread1 = createSampleThread(now);
285
- const thread2 = { ...createSampleThread(now), resourceId: thread1.resourceId };
259
+ const thread1 = createSampleThread({ date: now });
260
+ const thread2 = { ...createSampleThread({ date: now }), resourceId: thread1.resourceId };
286
261
  const threads = [thread1, thread2];
287
262
 
288
263
  await Promise.all(threads.map(thread => store.saveThread({ thread })));
@@ -320,9 +295,9 @@ describe('UpstashStore', () => {
320
295
 
321
296
  it('should save and retrieve messages in order', async () => {
322
297
  const messages: MastraMessageV2[] = [
323
- createSampleMessage(threadId, 'First'),
324
- createSampleMessage(threadId, 'Second'),
325
- createSampleMessage(threadId, 'Third'),
298
+ createSampleMessageV2({ threadId, content: 'First' }),
299
+ createSampleMessageV2({ threadId, content: 'Second' }),
300
+ createSampleMessageV2({ threadId, content: 'Third' }),
326
301
  ];
327
302
 
328
303
  await store.saveMessages({ messages: messages, format: 'v2' });
@@ -366,7 +341,9 @@ describe('UpstashStore', () => {
366
341
  const thread = createSampleThread();
367
342
  await store.saveThread({ thread });
368
343
 
369
- const messages = Array.from({ length: 15 }, (_, i) => createSampleMessage(thread.id, `Message ${i + 1}`));
344
+ const messages = Array.from({ length: 15 }, (_, i) =>
345
+ createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
346
+ );
370
347
 
371
348
  await store.saveMessages({ messages, format: 'v2' });
372
349
 
@@ -408,7 +385,7 @@ describe('UpstashStore', () => {
408
385
  await store.saveThread({ thread });
409
386
 
410
387
  const messages = Array.from({ length: 10 }, (_, i) => {
411
- const message = createSampleMessage(thread.id, `Message ${i + 1}`);
388
+ const message = createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` });
412
389
  // Ensure different timestamps
413
390
  message.createdAt = new Date(Date.now() + i * 1000);
414
391
  return message;
@@ -437,7 +414,9 @@ describe('UpstashStore', () => {
437
414
  const thread = createSampleThread();
438
415
  await store.saveThread({ thread });
439
416
 
440
- const messages = Array.from({ length: 5 }, (_, i) => createSampleMessage(thread.id, `Message ${i + 1}`));
417
+ const messages = Array.from({ length: 5 }, (_, i) =>
418
+ createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
419
+ );
441
420
 
442
421
  await store.saveMessages({ messages, format: 'v2' });
443
422
 
@@ -466,13 +445,13 @@ describe('UpstashStore', () => {
466
445
  const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
467
446
 
468
447
  const oldMessages = Array.from({ length: 3 }, (_, i) => {
469
- const message = createSampleMessage(thread.id, `Old Message ${i + 1}`);
448
+ const message = createSampleMessageV2({ threadId: thread.id, content: `Old Message ${i + 1}` });
470
449
  message.createdAt = yesterday;
471
450
  return message;
472
451
  });
473
452
 
474
453
  const newMessages = Array.from({ length: 4 }, (_, i) => {
475
- const message = createSampleMessage(thread.id, `New Message ${i + 1}`);
454
+ const message = createSampleMessageV2({ threadId: thread.id, content: `New Message ${i + 1}` });
476
455
  message.createdAt = tomorrow;
477
456
  return message;
478
457
  });
@@ -995,6 +974,89 @@ describe('UpstashStore', () => {
995
974
  });
996
975
  });
997
976
 
977
+ describe('alterTable (no-op/schemaless)', () => {
978
+ const TEST_TABLE = 'test_alter_table'; // Use "table" or "collection" as appropriate
979
+ beforeEach(async () => {
980
+ await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
981
+ });
982
+
983
+ afterEach(async () => {
984
+ await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
985
+ });
986
+
987
+ it('allows inserting records with new fields without alterTable', async () => {
988
+ await store.insert({
989
+ tableName: TEST_TABLE as TABLE_NAMES,
990
+ record: { id: '1', name: 'Alice' },
991
+ });
992
+ await store.insert({
993
+ tableName: TEST_TABLE as TABLE_NAMES,
994
+ record: { id: '2', name: 'Bob', newField: 123 },
995
+ });
996
+
997
+ const row = await store.load<{ id: string; name: string; newField?: number }>({
998
+ tableName: TEST_TABLE as TABLE_NAMES,
999
+ keys: { id: '2' },
1000
+ });
1001
+ expect(row?.newField).toBe(123);
1002
+ });
1003
+
1004
+ it('does not throw when calling alterTable (no-op)', async () => {
1005
+ await expect(
1006
+ store.alterTable({
1007
+ tableName: TEST_TABLE as TABLE_NAMES,
1008
+ schema: {
1009
+ id: { type: 'text', primaryKey: true, nullable: false },
1010
+ name: { type: 'text', nullable: true },
1011
+ extra: { type: 'integer', nullable: true },
1012
+ },
1013
+ ifNotExists: [],
1014
+ }),
1015
+ ).resolves.not.toThrow();
1016
+ });
1017
+
1018
+ it('can add multiple new fields at write time', async () => {
1019
+ await store.insert({
1020
+ tableName: TEST_TABLE as TABLE_NAMES,
1021
+ record: { id: '3', name: 'Charlie', age: 30, city: 'Paris' },
1022
+ });
1023
+ const row = await store.load<{ id: string; name: string; age?: number; city?: string }>({
1024
+ tableName: TEST_TABLE as TABLE_NAMES,
1025
+ keys: { id: '3' },
1026
+ });
1027
+ expect(row?.age).toBe(30);
1028
+ expect(row?.city).toBe('Paris');
1029
+ });
1030
+
1031
+ it('can retrieve all fields, including dynamically added ones', async () => {
1032
+ await store.insert({
1033
+ tableName: TEST_TABLE as TABLE_NAMES,
1034
+ record: { id: '4', name: 'Dana', hobby: 'skiing' },
1035
+ });
1036
+ const row = await store.load<{ id: string; name: string; hobby?: string }>({
1037
+ tableName: TEST_TABLE as TABLE_NAMES,
1038
+ keys: { id: '4' },
1039
+ });
1040
+ expect(row?.hobby).toBe('skiing');
1041
+ });
1042
+
1043
+ it('does not restrict or error on arbitrary new fields', async () => {
1044
+ await expect(
1045
+ store.insert({
1046
+ tableName: TEST_TABLE as TABLE_NAMES,
1047
+ record: { id: '5', weirdField: { nested: true }, another: [1, 2, 3] },
1048
+ }),
1049
+ ).resolves.not.toThrow();
1050
+
1051
+ const row = await store.load<{ id: string; weirdField?: any; another?: any }>({
1052
+ tableName: TEST_TABLE as TABLE_NAMES,
1053
+ keys: { id: '5' },
1054
+ });
1055
+ expect(row?.weirdField).toEqual({ nested: true });
1056
+ expect(row?.another).toEqual([1, 2, 3]);
1057
+ });
1058
+ });
1059
+
998
1060
  describe('Pagination Features', () => {
999
1061
  beforeEach(async () => {
1000
1062
  // Clear all test data