@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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +19 -0
- package/dist/_tsup-dts-rollup.d.cts +14 -2
- package/dist/_tsup-dts-rollup.d.ts +14 -2
- package/dist/index.cjs +461 -434
- package/dist/index.js +461 -434
- package/package.json +4 -3
- package/src/storage/index.ts +97 -36
- package/src/storage/upstash.test.ts +233 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/upstash",
|
|
3
|
-
"version": "0.10.3-alpha.
|
|
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.
|
|
33
|
+
"vitest": "^3.2.2",
|
|
34
34
|
"@internal/lint": "0.0.10",
|
|
35
|
-
"@
|
|
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"
|
package/src/storage/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
636
|
-
|
|
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
|
|
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
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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 =>
|
|
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 =>
|
|
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
|
-
|
|
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 =
|
|
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 }, () => ({
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
296
|
+
createSampleMessageV2({ threadId, content: 'First' }),
|
|
297
|
+
createSampleMessageV2({ threadId, content: 'Second' }),
|
|
298
|
+
createSampleMessageV2({ threadId, content: 'Third' }),
|
|
326
299
|
];
|
|
327
300
|
|
|
328
|
-
await store.saveMessages({ messages
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 });
|