@mastra/upstash 0.10.3-alpha.0 → 0.10.3-alpha.2

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.2",
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
34
  "@internal/lint": "0.0.10",
35
- "@mastra/core": "0.10.4-alpha.0"
35
+ "@internal/storage-test-utils": "0.0.6",
36
+ "@mastra/core": "0.10.4-alpha.2"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@mastra/core": "^0.10.2-alpha.0"
@@ -36,6 +36,14 @@ export class UpstashStore extends MastraStorage {
36
36
  });
37
37
  }
38
38
 
39
+ public get supports(): {
40
+ selectByIncludeResourceScope: boolean;
41
+ } {
42
+ return {
43
+ selectByIncludeResourceScope: true,
44
+ };
45
+ }
46
+
39
47
  private transformEvalRecord(record: Record<string, any>): EvalRow {
40
48
  // Parse JSON strings if needed
41
49
  let result = record.result;
@@ -93,17 +101,6 @@ export class UpstashStore extends MastraStorage {
93
101
  return `${tableName}:${keyParts.join(':')}`;
94
102
  }
95
103
 
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
104
  /**
108
105
  * Scans for keys matching the given pattern using SCAN and returns them as an array.
109
106
  * @param pattern Redis key pattern, e.g. "table:*"
@@ -147,7 +144,8 @@ export class UpstashStore extends MastraStorage {
147
144
  }
148
145
 
149
146
  private getMessageKey(threadId: string, messageId: string): string {
150
- return this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
147
+ const key = this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
148
+ return key;
151
149
  }
152
150
 
153
151
  private getThreadMessagesKey(threadId: string): string {
@@ -454,6 +452,20 @@ export class UpstashStore extends MastraStorage {
454
452
  await this.redis.set(`schema:${tableName}`, schema);
455
453
  }
456
454
 
455
+ /**
456
+ * No-op: This backend is schemaless and does not require schema changes.
457
+ * @param tableName Name of the table
458
+ * @param schema Schema of the table
459
+ * @param ifNotExists Array of column names to add if they don't exist
460
+ */
461
+ async alterTable(_args: {
462
+ tableName: TABLE_NAMES;
463
+ schema: Record<string, StorageColumn>;
464
+ ifNotExists: string[];
465
+ }): Promise<void> {
466
+ // Nothing to do here, Redis is schemaless
467
+ }
468
+
457
469
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
458
470
  const pattern = `${tableName}:*`;
459
471
  await this.scanAndDelete(pattern);
@@ -632,8 +644,25 @@ export class UpstashStore extends MastraStorage {
632
644
  }
633
645
 
634
646
  async deleteThread({ threadId }: { threadId: string }): Promise<void> {
635
- const key = this.getKey(TABLE_THREADS, { id: threadId });
636
- await this.redis.del(key);
647
+ // Delete thread metadata and sorted set
648
+ const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
649
+ const threadMessagesKey = this.getThreadMessagesKey(threadId);
650
+ const messageIds: string[] = await this.redis.zrange(threadMessagesKey, 0, -1);
651
+
652
+ const pipeline = this.redis.pipeline();
653
+ pipeline.del(threadKey);
654
+ pipeline.del(threadMessagesKey);
655
+
656
+ for (let i = 0; i < messageIds.length; i++) {
657
+ const messageId = messageIds[i];
658
+ const messageKey = this.getMessageKey(threadId, messageId as string);
659
+ pipeline.del(messageKey);
660
+ }
661
+
662
+ await pipeline.exec();
663
+
664
+ // Bulk delete all message keys for this thread if any remain
665
+ await this.scanAndDelete(this.getMessageKey(threadId, '*'));
637
666
  }
638
667
 
639
668
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
@@ -644,6 +673,17 @@ export class UpstashStore extends MastraStorage {
644
673
  const { messages, format = 'v1' } = args;
645
674
  if (messages.length === 0) return [];
646
675
 
676
+ const threadId = messages[0]?.threadId;
677
+ if (!threadId) {
678
+ throw new Error('Thread ID is required');
679
+ }
680
+
681
+ // Check if thread exists
682
+ const thread = await this.getThreadById({ threadId });
683
+ if (!thread) {
684
+ throw new Error(`Thread ${threadId} not found`);
685
+ }
686
+
647
687
  // Add an index to each message to maintain order
648
688
  const messagesWithIndex = messages.map((message, index) => ({
649
689
  ...message,
@@ -656,7 +696,8 @@ export class UpstashStore extends MastraStorage {
656
696
  const pipeline = this.redis.pipeline();
657
697
  for (const message of batch) {
658
698
  const key = this.getMessageKey(message.threadId!, message.id);
659
- const score = message._index !== undefined ? message._index : new Date(message.createdAt).getTime();
699
+ const createdAtScore = new Date(message.createdAt).getTime();
700
+ const score = message._index !== undefined ? message._index : createdAtScore;
660
701
 
661
702
  // Store the message data
662
703
  pipeline.set(key, message);
@@ -824,6 +865,7 @@ export class UpstashStore extends MastraStorage {
824
865
  }
825
866
 
826
867
  const messageIds = new Set<string>();
868
+ const messageIdToThreadIds: Record<string, string> = {};
827
869
 
828
870
  if (limit === 0 && !selectBy?.include) {
829
871
  return [];
@@ -834,23 +876,32 @@ export class UpstashStore extends MastraStorage {
834
876
  for (const item of selectBy.include) {
835
877
  messageIds.add(item.id);
836
878
 
837
- if (item.withPreviousMessages || item.withNextMessages) {
838
- // Get the rank of this message in the sorted set
839
- const rank = await this.redis.zrank(threadMessagesKey, item.id);
840
- if (rank === null) continue;
841
-
842
- // Get previous messages if requested
843
- if (item.withPreviousMessages) {
844
- const start = Math.max(0, rank - item.withPreviousMessages);
845
- const prevIds = rank === 0 ? [] : await this.redis.zrange(threadMessagesKey, start, rank - 1);
846
- prevIds.forEach(id => messageIds.add(id as string));
847
- }
879
+ // Use per-include threadId if present, else fallback to main threadId
880
+ const itemThreadId = item.threadId || threadId;
881
+ messageIdToThreadIds[item.id] = itemThreadId;
882
+ const itemThreadMessagesKey = this.getThreadMessagesKey(itemThreadId);
883
+
884
+ // Get the rank of this message in the sorted set
885
+ const rank = await this.redis.zrank(itemThreadMessagesKey, item.id);
886
+ if (rank === null) continue;
887
+
888
+ // Get previous messages if requested
889
+ if (item.withPreviousMessages) {
890
+ const start = Math.max(0, rank - item.withPreviousMessages);
891
+ const prevIds = rank === 0 ? [] : await this.redis.zrange(itemThreadMessagesKey, start, rank - 1);
892
+ prevIds.forEach(id => {
893
+ messageIds.add(id as string);
894
+ messageIdToThreadIds[id as string] = itemThreadId;
895
+ });
896
+ }
848
897
 
849
- // Get next messages if requested
850
- if (item.withNextMessages) {
851
- const nextIds = await this.redis.zrange(threadMessagesKey, rank + 1, rank + item.withNextMessages);
852
- nextIds.forEach(id => messageIds.add(id as string));
853
- }
898
+ // Get next messages if requested
899
+ if (item.withNextMessages) {
900
+ const nextIds = await this.redis.zrange(itemThreadMessagesKey, rank + 1, rank + item.withNextMessages);
901
+ nextIds.forEach(id => {
902
+ messageIds.add(id as string);
903
+ messageIdToThreadIds[id as string] = itemThreadId;
904
+ });
854
905
  }
855
906
  }
856
907
  }
@@ -859,19 +910,29 @@ export class UpstashStore extends MastraStorage {
859
910
  if (limit === Number.MAX_SAFE_INTEGER) {
860
911
  // Get all messages
861
912
  const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
862
- allIds.forEach(id => messageIds.add(id as string));
913
+ allIds.forEach(id => {
914
+ messageIds.add(id as string);
915
+ messageIdToThreadIds[id as string] = threadId;
916
+ });
863
917
  } else if (limit > 0) {
864
918
  // Get limited number of recent messages
865
919
  const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
866
- latestIds.forEach(id => messageIds.add(id as string));
920
+ latestIds.forEach(id => {
921
+ messageIds.add(id as string);
922
+ messageIdToThreadIds[id as string] = threadId;
923
+ });
867
924
  }
868
925
 
869
926
  // Fetch all needed messages in parallel
870
927
  const messages = (
871
928
  await Promise.all(
872
- Array.from(messageIds).map(async id =>
873
- this.redis.get<MastraMessageV2 & { _index?: number }>(this.getMessageKey(threadId, id)),
874
- ),
929
+ Array.from(messageIds).map(async id => {
930
+ const tId = messageIdToThreadIds[id] || threadId;
931
+ const byThreadId = await this.redis.get<MastraMessageV2 & { _index?: number }>(this.getMessageKey(tId, id));
932
+ if (byThreadId) return byThreadId;
933
+
934
+ return null;
935
+ }),
875
936
  )
876
937
  ).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[];
877
938
 
@@ -1,4 +1,10 @@
1
1
  import { randomUUID } from 'crypto';
2
+ import {
3
+ checkWorkflowSnapshot,
4
+ createSampleMessageV2,
5
+ createSampleThread,
6
+ createSampleWorkflowSnapshot,
7
+ } from '@internal/storage-test-utils';
2
8
  import type { MastraMessageV2 } from '@mastra/core';
3
9
  import type { TABLE_NAMES } from '@mastra/core/storage';
4
10
  import {
@@ -9,56 +15,13 @@ import {
9
15
  TABLE_TRACES,
10
16
  } from '@mastra/core/storage';
11
17
  import type { WorkflowRunState } from '@mastra/core/workflows';
12
- import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest';
18
+ import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
13
19
 
14
20
  import { UpstashStore } from './index';
15
21
 
16
22
  // Increase timeout for all tests in this file to 30 seconds
17
23
  vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
18
24
 
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
25
  const createSampleTrace = (name: string, scope?: string, attributes?: Record<string, string>) => ({
63
26
  id: `trace-${randomUUID()}`,
64
27
  parentSpanId: `span-${randomUUID()}`,
@@ -93,13 +56,6 @@ const createSampleEval = (agentName: string, isTest = false) => {
93
56
  };
94
57
  };
95
58
 
96
- const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
97
- if (typeof snapshot === 'string') {
98
- throw new Error('Expected WorkflowRunState, got string');
99
- }
100
- expect(snapshot.context?.[stepId]?.status).toBe(status);
101
- };
102
-
103
59
  describe('UpstashStore', () => {
104
60
  let store: UpstashStore;
105
61
  const testTableName = 'test_table';
@@ -176,7 +132,7 @@ describe('UpstashStore', () => {
176
132
 
177
133
  it('should create and retrieve a thread', async () => {
178
134
  const now = new Date();
179
- const thread = createSampleThread(now);
135
+ const thread = createSampleThread({ date: now });
180
136
 
181
137
  const savedThread = await store.saveThread({ thread });
182
138
  expect(savedThread).toEqual(thread);
@@ -196,7 +152,7 @@ describe('UpstashStore', () => {
196
152
 
197
153
  it('should get threads by resource ID', async () => {
198
154
  const thread1 = createSampleThread();
199
- const thread2 = { ...createSampleThread(), resourceId: thread1.resourceId };
155
+ const thread2 = createSampleThread({ resourceId: thread1.resourceId });
200
156
  const threads = [thread1, thread2];
201
157
 
202
158
  const resourceId = threads[0].resourceId;
@@ -229,13 +185,30 @@ describe('UpstashStore', () => {
229
185
  it('should fetch >100000 threads by resource ID', async () => {
230
186
  const resourceId = `resource-${randomUUID()}`;
231
187
  const total = 100_000;
232
- const threads = Array.from({ length: total }, () => ({ ...createSampleThread(), resourceId }));
188
+ const threads = Array.from({ length: total }, () => createSampleThread({ resourceId }));
233
189
 
234
190
  await store.batchInsert({ tableName: TABLE_THREADS, records: threads });
235
191
 
236
192
  const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
237
193
  expect(retrievedThreads).toHaveLength(total);
238
194
  });
195
+ it('should delete thread and its messages', async () => {
196
+ const thread = createSampleThread();
197
+ await store.saveThread({ thread });
198
+
199
+ // Add some messages
200
+ const messages = [createSampleMessageV2({ threadId: thread.id }), createSampleMessageV2({ threadId: thread.id })];
201
+ await store.saveMessages({ messages, format: 'v2' });
202
+
203
+ await store.deleteThread({ threadId: thread.id });
204
+
205
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
206
+ expect(retrievedThread).toBeNull();
207
+
208
+ // Verify messages were also deleted
209
+ const retrievedMessages = await store.getMessages({ threadId: thread.id });
210
+ expect(retrievedMessages).toHaveLength(0);
211
+ });
239
212
  });
240
213
 
241
214
  describe('Date Handling', () => {
@@ -245,7 +218,7 @@ describe('UpstashStore', () => {
245
218
 
246
219
  it('should handle Date objects in thread operations', async () => {
247
220
  const now = new Date();
248
- const thread = createSampleThread(now);
221
+ const thread = createSampleThread({ date: now });
249
222
 
250
223
  await store.saveThread({ thread });
251
224
  const retrievedThread = await store.getThreadById({ threadId: thread.id });
@@ -257,7 +230,7 @@ describe('UpstashStore', () => {
257
230
 
258
231
  it('should handle ISO string dates in thread operations', async () => {
259
232
  const now = new Date();
260
- const thread = createSampleThread(now);
233
+ const thread = createSampleThread({ date: now });
261
234
 
262
235
  await store.saveThread({ thread });
263
236
  const retrievedThread = await store.getThreadById({ threadId: thread.id });
@@ -269,7 +242,7 @@ describe('UpstashStore', () => {
269
242
 
270
243
  it('should handle mixed date formats in thread operations', async () => {
271
244
  const now = new Date();
272
- const thread = createSampleThread(now);
245
+ const thread = createSampleThread({ date: now });
273
246
 
274
247
  await store.saveThread({ thread });
275
248
  const retrievedThread = await store.getThreadById({ threadId: thread.id });
@@ -281,8 +254,8 @@ describe('UpstashStore', () => {
281
254
 
282
255
  it('should handle date serialization in getThreadsByResourceId', async () => {
283
256
  const now = new Date();
284
- const thread1 = createSampleThread(now);
285
- const thread2 = { ...createSampleThread(now), resourceId: thread1.resourceId };
257
+ const thread1 = createSampleThread({ date: now });
258
+ const thread2 = { ...createSampleThread({ date: now }), resourceId: thread1.resourceId };
286
259
  const threads = [thread1, thread2];
287
260
 
288
261
  await Promise.all(threads.map(thread => store.saveThread({ thread })));
@@ -320,18 +293,122 @@ describe('UpstashStore', () => {
320
293
 
321
294
  it('should save and retrieve messages in order', async () => {
322
295
  const messages: MastraMessageV2[] = [
323
- createSampleMessage(threadId, 'First'),
324
- createSampleMessage(threadId, 'Second'),
325
- createSampleMessage(threadId, 'Third'),
296
+ createSampleMessageV2({ threadId, content: 'First' }),
297
+ createSampleMessageV2({ threadId, content: 'Second' }),
298
+ createSampleMessageV2({ threadId, content: 'Third' }),
326
299
  ];
327
300
 
328
- await store.saveMessages({ messages: messages, format: 'v2' });
301
+ await store.saveMessages({ messages, format: 'v2' });
329
302
 
330
303
  const retrievedMessages = await store.getMessages({ threadId, format: 'v2' });
331
304
  expect(retrievedMessages).toHaveLength(3);
332
305
  expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
333
306
  });
334
307
 
308
+ it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
309
+ const thread = createSampleThread({ id: 'thread-one' });
310
+ await store.saveThread({ thread });
311
+
312
+ const thread2 = createSampleThread({ id: 'thread-two' });
313
+ await store.saveThread({ thread: thread2 });
314
+
315
+ const thread3 = createSampleThread({ id: 'thread-three' });
316
+ await store.saveThread({ thread: thread3 });
317
+
318
+ const messages: MastraMessageV2[] = [
319
+ createSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
320
+ createSampleMessageV2({ threadId: 'thread-one', content: 'Second', resourceId: 'cross-thread-resource' }),
321
+ createSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
322
+
323
+ createSampleMessageV2({ threadId: 'thread-two', content: 'Fourth', resourceId: 'cross-thread-resource' }),
324
+ createSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
325
+ createSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
326
+
327
+ createSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
328
+ createSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
329
+ ];
330
+
331
+ await store.saveMessages({ messages: messages, format: 'v2' });
332
+
333
+ const retrievedMessages = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
334
+ expect(retrievedMessages).toHaveLength(3);
335
+ expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
336
+
337
+ const retrievedMessages2 = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
338
+ expect(retrievedMessages2).toHaveLength(3);
339
+ expect(retrievedMessages2.map((m: any) => m.content.parts[0].text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
340
+
341
+ const retrievedMessages3 = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
342
+ expect(retrievedMessages3).toHaveLength(2);
343
+ expect(retrievedMessages3.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
344
+
345
+ const crossThreadMessages = await store.getMessages({
346
+ threadId: 'thread-doesnt-exist',
347
+ format: 'v2',
348
+ selectBy: {
349
+ last: 0,
350
+ include: [
351
+ {
352
+ id: messages[1].id,
353
+ threadId: 'thread-one',
354
+ withNextMessages: 2,
355
+ withPreviousMessages: 2,
356
+ },
357
+ {
358
+ id: messages[4].id,
359
+ threadId: 'thread-two',
360
+ withPreviousMessages: 2,
361
+ withNextMessages: 2,
362
+ },
363
+ ],
364
+ },
365
+ });
366
+
367
+ expect(crossThreadMessages).toHaveLength(6);
368
+ expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
369
+ expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
370
+
371
+ const crossThreadMessages2 = await store.getMessages({
372
+ threadId: 'thread-one',
373
+ format: 'v2',
374
+ selectBy: {
375
+ last: 0,
376
+ include: [
377
+ {
378
+ id: messages[4].id,
379
+ threadId: 'thread-two',
380
+ withPreviousMessages: 1,
381
+ withNextMessages: 1,
382
+ },
383
+ ],
384
+ },
385
+ });
386
+
387
+ expect(crossThreadMessages2).toHaveLength(3);
388
+ expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
389
+ expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
390
+
391
+ const crossThreadMessages3 = await store.getMessages({
392
+ threadId: 'thread-two',
393
+ format: 'v2',
394
+ selectBy: {
395
+ last: 0,
396
+ include: [
397
+ {
398
+ id: messages[1].id,
399
+ threadId: 'thread-one',
400
+ withNextMessages: 1,
401
+ withPreviousMessages: 1,
402
+ },
403
+ ],
404
+ },
405
+ });
406
+
407
+ expect(crossThreadMessages3).toHaveLength(3);
408
+ expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
409
+ expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
410
+ });
411
+
335
412
  it('should handle empty message array', async () => {
336
413
  const result = await store.saveMessages({ messages: [] });
337
414
  expect(result).toEqual([]);
@@ -366,7 +443,9 @@ describe('UpstashStore', () => {
366
443
  const thread = createSampleThread();
367
444
  await store.saveThread({ thread });
368
445
 
369
- const messages = Array.from({ length: 15 }, (_, i) => createSampleMessage(thread.id, `Message ${i + 1}`));
446
+ const messages = Array.from({ length: 15 }, (_, i) =>
447
+ createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
448
+ );
370
449
 
371
450
  await store.saveMessages({ messages, format: 'v2' });
372
451
 
@@ -408,7 +487,7 @@ describe('UpstashStore', () => {
408
487
  await store.saveThread({ thread });
409
488
 
410
489
  const messages = Array.from({ length: 10 }, (_, i) => {
411
- const message = createSampleMessage(thread.id, `Message ${i + 1}`);
490
+ const message = createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` });
412
491
  // Ensure different timestamps
413
492
  message.createdAt = new Date(Date.now() + i * 1000);
414
493
  return message;
@@ -437,7 +516,9 @@ describe('UpstashStore', () => {
437
516
  const thread = createSampleThread();
438
517
  await store.saveThread({ thread });
439
518
 
440
- const messages = Array.from({ length: 5 }, (_, i) => createSampleMessage(thread.id, `Message ${i + 1}`));
519
+ const messages = Array.from({ length: 5 }, (_, i) =>
520
+ createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
521
+ );
441
522
 
442
523
  await store.saveMessages({ messages, format: 'v2' });
443
524
 
@@ -466,13 +547,13 @@ describe('UpstashStore', () => {
466
547
  const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
467
548
 
468
549
  const oldMessages = Array.from({ length: 3 }, (_, i) => {
469
- const message = createSampleMessage(thread.id, `Old Message ${i + 1}`);
550
+ const message = createSampleMessageV2({ threadId: thread.id, content: `Old Message ${i + 1}` });
470
551
  message.createdAt = yesterday;
471
552
  return message;
472
553
  });
473
554
 
474
555
  const newMessages = Array.from({ length: 4 }, (_, i) => {
475
- const message = createSampleMessage(thread.id, `New Message ${i + 1}`);
556
+ const message = createSampleMessageV2({ threadId: thread.id, content: `New Message ${i + 1}` });
476
557
  message.createdAt = tomorrow;
477
558
  return message;
478
559
  });
@@ -995,6 +1076,89 @@ describe('UpstashStore', () => {
995
1076
  });
996
1077
  });
997
1078
 
1079
+ describe('alterTable (no-op/schemaless)', () => {
1080
+ const TEST_TABLE = 'test_alter_table'; // Use "table" or "collection" as appropriate
1081
+ beforeEach(async () => {
1082
+ await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
1083
+ });
1084
+
1085
+ afterEach(async () => {
1086
+ await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
1087
+ });
1088
+
1089
+ it('allows inserting records with new fields without alterTable', async () => {
1090
+ await store.insert({
1091
+ tableName: TEST_TABLE as TABLE_NAMES,
1092
+ record: { id: '1', name: 'Alice' },
1093
+ });
1094
+ await store.insert({
1095
+ tableName: TEST_TABLE as TABLE_NAMES,
1096
+ record: { id: '2', name: 'Bob', newField: 123 },
1097
+ });
1098
+
1099
+ const row = await store.load<{ id: string; name: string; newField?: number }>({
1100
+ tableName: TEST_TABLE as TABLE_NAMES,
1101
+ keys: { id: '2' },
1102
+ });
1103
+ expect(row?.newField).toBe(123);
1104
+ });
1105
+
1106
+ it('does not throw when calling alterTable (no-op)', async () => {
1107
+ await expect(
1108
+ store.alterTable({
1109
+ tableName: TEST_TABLE as TABLE_NAMES,
1110
+ schema: {
1111
+ id: { type: 'text', primaryKey: true, nullable: false },
1112
+ name: { type: 'text', nullable: true },
1113
+ extra: { type: 'integer', nullable: true },
1114
+ },
1115
+ ifNotExists: [],
1116
+ }),
1117
+ ).resolves.not.toThrow();
1118
+ });
1119
+
1120
+ it('can add multiple new fields at write time', async () => {
1121
+ await store.insert({
1122
+ tableName: TEST_TABLE as TABLE_NAMES,
1123
+ record: { id: '3', name: 'Charlie', age: 30, city: 'Paris' },
1124
+ });
1125
+ const row = await store.load<{ id: string; name: string; age?: number; city?: string }>({
1126
+ tableName: TEST_TABLE as TABLE_NAMES,
1127
+ keys: { id: '3' },
1128
+ });
1129
+ expect(row?.age).toBe(30);
1130
+ expect(row?.city).toBe('Paris');
1131
+ });
1132
+
1133
+ it('can retrieve all fields, including dynamically added ones', async () => {
1134
+ await store.insert({
1135
+ tableName: TEST_TABLE as TABLE_NAMES,
1136
+ record: { id: '4', name: 'Dana', hobby: 'skiing' },
1137
+ });
1138
+ const row = await store.load<{ id: string; name: string; hobby?: string }>({
1139
+ tableName: TEST_TABLE as TABLE_NAMES,
1140
+ keys: { id: '4' },
1141
+ });
1142
+ expect(row?.hobby).toBe('skiing');
1143
+ });
1144
+
1145
+ it('does not restrict or error on arbitrary new fields', async () => {
1146
+ await expect(
1147
+ store.insert({
1148
+ tableName: TEST_TABLE as TABLE_NAMES,
1149
+ record: { id: '5', weirdField: { nested: true }, another: [1, 2, 3] },
1150
+ }),
1151
+ ).resolves.not.toThrow();
1152
+
1153
+ const row = await store.load<{ id: string; weirdField?: any; another?: any }>({
1154
+ tableName: TEST_TABLE as TABLE_NAMES,
1155
+ keys: { id: '5' },
1156
+ });
1157
+ expect(row?.weirdField).toEqual({ nested: true });
1158
+ expect(row?.another).toEqual([1, 2, 3]);
1159
+ });
1160
+ });
1161
+
998
1162
  describe('Pagination Features', () => {
999
1163
  beforeEach(async () => {
1000
1164
  // Clear all test data
@@ -1157,10 +1321,7 @@ describe('UpstashStore', () => {
1157
1321
  describe('Enhanced existing methods with pagination', () => {
1158
1322
  it('should support pagination in getThreadsByResourceId', async () => {
1159
1323
  const resourceId = 'enhanced-resource';
1160
- const threads = Array.from({ length: 17 }, () => ({
1161
- ...createSampleThread(),
1162
- resourceId,
1163
- }));
1324
+ const threads = Array.from({ length: 17 }, () => createSampleThread({ resourceId }));
1164
1325
 
1165
1326
  for (const thread of threads) {
1166
1327
  await store.saveThread({ thread });