@mastra/upstash 0.11.1-alpha.0 → 0.11.1-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.
@@ -321,9 +321,9 @@ describe('UpstashStore', () => {
321
321
 
322
322
  it('should save and retrieve messages in order', async () => {
323
323
  const messages: MastraMessageV2[] = [
324
- createSampleMessageV2({ threadId, content: 'First' }),
325
- createSampleMessageV2({ threadId, content: 'Second' }),
326
- createSampleMessageV2({ threadId, content: 'Third' }),
324
+ createSampleMessageV2({ threadId, content: { content: 'First' } }),
325
+ createSampleMessageV2({ threadId, content: { content: 'Second' } }),
326
+ createSampleMessageV2({ threadId, content: { content: 'Third' } }),
327
327
  ];
328
328
 
329
329
  await store.saveMessages({ messages, format: 'v2' });
@@ -344,16 +344,48 @@ describe('UpstashStore', () => {
344
344
  await store.saveThread({ thread: thread3 });
345
345
 
346
346
  const messages: MastraMessageV2[] = [
347
- createSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
348
- createSampleMessageV2({ threadId: 'thread-one', content: 'Second', resourceId: 'cross-thread-resource' }),
349
- createSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
347
+ createSampleMessageV2({
348
+ threadId: 'thread-one',
349
+ content: { content: 'First' },
350
+ resourceId: 'cross-thread-resource',
351
+ }),
352
+ createSampleMessageV2({
353
+ threadId: 'thread-one',
354
+ content: { content: 'Second' },
355
+ resourceId: 'cross-thread-resource',
356
+ }),
357
+ createSampleMessageV2({
358
+ threadId: 'thread-one',
359
+ content: { content: 'Third' },
360
+ resourceId: 'cross-thread-resource',
361
+ }),
350
362
 
351
- createSampleMessageV2({ threadId: 'thread-two', content: 'Fourth', resourceId: 'cross-thread-resource' }),
352
- createSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
353
- createSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
363
+ createSampleMessageV2({
364
+ threadId: 'thread-two',
365
+ content: { content: 'Fourth' },
366
+ resourceId: 'cross-thread-resource',
367
+ }),
368
+ createSampleMessageV2({
369
+ threadId: 'thread-two',
370
+ content: { content: 'Fifth' },
371
+ resourceId: 'cross-thread-resource',
372
+ }),
373
+ createSampleMessageV2({
374
+ threadId: 'thread-two',
375
+ content: { content: 'Sixth' },
376
+ resourceId: 'cross-thread-resource',
377
+ }),
354
378
 
355
- createSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
356
- createSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
379
+ createSampleMessageV2({
380
+ threadId: 'thread-three',
381
+ content: { content: 'Seventh' },
382
+ resourceId: 'other-resource',
383
+ }),
384
+ createSampleMessageV2({
385
+ threadId: 'thread-three',
386
+ content: { content: 'Eighth' },
387
+ resourceId: 'other-resource',
388
+ }),
357
389
  ];
358
390
 
359
391
  await store.saveMessages({ messages: messages, format: 'v2' });
@@ -466,13 +498,88 @@ describe('UpstashStore', () => {
466
498
  expect(retrievedMessages[0].content).toEqual(messages[0].content);
467
499
  });
468
500
 
501
+ it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
502
+ const thread = await createSampleThread();
503
+ await store.saveThread({ thread });
504
+ const baseMessage = createSampleMessageV2({
505
+ threadId: thread.id,
506
+ createdAt: new Date(),
507
+ content: { content: 'Original' },
508
+ resourceId: thread.resourceId,
509
+ });
510
+
511
+ // Insert the message for the first time
512
+ await store.saveMessages({ messages: [baseMessage], format: 'v2' });
513
+
514
+ // Insert again with the same id and threadId but different content
515
+ const updatedMessage = {
516
+ ...createSampleMessageV2({
517
+ threadId: thread.id,
518
+ createdAt: new Date(),
519
+ content: { content: 'Updated' },
520
+ resourceId: thread.resourceId,
521
+ }),
522
+ id: baseMessage.id,
523
+ };
524
+
525
+ await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
526
+
527
+ // Retrieve messages for the thread
528
+ const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
529
+
530
+ // Only one message should exist for that id+threadId
531
+ expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
532
+
533
+ // The content should be the updated one
534
+ expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
535
+ });
536
+
537
+ it('should upsert messages: duplicate id and different threadid', async () => {
538
+ const thread1 = await createSampleThread();
539
+ const thread2 = await createSampleThread();
540
+ await store.saveThread({ thread: thread1 });
541
+ await store.saveThread({ thread: thread2 });
542
+
543
+ const message = createSampleMessageV2({
544
+ threadId: thread1.id,
545
+ createdAt: new Date(),
546
+ content: { content: 'Thread1 Content' },
547
+ resourceId: thread1.resourceId,
548
+ });
549
+
550
+ // Insert message into thread1
551
+ await store.saveMessages({ messages: [message], format: 'v2' });
552
+
553
+ // Attempt to insert a message with the same id but different threadId
554
+ const conflictingMessage = {
555
+ ...createSampleMessageV2({
556
+ threadId: thread2.id, // different thread
557
+ content: { content: 'Thread2 Content' },
558
+ resourceId: thread2.resourceId,
559
+ }),
560
+ id: message.id,
561
+ };
562
+
563
+ // Save should move the message to the new thread
564
+ await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
565
+
566
+ // Retrieve messages for both threads
567
+ const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
568
+ const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
569
+
570
+ // Thread 1 should NOT have the message with that id
571
+ expect(thread1Messages.find(m => m.id === message.id)).toBeUndefined();
572
+
573
+ // Thread 2 should have the message with that id
574
+ expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
575
+ });
469
576
  describe('getMessagesPaginated', () => {
470
577
  it('should return paginated messages with total count', async () => {
471
578
  const thread = createSampleThread();
472
579
  await store.saveThread({ thread });
473
580
 
474
581
  const messages = Array.from({ length: 15 }, (_, i) =>
475
- createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
582
+ createSampleMessageV2({ threadId: thread.id, content: { content: `Message ${i + 1}` } }),
476
583
  );
477
584
 
478
585
  await store.saveMessages({ messages, format: 'v2' });
@@ -512,7 +619,7 @@ describe('UpstashStore', () => {
512
619
  await store.saveThread({ thread });
513
620
 
514
621
  const messages = Array.from({ length: 10 }, (_, i) => {
515
- const message = createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` });
622
+ const message = createSampleMessageV2({ threadId: thread.id, content: { content: `Message ${i + 1}` } });
516
623
  // Ensure different timestamps
517
624
  message.createdAt = new Date(Date.now() + i * 1000);
518
625
  return message;
@@ -545,13 +652,13 @@ describe('UpstashStore', () => {
545
652
  const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
546
653
 
547
654
  const oldMessages = Array.from({ length: 3 }, (_, i) => {
548
- const message = createSampleMessageV2({ threadId: thread.id, content: `Old Message ${i + 1}` });
655
+ const message = createSampleMessageV2({ threadId: thread.id, content: { content: `Old Message ${i + 1}` } });
549
656
  message.createdAt = yesterday;
550
657
  return message;
551
658
  });
552
659
 
553
660
  const newMessages = Array.from({ length: 4 }, (_, i) => {
554
- const message = createSampleMessageV2({ threadId: thread.id, content: `New Message ${i + 1}` });
661
+ const message = createSampleMessageV2({ threadId: thread.id, content: { content: `New Message ${i + 1}` } });
555
662
  message.createdAt = tomorrow;
556
663
  return message;
557
664
  });
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
2
 
3
+ import type { UpstashVectorFilter } from './filter';
3
4
  import { UpstashFilterTranslator } from './filter';
4
5
 
5
6
  describe('UpstashFilterTranslator', () => {
@@ -22,7 +23,7 @@ describe('UpstashFilterTranslator', () => {
22
23
 
23
24
  it('translates nested paths', () => {
24
25
  expect(translator.translate({ 'geography.continent': 'Asia' })).toBe("geography.continent = 'Asia'");
25
- expect(translator.translate({ geography: { continent: 'Asia' } })).toBe("geography.continent = 'Asia'");
26
+ expect(translator.translate({ geography: { continent: 'Asia' } } as any)).toBe("geography.continent = 'Asia'");
26
27
  });
27
28
 
28
29
  it('translates comparison operators', () => {
@@ -65,7 +66,7 @@ describe('UpstashFilterTranslator', () => {
65
66
  });
66
67
 
67
68
  it('translates complex nested conditions', () => {
68
- const filter = {
69
+ const filter: UpstashVectorFilter = {
69
70
  $and: [
70
71
  { population: { $gte: 1000000 } },
71
72
  { 'geography.continent': 'Asia' },
@@ -154,7 +155,7 @@ describe('UpstashFilterTranslator', () => {
154
155
 
155
156
  describe('complex scenarios', () => {
156
157
  it('deeply nested logical operators', () => {
157
- const filter = {
158
+ const filter: UpstashVectorFilter = {
158
159
  $or: [
159
160
  {
160
161
  $and: [
@@ -214,7 +215,7 @@ describe('UpstashFilterTranslator', () => {
214
215
  });
215
216
 
216
217
  it('complex filtering with all operator types', () => {
217
- const filter = {
218
+ const filter: UpstashVectorFilter = {
218
219
  $and: [
219
220
  {
220
221
  $or: [{ name: { $regex: 'San*' } }, { name: { $regex: 'New*' } }],
@@ -531,7 +532,7 @@ describe('UpstashFilterTranslator', () => {
531
532
  });
532
533
 
533
534
  it('throws error for invalid $not operator', () => {
534
- expect(() => translator.translate({ field: { $not: true } })).toThrow();
535
+ expect(() => translator.translate({ field: { $not: true } } as any)).toThrow();
535
536
  });
536
537
 
537
538
  it('throws error for regex operators', () => {
@@ -539,7 +540,7 @@ describe('UpstashFilterTranslator', () => {
539
540
  expect(() => translator.translate(filter)).toThrow();
540
541
  });
541
542
  it('throws error for non-logical operators at top level', () => {
542
- const invalidFilters = [{ $gt: 100 }, { $in: ['value1', 'value2'] }, { $eq: true }];
543
+ const invalidFilters: any = [{ $gt: 100 }, { $in: ['value1', 'value2'] }, { $eq: true }];
543
544
 
544
545
  invalidFilters.forEach(filter => {
545
546
  expect(() => translator.translate(filter)).toThrow(/Invalid top-level operator/);
@@ -1,7 +1,13 @@
1
1
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
2
- import type { FieldCondition, OperatorSupport, VectorFilter } from '@mastra/core/vector/filter';
2
+ import type { OperatorSupport, VectorFilter, OperatorValueMap } from '@mastra/core/vector/filter';
3
3
 
4
- export class UpstashFilterTranslator extends BaseFilterTranslator {
4
+ type UpstashOperatorValueMap = Omit<OperatorValueMap, '$options' | '$elemMatch'> & {
5
+ $contains: string;
6
+ };
7
+
8
+ export type UpstashVectorFilter = VectorFilter<keyof UpstashOperatorValueMap, UpstashOperatorValueMap>;
9
+
10
+ export class UpstashFilterTranslator extends BaseFilterTranslator<UpstashVectorFilter, string | undefined> {
5
11
  protected override getSupportedOperators(): OperatorSupport {
6
12
  return {
7
13
  ...BaseFilterTranslator.DEFAULT_OPERATORS,
@@ -11,13 +17,13 @@ export class UpstashFilterTranslator extends BaseFilterTranslator {
11
17
  };
12
18
  }
13
19
 
14
- translate(filter?: VectorFilter): string | undefined {
20
+ translate(filter?: UpstashVectorFilter): string | undefined {
15
21
  if (this.isEmpty(filter)) return undefined;
16
22
  this.validateFilter(filter);
17
23
  return this.translateNode(filter);
18
24
  }
19
25
 
20
- private translateNode(node: VectorFilter | FieldCondition, path: string = ''): string {
26
+ private translateNode(node: UpstashVectorFilter, path: string = ''): string {
21
27
  if (this.isRegex(node)) {
22
28
  throw new Error('Direct regex pattern format is not supported in Upstash');
23
29
  }
@@ -1146,7 +1146,7 @@ describe.skipIf(!process.env.UPSTASH_VECTOR_URL || !process.env.UPSTASH_VECTOR_T
1146
1146
  vectorStore.query({
1147
1147
  indexName: filterIndexName,
1148
1148
  queryVector: createVector(0),
1149
- filter: { field: { $invalidOp: 'value' } },
1149
+ filter: { field: { $invalidOp: 'value' } as any },
1150
1150
  }),
1151
1151
  ).rejects.toThrow();
1152
1152
  });
@@ -1196,7 +1196,7 @@ describe.skipIf(!process.env.UPSTASH_VECTOR_URL || !process.env.UPSTASH_VECTOR_T
1196
1196
  const results = await vectorStore.query({
1197
1197
  indexName: filterIndexName,
1198
1198
  queryVector: createVector(0),
1199
- filter: { $and: { not: 'an array' } },
1199
+ filter: { $and: { not: 'an array' } as any },
1200
1200
  });
1201
1201
  expect(results.length).toBeGreaterThan(0);
1202
1202
  });
@@ -1,3 +1,4 @@
1
+ import { MastraError, ErrorDomain, ErrorCategory } from '@mastra/core/error';
1
2
  import { MastraVector } from '@mastra/core/vector';
2
3
  import type {
3
4
  CreateIndexParams,
@@ -10,12 +11,14 @@ import type {
10
11
  UpdateVectorParams,
11
12
  UpsertVectorParams,
12
13
  } from '@mastra/core/vector';
13
- import type { VectorFilter } from '@mastra/core/vector/filter';
14
14
  import { Index } from '@upstash/vector';
15
15
 
16
16
  import { UpstashFilterTranslator } from './filter';
17
+ import type { UpstashVectorFilter } from './filter';
17
18
 
18
- export class UpstashVector extends MastraVector {
19
+ type UpstashQueryVectorParams = QueryVectorParams<UpstashVectorFilter>;
20
+
21
+ export class UpstashVector extends MastraVector<UpstashVectorFilter> {
19
22
  private client: Index;
20
23
 
21
24
  /**
@@ -46,18 +49,30 @@ export class UpstashVector extends MastraVector {
46
49
  metadata: metadata?.[index],
47
50
  }));
48
51
 
49
- await this.client.upsert(points, {
50
- namespace,
51
- });
52
- return generatedIds;
52
+ try {
53
+ await this.client.upsert(points, {
54
+ namespace,
55
+ });
56
+ return generatedIds;
57
+ } catch (error) {
58
+ throw new MastraError(
59
+ {
60
+ id: 'STORAGE_UPSTASH_VECTOR_UPSERT_FAILED',
61
+ domain: ErrorDomain.STORAGE,
62
+ category: ErrorCategory.THIRD_PARTY,
63
+ details: { namespace, vectorCount: vectors.length },
64
+ },
65
+ error,
66
+ );
67
+ }
53
68
  }
54
69
 
55
70
  /**
56
71
  * Transforms a Mastra vector filter into an Upstash-compatible filter string.
57
- * @param {VectorFilter} [filter] - The filter to transform.
72
+ * @param {UpstashVectorFilter} [filter] - The filter to transform.
58
73
  * @returns {string | undefined} The transformed filter string, or undefined if no filter is provided.
59
74
  */
60
- transformFilter(filter?: VectorFilter) {
75
+ transformFilter(filter?: UpstashVectorFilter) {
61
76
  const translator = new UpstashFilterTranslator();
62
77
  return translator.translate(filter);
63
78
  }
@@ -82,25 +97,37 @@ export class UpstashVector extends MastraVector {
82
97
  topK = 10,
83
98
  filter,
84
99
  includeVector = false,
85
- }: QueryVectorParams): Promise<QueryResult[]> {
86
- const ns = this.client.namespace(namespace);
87
-
88
- const filterString = this.transformFilter(filter);
89
- const results = await ns.query({
90
- topK,
91
- vector: queryVector,
92
- includeVectors: includeVector,
93
- includeMetadata: true,
94
- ...(filterString ? { filter: filterString } : {}),
95
- });
100
+ }: UpstashQueryVectorParams): Promise<QueryResult[]> {
101
+ try {
102
+ const ns = this.client.namespace(namespace);
96
103
 
97
- // Map the results to our expected format
98
- return (results || []).map(result => ({
99
- id: `${result.id}`,
100
- score: result.score,
101
- metadata: result.metadata,
102
- ...(includeVector && { vector: result.vector || [] }),
103
- }));
104
+ const filterString = this.transformFilter(filter);
105
+ const results = await ns.query({
106
+ topK,
107
+ vector: queryVector,
108
+ includeVectors: includeVector,
109
+ includeMetadata: true,
110
+ ...(filterString ? { filter: filterString } : {}),
111
+ });
112
+
113
+ // Map the results to our expected format
114
+ return (results || []).map(result => ({
115
+ id: `${result.id}`,
116
+ score: result.score,
117
+ metadata: result.metadata,
118
+ ...(includeVector && { vector: result.vector || [] }),
119
+ }));
120
+ } catch (error) {
121
+ throw new MastraError(
122
+ {
123
+ id: 'STORAGE_UPSTASH_VECTOR_QUERY_FAILED',
124
+ domain: ErrorDomain.STORAGE,
125
+ category: ErrorCategory.THIRD_PARTY,
126
+ details: { namespace, topK },
127
+ },
128
+ error,
129
+ );
130
+ }
104
131
  }
105
132
 
106
133
  /**
@@ -108,8 +135,19 @@ export class UpstashVector extends MastraVector {
108
135
  * @returns {Promise<string[]>} A promise that resolves to a list of index names.
109
136
  */
110
137
  async listIndexes(): Promise<string[]> {
111
- const indexes = await this.client.listNamespaces();
112
- return indexes.filter(Boolean);
138
+ try {
139
+ const indexes = await this.client.listNamespaces();
140
+ return indexes.filter(Boolean);
141
+ } catch (error) {
142
+ throw new MastraError(
143
+ {
144
+ id: 'STORAGE_UPSTASH_VECTOR_LIST_INDEXES_FAILED',
145
+ domain: ErrorDomain.STORAGE,
146
+ category: ErrorCategory.THIRD_PARTY,
147
+ },
148
+ error,
149
+ );
150
+ }
113
151
  }
114
152
 
115
153
  /**
@@ -119,13 +157,25 @@ export class UpstashVector extends MastraVector {
119
157
  * @returns A promise that resolves to the index statistics including dimension, count and metric
120
158
  */
121
159
  async describeIndex({ indexName: namespace }: DescribeIndexParams): Promise<IndexStats> {
122
- const info = await this.client.info();
160
+ try {
161
+ const info = await this.client.info();
123
162
 
124
- return {
125
- dimension: info.dimension,
126
- count: info.namespaces?.[namespace]?.vectorCount || 0,
127
- metric: info?.similarityFunction?.toLowerCase() as 'cosine' | 'euclidean' | 'dotproduct',
128
- };
163
+ return {
164
+ dimension: info.dimension,
165
+ count: info.namespaces?.[namespace]?.vectorCount || 0,
166
+ metric: info?.similarityFunction?.toLowerCase() as 'cosine' | 'euclidean' | 'dotproduct',
167
+ };
168
+ } catch (error) {
169
+ throw new MastraError(
170
+ {
171
+ id: 'STORAGE_UPSTASH_VECTOR_DESCRIBE_INDEX_FAILED',
172
+ domain: ErrorDomain.STORAGE,
173
+ category: ErrorCategory.THIRD_PARTY,
174
+ details: { namespace },
175
+ },
176
+ error,
177
+ );
178
+ }
129
179
  }
130
180
 
131
181
  /**
@@ -137,7 +187,15 @@ export class UpstashVector extends MastraVector {
137
187
  try {
138
188
  await this.client.deleteNamespace(namespace);
139
189
  } catch (error) {
140
- this.logger.error('Failed to delete namespace:', error);
190
+ throw new MastraError(
191
+ {
192
+ id: 'STORAGE_UPSTASH_VECTOR_DELETE_INDEX_FAILED',
193
+ domain: ErrorDomain.STORAGE,
194
+ category: ErrorCategory.THIRD_PARTY,
195
+ details: { namespace },
196
+ },
197
+ error,
198
+ );
141
199
  }
142
200
  }
143
201
 
@@ -162,7 +220,19 @@ export class UpstashVector extends MastraVector {
162
220
  if (!update.vector && update.metadata) {
163
221
  throw new Error('Both vector and metadata must be provided for an update');
164
222
  }
223
+ } catch (error) {
224
+ throw new MastraError(
225
+ {
226
+ id: 'STORAGE_UPSTASH_VECTOR_UPDATE_VECTOR_FAILED',
227
+ domain: ErrorDomain.STORAGE,
228
+ category: ErrorCategory.THIRD_PARTY,
229
+ details: { namespace, id },
230
+ },
231
+ error,
232
+ );
233
+ }
165
234
 
235
+ try {
166
236
  const updatePayload: any = { id: id };
167
237
  if (update.vector) {
168
238
  updatePayload.vector = update.vector;
@@ -180,8 +250,16 @@ export class UpstashVector extends MastraVector {
180
250
  await this.client.upsert(points, {
181
251
  namespace,
182
252
  });
183
- } catch (error: any) {
184
- throw new Error(`Failed to update vector by id: ${id} for index name: ${namespace}: ${error.message}`);
253
+ } catch (error) {
254
+ throw new MastraError(
255
+ {
256
+ id: 'STORAGE_UPSTASH_VECTOR_UPDATE_VECTOR_FAILED',
257
+ domain: ErrorDomain.STORAGE,
258
+ category: ErrorCategory.THIRD_PARTY,
259
+ details: { namespace, id },
260
+ },
261
+ error,
262
+ );
185
263
  }
186
264
  }
187
265
 
@@ -198,7 +276,16 @@ export class UpstashVector extends MastraVector {
198
276
  namespace,
199
277
  });
200
278
  } catch (error) {
201
- this.logger.error(`Failed to delete vector by id: ${id} for namespace: ${namespace}:`, error);
279
+ const mastraError = new MastraError(
280
+ {
281
+ id: 'STORAGE_UPSTASH_VECTOR_DELETE_VECTOR_FAILED',
282
+ domain: ErrorDomain.STORAGE,
283
+ category: ErrorCategory.THIRD_PARTY,
284
+ details: { namespace, id },
285
+ },
286
+ error,
287
+ );
288
+ this.logger?.error(mastraError.toString());
202
289
  }
203
290
  }
204
291
  }