@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/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +11 -0
- package/dist/_tsup-dts-rollup.d.cts +11 -2
- package/dist/_tsup-dts-rollup.d.ts +11 -2
- package/dist/index.cjs +371 -396
- package/dist/index.js +371 -396
- package/package.json +4 -3
- package/src/storage/index.ts +33 -13
- package/src/storage/upstash.test.ts +120 -58
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.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.
|
|
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.
|
|
36
|
+
"@mastra/core": "0.10.4-alpha.1"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
39
|
"@mastra/core": "^0.10.2-alpha.0"
|
package/src/storage/index.ts
CHANGED
|
@@ -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
|
-
|
|
636
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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
|