@mastra/upstash 0.11.1-alpha.1 → 0.12.0-alpha.3

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.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/upstash@0.11.1-alpha.1 build /home/runner/work/mastra/mastra/stores/upstash
2
+ > @mastra/upstash@0.12.0-alpha.3 build /home/runner/work/mastra/mastra/stores/upstash
3
3
  > tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.5.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 10322ms
9
+ TSC ⚡️ Build success in 10415ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/upstash/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/stores/upstash/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 11265ms
16
+ DTS ⚡️ Build success in 12348ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- CJS dist/index.cjs 55.79 KB
21
- CJS ⚡️ Build success in 1691ms
22
- ESM dist/index.js 54.97 KB
23
- ESM ⚡️ Build success in 1693ms
20
+ CJS dist/index.cjs 58.93 KB
21
+ CJS ⚡️ Build success in 2021ms
22
+ ESM dist/index.js 58.11 KB
23
+ ESM ⚡️ Build success in 2039ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @mastra/upstash
2
2
 
3
+ ## 0.12.0-alpha.3
4
+
5
+ ### Minor Changes
6
+
7
+ - 8a3bfd2: Update peerdeps to latest core
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [792c4c0]
12
+ - Updated dependencies [502fe05]
13
+ - Updated dependencies [4efcfa0]
14
+ - @mastra/core@0.10.7-alpha.3
15
+
16
+ ## 0.11.1-alpha.2
17
+
18
+ ### Patch Changes
19
+
20
+ - 15e9d26: Added per-resource working memory for LibSQL, Upstash, and PG
21
+ - 0fb9d64: [MASTRA-4018] Update saveMessages in storage adapters to upsert messages
22
+ - Updated dependencies [15e9d26]
23
+ - Updated dependencies [07d6d88]
24
+ - Updated dependencies [5d74aab]
25
+ - Updated dependencies [144eb0b]
26
+ - @mastra/core@0.10.7-alpha.2
27
+
3
28
  ## 0.11.1-alpha.1
4
29
 
5
30
  ### Patch Changes
@@ -11,6 +11,7 @@ import type { MastraMessageV2 } from '@mastra/core/agent';
11
11
  import { MastraStorage } from '@mastra/core/storage';
12
12
  import { MastraVector } from '@mastra/core/vector';
13
13
  import type { OperatorSupport } from '@mastra/core/vector/filter';
14
+ import type { OperatorValueMap } from '@mastra/core/vector/filter';
14
15
  import type { PaginationArgs } from '@mastra/core/storage';
15
16
  import type { PaginationInfo } from '@mastra/core/storage';
16
17
  import type { QueryResult } from '@mastra/core/vector';
@@ -18,6 +19,7 @@ import type { QueryVectorParams } from '@mastra/core/vector';
18
19
  import type { StorageColumn } from '@mastra/core/storage';
19
20
  import type { StorageGetMessagesArg } from '@mastra/core/storage';
20
21
  import type { StorageGetTracesArg } from '@mastra/core/storage';
22
+ import type { StorageResourceType } from '@mastra/core/storage';
21
23
  import type { StorageThreadType } from '@mastra/core/memory';
22
24
  import type { TABLE_NAMES } from '@mastra/core/storage';
23
25
  import type { UpdateVectorParams } from '@mastra/core/vector';
