@mastra/upstash 0.14.5 → 0.14.6-alpha.0

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,260 +0,0 @@
1
- import { BaseFilterTranslator } from '@mastra/core/vector/filter';
2
- import type { OperatorSupport, VectorFilter, OperatorValueMap } from '@mastra/core/vector/filter';
3
-
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> {
11
- protected override getSupportedOperators(): OperatorSupport {
12
- return {
13
- ...BaseFilterTranslator.DEFAULT_OPERATORS,
14
- array: ['$in', '$nin', '$all'],
15
- regex: ['$regex'],
16
- custom: ['$contains'],
17
- };
18
- }
19
-
20
- translate(filter?: UpstashVectorFilter): string | undefined {
21
- if (this.isEmpty(filter)) return undefined;
22
- this.validateFilter(filter);
23
- return this.translateNode(filter);
24
- }
25
-
26
- private translateNode(node: UpstashVectorFilter, path: string = ''): string {
27
- if (this.isRegex(node)) {
28
- throw new Error('Direct regex pattern format is not supported in Upstash');
29
- }
30
- if (node === null || node === undefined) {
31
- throw new Error('Filtering for null/undefined values is not supported by Upstash Vector');
32
- }
33
-
34
- // Handle primitives (direct equality)
35
- if (this.isPrimitive(node)) {
36
- if (node === null || node === undefined) {
37
- throw new Error('Filtering for null/undefined values is not supported by Upstash Vector');
38
- }
39
- return this.formatComparison(path, '=', node);
40
- }
41
-
42
- // Handle arrays (IN operator)
43
- if (Array.isArray(node)) {
44
- if (node.length === 0) {
45
- return '(HAS FIELD empty AND HAS NOT FIELD empty)';
46
- }
47
- return `${path} IN (${this.formatArray(node)})`;
48
- }
49
-
50
- const entries = Object.entries(node as Record<string, any>);
51
- const conditions: string[] = [];
52
-
53
- for (const [key, value] of entries) {
54
- const newPath = path ? `${path}.${key}` : key;
55
-
56
- if (this.isOperator(key)) {
57
- conditions.push(this.translateOperator(key, value, path));
58
- } else if (typeof value === 'object' && value !== null) {
59
- conditions.push(this.translateNode(value, newPath));
60
- } else if (value === null || value === undefined) {
61
- throw new Error('Filtering for null/undefined values is not supported by Upstash Vector');
62
- } else {
63
- conditions.push(this.formatComparison(newPath, '=', value));
64
- }
65
- }
66
-
67
- return conditions.length > 1 ? `(${conditions.join(' AND ')})` : (conditions[0] ?? '');
68
- }
69
-
70
- private readonly COMPARISON_OPS = {
71
- $eq: '=',
72
- $ne: '!=',
73
- $gt: '>',
74
- $gte: '>=',
75
- $lt: '<',
76
- $lte: '<=',
77
- } as const;
78
-
79
- private translateOperator(operator: string, value: any, path: string): string {
80
- // Handle comparison operators
81
- if (this.isBasicOperator(operator) || this.isNumericOperator(operator)) {
82
- return this.formatComparison(path, this.COMPARISON_OPS[operator], value);
83
- }
84
-
85
- // Handle special operators
86
- switch (operator) {
87
- case '$in':
88
- if (!Array.isArray(value) || value.length === 0) {
89
- return '(HAS FIELD empty AND HAS NOT FIELD empty)'; // Always false
90
- }
91
- return `${path} IN (${this.formatArray(value)})`;
92
- case '$nin':
93
- return `${path} NOT IN (${this.formatArray(value)})`;
94
- case '$contains':
95
- return `${path} CONTAINS ${this.formatValue(value)}`;
96
- case '$regex':
97
- return `${path} GLOB ${this.formatValue(value)}`;
98
- case '$exists':
99
- return value ? `HAS FIELD ${path}` : `HAS NOT FIELD ${path}`;
100
-
101
- case '$and':
102
- if (!Array.isArray(value) || value.length === 0) {
103
- return '(HAS FIELD empty OR HAS NOT FIELD empty)';
104
- }
105
- return this.joinConditions(value, 'AND');
106
-
107
- case '$or':
108
- if (!Array.isArray(value) || value.length === 0) {
109
- return '(HAS FIELD empty AND HAS NOT FIELD empty)';
110
- }
111
- return this.joinConditions(value, 'OR');
112
-
113
- case '$not':
114
- return this.formatNot(path, value);
115
-
116
- case '$nor':
117
- return this.formatNot('', { $or: value });
118
- case '$all':
119
- return this.translateOperator(
120
- '$and',
121
- value.map((item: unknown) => ({ [path]: { $contains: item } })),
122
- '',
123
- );
124
-
125
- default:
126
- throw new Error(`Unsupported operator: ${operator}`);
127
- }
128
- }
129
-
130
- private readonly NEGATED_OPERATORS: Record<string, string> = {
131
- $eq: '$ne',
132
- $ne: '$eq',
133
- $gt: '$lte',
134
- $gte: '$lt',
135
- $lt: '$gte',
136
- $lte: '$gt',
137
- $in: '$nin',
138
- $nin: '$in',
139
- $exists: '$exists', // Special case - we'll flip the value
140
- };
141
-
142
- private formatNot(path: string, value: any): string {
143
- if (typeof value !== 'object') {
144
- return `${path} != ${this.formatValue(value)}`;
145
- }
146
-
147
- if (!Object.keys(value).some(k => k.startsWith('$'))) {
148
- const [fieldName, fieldValue] = Object.entries(value)[0] ?? [];
149
-
150
- // If it's a nested condition with an operator
151
- if (typeof fieldValue === 'object' && fieldValue !== null && Object.keys(fieldValue)[0]?.startsWith('$')) {
152
- const [op, val] = Object.entries(fieldValue)[0] ?? [];
153
- const negatedOp = this.NEGATED_OPERATORS[op as string];
154
- if (!negatedOp) throw new Error(`Unsupported operator in NOT: ${op}`);
155
-
156
- // Special case for $exists - negate the value instead of the operator
157
- if (op === '$exists') {
158
- return this.translateOperator(op, !val, fieldName ?? '');
159
- }
160
-
161
- return this.translateOperator(negatedOp, val, fieldName ?? '');
162
- }
163
-
164
- // Otherwise handle as simple field value
165
- return `${fieldName} != ${this.formatValue(fieldValue)}`;
166
- }
167
-
168
- // Handle top-level operators
169
- const [op, val] = Object.entries(value)[0] ?? [];
170
-
171
- // Handle comparison operators
172
- if (op === '$lt') return `${path} >= ${this.formatValue(val)}`;
173
- if (op === '$lte') return `${path} > ${this.formatValue(val)}`;
174
- if (op === '$gt') return `${path} <= ${this.formatValue(val)}`;
175
- if (op === '$gte') return `${path} < ${this.formatValue(val)}`;
176
- if (op === '$ne') return `${path} = ${this.formatValue(val)}`;
177
- if (op === '$eq') return `${path} != ${this.formatValue(val)}`;
178
-
179
- // Special cases
180
- if (op === '$contains') return `${path} NOT CONTAINS ${this.formatValue(val)}`;
181
- if (op === '$regex') return `${path} NOT GLOB ${this.formatValue(val)}`;
182
- if (op === '$in') return `${path} NOT IN (${this.formatArray(val as any[])})`;
183
- if (op === '$exists') return val ? `HAS NOT FIELD ${path}` : `HAS FIELD ${path}`;
184
-
185
- // Transform NOT(AND) into OR(NOT) and NOT(OR) into AND(NOT)
186
- if (op === '$and' || op === '$or') {
187
- const newOp = op === '$and' ? '$or' : '$and';
188
- const conditions = (val as any[]).map((condition: any) => {
189
- const [fieldName, fieldValue] = Object.entries(condition)[0] ?? [];
190
- return { [fieldName as string]: { $not: fieldValue } };
191
- });
192
- return this.translateOperator(newOp, conditions, '');
193
- }
194
-
195
- // NOT(NOR) is equivalent to OR
196
- if (op === '$nor') {
197
- return this.translateOperator('$or', val, '');
198
- }
199
-
200
- return `${path} != ${this.formatValue(val)}`;
201
- }
202
-
203
- private formatValue(value: any): string {
204
- if (value === null || value === undefined) {
205
- throw new Error('Filtering for null/undefined values is not supported by Upstash Vector');
206
- }
207
-
208
- if (typeof value === 'string') {
209
- // Check for quotes in the string content
210
- const hasSingleQuote = /'/g.test(value);
211
- const hasDoubleQuote = /"/g.test(value);
212
-
213
- // If string has both types of quotes, escape single quotes and use single quotes
214
- // If string has single quotes, use double quotes
215
- // Otherwise, use single quotes (default)
216
- if (hasSingleQuote && hasDoubleQuote) {
217
- return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
218
- }
219
- if (hasSingleQuote) {
220
- return `"${value}"`;
221
- }
222
- return `'${value}'`;
223
- }
224
-
225
- if (typeof value === 'number') {
226
- // Handle scientific notation by converting to decimal
227
- if (Math.abs(value) < 1e-6 || Math.abs(value) > 1e6) {
228
- return value.toFixed(20).replace(/\.?0+$/, '');
229
- }
230
- // Regular numbers (including zero and negative)
231
- return value.toString();
232
- }
233
-
234
- return String(value);
235
- }
236
-
237
- private formatArray(values: any[]): string {
238
- return values
239
- .map(value => {
240
- if (value === null || value === undefined) {
241
- throw new Error('Filtering for null/undefined values is not supported by Upstash Vector');
242
- }
243
- return this.formatValue(value);
244
- })
245
- .join(', ');
246
- }
247
-
248
- private formatComparison(path: string, op: string, value: any): string {
249
- return `${path} ${op} ${this.formatValue(value)}`;
250
- }
251
-
252
- private joinConditions(conditions: any[], operator: string): string {
253
- const translated = Array.isArray(conditions)
254
- ? conditions.map(c => this.translateNode(c))
255
- : [this.translateNode(conditions)];
256
-
257
- // Don't wrap in parentheses if there's only one condition
258
- return `(${translated.join(` ${operator} `)})`;
259
- }
260
- }