@@ -42,9 +44,9 @@ declare interface UpstashConfig {
42
44
  export { UpstashConfig }
43
45
  export { UpstashConfig as UpstashConfig_alias_1 }
44
46
 
45
- export declare class UpstashFilterTranslator extends BaseFilterTranslator {
47
+ export declare class UpstashFilterTranslator extends BaseFilterTranslator<UpstashVectorFilter, string | undefined> {
46
48
  protected getSupportedOperators(): OperatorSupport;
47
- translate(filter?: VectorFilter): string | undefined;
49
+ translate(filter?: UpstashVectorFilter): string | undefined;
48
50
  private translateNode;
49
51
  private readonly COMPARISON_OPS;
50
52
  private translateOperator;
@@ -56,11 +58,18 @@ export declare class UpstashFilterTranslator extends BaseFilterTranslator {
56
58
  private joinConditions;
57
59
  }
58
60
 
61
+ declare type UpstashOperatorValueMap = Omit<OperatorValueMap, '$options' | '$elemMatch'> & {
62
+ $contains: string;
63
+ };
64
+
65
+ declare type UpstashQueryVectorParams = QueryVectorParams<UpstashVectorFilter>;
66
+
59
67
  declare class UpstashStore extends MastraStorage {
60
68
  private redis;
61
69
  constructor(config: UpstashConfig);
62
70
  get supports(): {
63
71
  selectByIncludeResourceScope: boolean;
72
+ resourceWorkingMemory: boolean;
64
73
  };
65
74
  private transformEvalRecord;
66
75
  private parseJSON;
@@ -221,11 +230,22 @@ declare class UpstashStore extends MastraStorage {
221
230
  };
222
231
  }[];
223
232
  }): Promise<MastraMessageV2[]>;
233
+ getResourceById({ resourceId }: {
234
+ resourceId: string;
235
+ }): Promise<StorageResourceType | null>;
236
+ saveResource({ resource }: {
237
+ resource: StorageResourceType;
238
+ }): Promise<StorageResourceType>;
239
+ updateResource({ resourceId, workingMemory, metadata, }: {
240
+ resourceId: string;
241
+ workingMemory?: string;
242
+ metadata?: Record<string, unknown>;
243
+ }): Promise<StorageResourceType>;
224
244
  }
225
245
  export { UpstashStore }
226
246
  export { UpstashStore as UpstashStore_alias_1 }
227
247
 
228
- declare class UpstashVector extends MastraVector {
248
+ declare class UpstashVector extends MastraVector<UpstashVectorFilter> {
229
249
  private client;
230
250
  /**
231
251
  * Creates a new UpstashVector instance.
@@ -245,10 +265,10 @@ declare class UpstashVector extends MastraVector {
245
265
  upsert({ indexName: namespace, vectors, metadata, ids }: UpsertVectorParams): Promise<string[]>;
246
266
  /**
247
267
  * Transforms a Mastra vector filter into an Upstash-compatible filter string.
248
- * @param {VectorFilter} [filter] - The filter to transform.
268
+ * @param {UpstashVectorFilter} [filter] - The filter to transform.
249
269
  * @returns {string | undefined} The transformed filter string, or undefined if no filter is provided.
250
270
  */
251
- transformFilter(filter?: VectorFilter): string | undefined;
271
+ transformFilter(filter?: UpstashVectorFilter): string | undefined;
252
272
  /**
253
273
  * Creates a new index. For Upstash, this is a no-op as indexes (known as namespaces in Upstash) are created on-the-fly.
254
274
  * @param {CreateIndexParams} _params - The parameters for creating the index (ignored).
@@ -260,7 +280,7 @@ declare class UpstashVector extends MastraVector {
260
280
  * @param {QueryVectorParams} params - The parameters for the query operation. indexName is the namespace in Upstash.
261
281
  * @returns {Promise<QueryResult[]>} A promise that resolves to the query results.
262
282
  */
263
- query({ indexName: namespace, queryVector, topK, filter, includeVector, }: QueryVectorParams): Promise<QueryResult[]>;
283
+ query({ indexName: namespace, queryVector, topK, filter, includeVector, }: UpstashQueryVectorParams): Promise<QueryResult[]>;
264
284
  /**
265
285
  * Lists all namespaces in the Upstash vector index, which correspond to indexes.
266
286
  * @returns {Promise<string[]>} A promise that resolves to a list of index names.
@@ -302,4 +322,6 @@ declare class UpstashVector extends MastraVector {
302
322
  export { UpstashVector }
303
323
  export { UpstashVector as UpstashVector_alias_1 }
304
324
 
325
+ export declare type UpstashVectorFilter = VectorFilter<keyof UpstashOperatorValueMap, UpstashOperatorValueMap>;
326
+
305
327
  export { }
@@ -11,6 +11,7 @@ import type { MastraMessageV2 } from '@mastra/core/agent';
11
11
  import { MastraStorage } from '@mastra/core/storage';
12
12
  import { MastraVector } from '@mastra/core/vector';
13
13
  import type { OperatorSupport } from '@mastra/core/vector/filter';
14
+ import type { OperatorValueMap } from '@mastra/core/vector/filter';
14
15
  import type { PaginationArgs } from '@mastra/core/storage';
15
16
  import type { PaginationInfo } from '@mastra/core/storage';
16
17
  import type { QueryResult } from '@mastra/core/vector';
@@ -18,6 +19,7 @@ import type { QueryVectorParams } from '@mastra/core/vector';
18
19
  import type { StorageColumn } from '@mastra/core/storage';
19
20
  import type { StorageGetMessagesArg } from '@mastra/core/storage';
20
21
  import type { StorageGetTracesArg } from '@mastra/core/storage';
22
+ import type { StorageResourceType } from '@mastra/core/storage';
21
23
  import type { StorageThreadType } from '@mastra/core/memory';
22
24
  import type { TABLE_NAMES } from '@mastra/core/storage';
23
25
  import type { UpdateVectorParams } from '@mastra/core/vector';
@@ -42,9 +44,9 @@ declare interface UpstashConfig {
42
44
  export { UpstashConfig }
43
45
  export { UpstashConfig as UpstashConfig_alias_1 }
44
46
 
45
- export declare class UpstashFilterTranslator extends BaseFilterTranslator {
47
+ export declare class UpstashFilterTranslator extends BaseFilterTranslator<UpstashVectorFilter, string | undefined> {
46
48
  protected getSupportedOperators(): OperatorSupport;
47
- translate(filter?: VectorFilter): string | undefined;
49
+ translate(filter?: UpstashVectorFilter): string | undefined;
48
50
  private translateNode;
49
51
  private readonly COMPARISON_OPS;
50
52
  private translateOperator;
@@ -56,11 +58,18 @@ export declare class UpstashFilterTranslator extends BaseFilterTranslator {
56
58
  private joinConditions;
57
59
  }
58
60
 
61
+ declare type UpstashOperatorValueMap = Omit<OperatorValueMap, '$options' | '$elemMatch'> & {
62
+ $contains: string;
63
+ };
64
+
65
+ declare type UpstashQueryVectorParams = QueryVectorParams<UpstashVectorFilter>;
66
+
59
67
  declare class UpstashStore extends MastraStorage {
60
68
  private redis;
61
69
  constructor(config: UpstashConfig);
62
70
  get supports(): {
63
71
  selectByIncludeResourceScope: boolean;
72
+ resourceWorkingMemory: boolean;
64
73
  };
65
74
  private transformEvalRecord;
66
75
  private parseJSON;
@@ -221,11 +230,22 @@ declare class UpstashStore extends MastraStorage {
221
230
  };
222
231
  }[];
223
232
  }): Promise<MastraMessageV2[]>;
233
+ getResourceById({ resourceId }: {
234
+ resourceId: string;
235
+ }): Promise<StorageResourceType | null>;
236
+ saveResource({ resource }: {
237
+ resource: StorageResourceType;
238
+ }): Promise<StorageResourceType>;
239
+ updateResource({ resourceId, workingMemory, metadata, }: {
240
+ resourceId: string;
241
+ workingMemory?: string;
242
+ metadata?: Record<string, unknown>;
243
+ }): Promise<StorageResourceType>;
224
244
  }
225
245
  export { UpstashStore }
226
246
  export { UpstashStore as UpstashStore_alias_1 }
227
247
 
228
- declare class UpstashVector extends MastraVector {
248
+ declare class UpstashVector extends MastraVector<UpstashVectorFilter> {
229
249
  private client;
230
250
  /**
231
251
  * Creates a new UpstashVector instance.
@@ -245,10 +265,10 @@ declare class UpstashVector extends MastraVector {
245
265
  upsert({ indexName: namespace, vectors, metadata, ids }: UpsertVectorParams): Promise<string[]>;
246
266
  /**
247
267
  * Transforms a Mastra vector filter into an Upstash-compatible filter string.
248
- * @param {VectorFilter} [filter] - The filter to transform.
268
+ * @param {UpstashVectorFilter} [filter] - The filter to transform.
249
269
  * @returns {string | undefined} The transformed filter string, or undefined if no filter is provided.
250
270
  */
251
- transformFilter(filter?: VectorFilter): string | undefined;
271
+ transformFilter(filter?: UpstashVectorFilter): string | undefined;
252
272
  /**
253
273
  * Creates a new index. For Upstash, this is a no-op as indexes (known as namespaces in Upstash) are created on-the-fly.
254
274
  * @param {CreateIndexParams} _params - The parameters for creating the index (ignored).
@@ -260,7 +280,7 @@ declare class UpstashVector extends MastraVector {
260
280
  * @param {QueryVectorParams} params - The parameters for the query operation. indexName is the namespace in Upstash.
261
281
  * @returns {Promise<QueryResult[]>} A promise that resolves to the query results.
262
282
  */
263
- query({ indexName: namespace, queryVector, topK, filter, includeVector, }: QueryVectorParams): Promise<QueryResult[]>;
283
+ query({ indexName: namespace, queryVector, topK, filter, includeVector, }: UpstashQueryVectorParams): Promise<QueryResult[]>;
264
284
  /**
265
285
  * Lists all namespaces in the Upstash vector index, which correspond to indexes.
266
286
  * @returns {Promise<string[]>} A promise that resolves to a list of index names.
@@ -302,4 +322,6 @@ declare class UpstashVector extends MastraVector {
302
322
  export { UpstashVector }
303
323
  export { UpstashVector as UpstashVector_alias_1 }
304
324
 
325
+ export declare type UpstashVectorFilter = VectorFilter<keyof UpstashOperatorValueMap, UpstashOperatorValueMap>;
326
+
305
327
  export { }
package/dist/index.cjs CHANGED
@@ -20,7 +20,8 @@ var UpstashStore = class extends storage.MastraStorage {
20
20
  }
21
21
  get supports() {
22
22
  return {
23
- selectByIncludeResourceScope: true
23
+ selectByIncludeResourceScope: true,
24
+ resourceWorkingMemory: true
24
25
  };
25
26
  }
26
27
  transformEvalRecord(record) {
@@ -698,6 +699,23 @@ var UpstashStore = class extends storage.MastraStorage {
698
699
  const key = this.getMessageKey(message.threadId, message.id);
699
700
  const createdAtScore = new Date(message.createdAt).getTime();
700
701
  const score = message._index !== void 0 ? message._index : createdAtScore;
702
+ const existingKeyPattern = this.getMessageKey("*", message.id);
703
+ const keys = await this.scanKeys(existingKeyPattern);
704
+ if (keys.length > 0) {
705
+ const pipeline2 = this.redis.pipeline();
706
+ keys.forEach((key2) => pipeline2.get(key2));
707
+ const results = await pipeline2.exec();
708
+ const existingMessages = results.filter(
709
+ (msg) => msg !== null
710
+ );
711
+ for (const existingMessage of existingMessages) {
712
+ const existingMessageKey = this.getMessageKey(existingMessage.threadId, existingMessage.id);
713
+ if (existingMessage && existingMessage.threadId !== message.threadId) {
714
+ pipeline.del(existingMessageKey);
715
+ pipeline.zrem(this.getThreadMessagesKey(existingMessage.threadId), existingMessage.id);
716
+ }
717
+ }
718
+ }
701
719
  pipeline.set(key, message);
702
720
  pipeline.zadd(this.getThreadMessagesKey(message.threadId), {
703
721
  score,
@@ -1173,6 +1191,75 @@ var UpstashStore = class extends storage.MastraStorage {
1173
1191
  this.logger.error("updateMessages is not yet implemented in UpstashStore");
1174
1192
  throw new Error("Method not implemented");
1175
1193
  }
1194
+ async getResourceById({ resourceId }) {
1195
+ try {
1196
+ const key = `${storage.TABLE_RESOURCES}:${resourceId}`;
1197
+ const data = await this.redis.get(key);
1198
+ if (!data) {
1199
+ return null;
1200
+ }
1201
+ return {
1202
+ ...data,
1203
+ createdAt: new Date(data.createdAt),
1204
+ updatedAt: new Date(data.updatedAt),
1205
+ // Ensure workingMemory is always returned as a string, regardless of automatic parsing
1206
+ workingMemory: typeof data.workingMemory === "object" ? JSON.stringify(data.workingMemory) : data.workingMemory,
1207
+ metadata: typeof data.metadata === "string" ? JSON.parse(data.metadata) : data.metadata
1208
+ };
1209
+ } catch (error) {
1210
+ this.logger.error("Error getting resource by ID:", error);
1211
+ throw error;
1212
+ }
1213
+ }
1214
+ async saveResource({ resource }) {
1215
+ try {
1216
+ const key = `${storage.TABLE_RESOURCES}:${resource.id}`;
1217
+ const serializedResource = {
1218
+ ...resource,
1219
+ metadata: JSON.stringify(resource.metadata),
1220
+ createdAt: resource.createdAt.toISOString(),
1221
+ updatedAt: resource.updatedAt.toISOString()
1222
+ };
1223
+ await this.redis.set(key, serializedResource);
1224
+ return resource;
1225
+ } catch (error) {
1226
+ this.logger.error("Error saving resource:", error);
1227
+ throw error;
1228
+ }
1229
+ }
1230
+ async updateResource({
1231
+ resourceId,
1232
+ workingMemory,
1233
+ metadata
1234
+ }) {
1235
+ try {
1236
+ const existingResource = await this.getResourceById({ resourceId });
1237
+ if (!existingResource) {
1238
+ const newResource = {
1239
+ id: resourceId,
1240
+ workingMemory,
1241
+ metadata: metadata || {},
1242
+ createdAt: /* @__PURE__ */ new Date(),
1243
+ updatedAt: /* @__PURE__ */ new Date()
1244
+ };
1245
+ return this.saveResource({ resource: newResource });
1246
+ }
1247
+ const updatedResource = {
1248
+ ...existingResource,
1249
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1250
+ metadata: {
1251
+ ...existingResource.metadata,
1252
+ ...metadata
1253
+ },
1254
+ updatedAt: /* @__PURE__ */ new Date()
1255
+ };
1256
+ await this.saveResource({ resource: updatedResource });
1257
+ return updatedResource;
1258
+ } catch (error) {
1259
+ this.logger.error("Error updating resource:", error);
1260
+ throw error;
1261
+ }
1262
+ }
1176
1263
  };
1177
1264
  var UpstashFilterTranslator = class extends filter.BaseFilterTranslator {
1178
1265
  getSupportedOperators() {
@@ -1413,7 +1500,7 @@ var UpstashVector = class extends vector.MastraVector {
1413
1500
  }
1414
1501
  /**
1415
1502
  * Transforms a Mastra vector filter into an Upstash-compatible filter string.
1416
- * @param {VectorFilter} [filter] - The filter to transform.
1503
+ * @param {UpstashVectorFilter} [filter] - The filter to transform.
1417
1504
  * @returns {string | undefined} The transformed filter string, or undefined if no filter is provided.
1418
1505
  */
1419
1506
  transformFilter(filter) {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MessageList } from '@mastra/core/agent';
2
2
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
3
- import { MastraStorage, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_EVALS, TABLE_TRACES, TABLE_THREADS } from '@mastra/core/storage';
3
+ import { MastraStorage, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_EVALS, TABLE_TRACES, TABLE_THREADS, TABLE_RESOURCES } from '@mastra/core/storage';
4
4
  import { Redis } from '@upstash/redis';
5
5
  import { MastraVector } from '@mastra/core/vector';
6
6
  import { Index } from '@upstash/vector';
@@ -18,7 +18,8 @@ var UpstashStore = class extends MastraStorage {
18
18
  }
19
19
  get supports() {
20
20
  return {
21
- selectByIncludeResourceScope: true
21
+ selectByIncludeResourceScope: true,
22
+ resourceWorkingMemory: true
22
23
  };
23
24
  }
24
25
  transformEvalRecord(record) {
@@ -696,6 +697,23 @@ var UpstashStore = class extends MastraStorage {
696
697
  const key = this.getMessageKey(message.threadId, message.id);
697
698
  const createdAtScore = new Date(message.createdAt).getTime();
698
699
  const score = message._index !== void 0 ? message._index : createdAtScore;
700
+ const existingKeyPattern = this.getMessageKey("*", message.id);
701
+ const keys = await this.scanKeys(existingKeyPattern);
702
+ if (keys.length > 0) {
703
+ const pipeline2 = this.redis.pipeline();
704
+ keys.forEach((key2) => pipeline2.get(key2));
705
+ const results = await pipeline2.exec();
706
+ const existingMessages = results.filter(
707
+ (msg) => msg !== null
708
+ );
709
+ for (const existingMessage of existingMessages) {
710
+ const existingMessageKey = this.getMessageKey(existingMessage.threadId, existingMessage.id);
711
+ if (existingMessage && existingMessage.threadId !== message.threadId) {
712
+ pipeline.del(existingMessageKey);
713
+ pipeline.zrem(this.getThreadMessagesKey(existingMessage.threadId), existingMessage.id);
714
+ }
715
+ }
716
+ }
699
717
  pipeline.set(key, message);
700
718
  pipeline.zadd(this.getThreadMessagesKey(message.threadId), {
701
719
  score,
@@ -1171,6 +1189,75 @@ var UpstashStore = class extends MastraStorage {
1171
1189
  this.logger.error("updateMessages is not yet implemented in UpstashStore");
1172
1190
  throw new Error("Method not implemented");
1173
1191
  }
1192
+ async getResourceById({ resourceId }) {
1193
+ try {
1194
+ const key = `${TABLE_RESOURCES}:${resourceId}`;
1195
+ const data = await this.redis.get(key);
1196
+ if (!data) {
1197
+ return null;
1198
+ }
1199
+ return {
1200
+ ...data,
1201
+ createdAt: new Date(data.createdAt),
1202
+ updatedAt: new Date(data.updatedAt),
1203
+ // Ensure workingMemory is always returned as a string, regardless of automatic parsing
1204
+ workingMemory: typeof data.workingMemory === "object" ? JSON.stringify(data.workingMemory) : data.workingMemory,
1205
+ metadata: typeof data.metadata === "string" ? JSON.parse(data.metadata) : data.metadata
1206
+ };
1207
+ } catch (error) {
1208
+ this.logger.error("Error getting resource by ID:", error);
1209
+ throw error;
1210
+ }
1211
+ }
1212
+ async saveResource({ resource }) {
1213
+ try {
1214
+ const key = `${TABLE_RESOURCES}:${resource.id}`;
1215
+ const serializedResource = {
1216
+ ...resource,
1217
+ metadata: JSON.stringify(resource.metadata),
1218
+ createdAt: resource.createdAt.toISOString(),
1219
+ updatedAt: resource.updatedAt.toISOString()
1220
+ };
1221
+ await this.redis.set(key, serializedResource);
1222
+ return resource;
1223
+ } catch (error) {
1224
+ this.logger.error("Error saving resource:", error);
1225
+ throw error;
1226
+ }
1227
+ }
1228
+ async updateResource({
1229
+ resourceId,
1230
+ workingMemory,
1231
+ metadata
1232
+ }) {
1233
+ try {
1234
+ const existingResource = await this.getResourceById({ resourceId });
1235
+ if (!existingResource) {
1236
+ const newResource = {
1237
+ id: resourceId,
1238
+ workingMemory,
1239
+ metadata: metadata || {},
1240
+ createdAt: /* @__PURE__ */ new Date(),
1241
+ updatedAt: /* @__PURE__ */ new Date()
1242
+ };
1243
+ return this.saveResource({ resource: newResource });
1244
+ }
1245
+ const updatedResource = {
1246
+ ...existingResource,
1247
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1248
+ metadata: {
1249
+ ...existingResource.metadata,
1250
+ ...metadata
1251
+ },
1252
+ updatedAt: /* @__PURE__ */ new Date()
1253
+ };
1254
+ await this.saveResource({ resource: updatedResource });
1255
+ return updatedResource;
1256
+ } catch (error) {
1257
+ this.logger.error("Error updating resource:", error);
1258
+ throw error;
1259
+ }
1260
+ }
1174
1261
  };
1175
1262
  var UpstashFilterTranslator = class extends BaseFilterTranslator {
1176
1263
  getSupportedOperators() {
@@ -1411,7 +1498,7 @@ var UpstashVector = class extends MastraVector {
1411
1498
  }
1412
1499
  /**
1413
1500
  * Transforms a Mastra vector filter into an Upstash-compatible filter string.
1414
- * @param {VectorFilter} [filter] - The filter to transform.
1501
+ * @param {UpstashVectorFilter} [filter] - The filter to transform.
1415
1502
  * @returns {string | undefined} The transformed filter string, or undefined if no filter is provided.
1416
1503
  */
1417
1504
  transformFilter(filter) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/upstash",
3
- "version": "0.11.1-alpha.1",
3
+ "version": "0.12.0-alpha.3",
4
4
  "description": "Upstash provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,16 +27,16 @@
27
27
  "@microsoft/api-extractor": "^7.52.8",
28
28
  "@types/node": "^20.19.0",
29
29
  "dotenv": "^16.5.0",
30
- "eslint": "^9.28.0",
30
+ "eslint": "^9.29.0",
31
31
  "tsup": "^8.5.0",
32
32
  "typescript": "^5.8.3",
33
33
  "vitest": "^3.2.3",
34
- "@internal/lint": "0.0.13",
35
34
  "@internal/storage-test-utils": "0.0.9",
36
- "@mastra/core": "0.10.7-alpha.1"
35
+ "@internal/lint": "0.0.13",
36
+ "@mastra/core": "0.10.7-alpha.3"
37
37
  },
38
38
  "peerDependencies": {
39
- "@mastra/core": ">=0.10.4-0 <0.11.0"
39
+ "@mastra/core": ">=0.10.7-0 <0.11.0-0"
40
40
  },
41
41
  "scripts": {
42
42
  "pretest": "docker compose up -d",
@@ -7,6 +7,7 @@ import {
7
7
  MastraStorage,
8
8
  TABLE_MESSAGES,
9
9
  TABLE_THREADS,
10
+ TABLE_RESOURCES,
10
11
  TABLE_WORKFLOW_SNAPSHOT,
11
12
  TABLE_EVALS,
12
13
  TABLE_TRACES,
@@ -15,6 +16,7 @@ import type {
15
16
  TABLE_NAMES,
16
17
  StorageColumn,
17
18
  StorageGetMessagesArg,
19
+ StorageResourceType,
18
20
  EvalRow,
19
21
  WorkflowRuns,
20
22
  WorkflowRun,
@@ -43,9 +45,11 @@ export class UpstashStore extends MastraStorage {
43
45
 
44
46
  public get supports(): {
45
47
  selectByIncludeResourceScope: boolean;
48
+ resourceWorkingMemory: boolean;
46
49
  } {
47
50
  return {
48
51
  selectByIncludeResourceScope: true,
52
+ resourceWorkingMemory: true,
49
53
  };
50
54
  }
51
55
 
@@ -853,6 +857,27 @@ export class UpstashStore extends MastraStorage {
853
857
  const createdAtScore = new Date(message.createdAt).getTime();
854
858
  const score = message._index !== undefined ? message._index : createdAtScore;
855
859
 
860
+ // Check if this message id exists in another thread
861
+ const existingKeyPattern = this.getMessageKey('*', message.id);
862
+ const keys = await this.scanKeys(existingKeyPattern);
863
+
864
+ if (keys.length > 0) {
865
+ const pipeline2 = this.redis.pipeline();
866
+ keys.forEach(key => pipeline2.get(key));
867
+ const results = await pipeline2.exec();
868
+ const existingMessages = results.filter(
869
+ (msg): msg is MastraMessageV2 | MastraMessageV1 => msg !== null,
870
+ ) as (MastraMessageV2 | MastraMessageV1)[];
871
+ for (const existingMessage of existingMessages) {
872
+ const existingMessageKey = this.getMessageKey(existingMessage.threadId!, existingMessage.id);
873
+ if (existingMessage && existingMessage.threadId !== message.threadId) {
874
+ pipeline.del(existingMessageKey);
875
+ // Remove from old thread's sorted set
876
+ pipeline.zrem(this.getThreadMessagesKey(existingMessage.threadId!), existingMessage.id);
877
+ }
878
+ }
879
+ }
880
+
856
881
  // Store the message data
857
882
  pipeline.set(key, message);
858
883
 
@@ -1508,4 +1533,88 @@ export class UpstashStore extends MastraStorage {
1508
1533
  this.logger.error('updateMessages is not yet implemented in UpstashStore');
1509
1534
  throw new Error('Method not implemented');
1510
1535
  }
1536
+
1537
+ async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
1538
+ try {
1539
+ const key = `${TABLE_RESOURCES}:${resourceId}`;
1540
+ const data = await this.redis.get<StorageResourceType>(key);
1541
+
1542
+ if (!data) {
1543
+ return null;
1544
+ }
1545
+
1546
+ return {
1547
+ ...data,
1548
+ createdAt: new Date(data.createdAt),
1549
+ updatedAt: new Date(data.updatedAt),
1550
+ // Ensure workingMemory is always returned as a string, regardless of automatic parsing
1551
+ workingMemory: typeof data.workingMemory === 'object' ? JSON.stringify(data.workingMemory) : data.workingMemory,
1552
+ metadata: typeof data.metadata === 'string' ? JSON.parse(data.metadata) : data.metadata,
1553
+ };
1554
+ } catch (error) {
1555
+ this.logger.error('Error getting resource by ID:', error);
1556
+ throw error;
1557
+ }
1558
+ }
1559
+
1560
+ async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
1561
+ try {
1562
+ const key = `${TABLE_RESOURCES}:${resource.id}`;
1563
+ const serializedResource = {
1564
+ ...resource,
1565
+ metadata: JSON.stringify(resource.metadata),
1566
+ createdAt: resource.createdAt.toISOString(),
1567
+ updatedAt: resource.updatedAt.toISOString(),
1568
+ };
1569
+
1570
+ await this.redis.set(key, serializedResource);
1571
+
1572
+ return resource;
1573
+ } catch (error) {
1574
+ this.logger.error('Error saving resource:', error);
1575
+ throw error;
1576
+ }
1577
+ }
1578
+
1579
+ async updateResource({
1580
+ resourceId,
1581
+ workingMemory,
1582
+ metadata,
1583
+ }: {
1584
+ resourceId: string;
1585
+ workingMemory?: string;
1586
+ metadata?: Record<string, unknown>;
1587
+ }): Promise<StorageResourceType> {
1588
+ try {
1589
+ const existingResource = await this.getResourceById({ resourceId });
1590
+
1591
+ if (!existingResource) {
1592
+ // Create new resource if it doesn't exist
1593
+ const newResource: StorageResourceType = {
1594
+ id: resourceId,
1595
+ workingMemory,
1596
+ metadata: metadata || {},
1597
+ createdAt: new Date(),
1598
+ updatedAt: new Date(),
1599
+ };
1600
+ return this.saveResource({ resource: newResource });
1601
+ }
1602
+
1603
+ const updatedResource = {
1604
+ ...existingResource,
1605
+ workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
1606
+ metadata: {
1607
+ ...existingResource.metadata,
1608
+ ...metadata,
1609
+ },
1610
+ updatedAt: new Date(),
1611
+ };
1612
+
1613
+ await this.saveResource({ resource: updatedResource });
1614
+ return updatedResource;
1615
+ } catch (error) {
1616
+ this.logger.error('Error updating resource:', error);
1617
+ throw error;
1618
+ }
1619
+ }
1511
1620
  }
@@ -498,6 +498,81 @@ describe('UpstashStore', () => {
498
498
  expect(retrievedMessages[0].content).toEqual(messages[0].content);
499
499
  });
500
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
+ });
501
576
  describe('getMessagesPaginated', () => {
502
577
  it('should return paginated messages with total count', async () => {
503
578
  const thread = createSampleThread();
@@ -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
  });
@@ -11,12 +11,14 @@ import type {
11
11
  UpdateVectorParams,
12
12
  UpsertVectorParams,
13
13
  } from '@mastra/core/vector';
14
- import type { VectorFilter } from '@mastra/core/vector/filter';
15
14
  import { Index } from '@upstash/vector';
16
15
 
17
16
  import { UpstashFilterTranslator } from './filter';
17
+ import type { UpstashVectorFilter } from './filter';
18
18
 
19
- export class UpstashVector extends MastraVector {
19
+ type UpstashQueryVectorParams = QueryVectorParams<UpstashVectorFilter>;
20
+
21
+ export class UpstashVector extends MastraVector<UpstashVectorFilter> {
20
22
  private client: Index;
21
23
 
22
24
  /**
@@ -67,10 +69,10 @@ export class UpstashVector extends MastraVector {
67
69
 
68
70
  /**
69
71
  * Transforms a Mastra vector filter into an Upstash-compatible filter string.
70
- * @param {VectorFilter} [filter] - The filter to transform.
72
+ * @param {UpstashVectorFilter} [filter] - The filter to transform.
71
73
  * @returns {string | undefined} The transformed filter string, or undefined if no filter is provided.
72
74
  */
73
- transformFilter(filter?: VectorFilter) {
75
+ transformFilter(filter?: UpstashVectorFilter) {
74
76
  const translator = new UpstashFilterTranslator();
75
77
  return translator.translate(filter);
76
78
  }
@@ -95,7 +97,7 @@ export class UpstashVector extends MastraVector {
95
97
  topK = 10,
96
98
  filter,
97
99
  includeVector = false,
98
- }: QueryVectorParams): Promise<QueryResult[]> {
100
+ }: UpstashQueryVectorParams): Promise<QueryResult[]> {
99
101
  try {
100
102
  const ns = this.client.namespace(namespace);
101
103