@mastra/upstash 0.3.1-alpha.3 → 0.3.1-alpha.5
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 +9 -9
- package/CHANGELOG.md +17 -0
- package/dist/_tsup-dts-rollup.d.cts +19 -11
- package/dist/_tsup-dts-rollup.d.ts +19 -11
- package/dist/index.cjs +154 -35
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +154 -36
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/storage/index.ts +117 -63
- package/src/storage/upstash.test.ts +168 -46
- package/src/vector/prompt.ts +77 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/upstash@0.3.1-alpha.
|
|
2
|
+
> @mastra/upstash@0.3.1-alpha.5 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
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
7
|
[34mCLI[39m tsup v8.4.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 9182ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
|
-
Analysis will use the bundled TypeScript version 5.8.
|
|
12
|
+
Analysis will use the bundled TypeScript version 5.8.3
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/upstash/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
|
-
Analysis will use the bundled TypeScript version 5.8.
|
|
14
|
+
Analysis will use the bundled TypeScript version 5.8.3
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/upstash/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 11741ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
20
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m28.65 KB[39m
|
|
21
|
+
[32mCJS[39m ⚡️ Build success in 1190ms
|
|
22
|
+
[32mESM[39m [1mdist/index.js [22m[32m28.51 KB[39m
|
|
23
|
+
[32mESM[39m ⚡️ Build success in 1193ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @mastra/upstash
|
|
2
2
|
|
|
3
|
+
## 0.3.1-alpha.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5f826d9: Moved vector store specific prompts from @mastra/rag to be exported from the store that the prompt belongs to, ie @mastra/pg
|
|
8
|
+
- Updated dependencies [3e7b69d]
|
|
9
|
+
- @mastra/core@0.9.1-alpha.5
|
|
10
|
+
|
|
11
|
+
## 0.3.1-alpha.4
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 479f490: [MASTRA-3131] Add getWorkflowRunByID and add resourceId as filter for getWorkflowRuns
|
|
16
|
+
- Updated dependencies [e4943b8]
|
|
17
|
+
- Updated dependencies [479f490]
|
|
18
|
+
- @mastra/core@0.9.1-alpha.4
|
|
19
|
+
|
|
3
20
|
## 0.3.1-alpha.3
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
|
@@ -14,8 +14,18 @@ import type { StorageThreadType } from '@mastra/core/memory';
|
|
|
14
14
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
15
15
|
import type { UpsertVectorParams } from '@mastra/core/vector';
|
|
16
16
|
import type { VectorFilter } from '@mastra/core/vector/filter';
|
|
17
|
+
import type { WorkflowRun } from '@mastra/core/storage';
|
|
18
|
+
import type { WorkflowRuns } from '@mastra/core/storage';
|
|
17
19
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Vector store specific prompt that details supported operators and examples.
|
|
23
|
+
* This prompt helps users construct valid filters for Upstash Vector.
|
|
24
|
+
*/
|
|
25
|
+
declare const UPSTASH_PROMPT = "When querying Upstash Vector, you can ONLY use the operators listed below. Any other operators will be rejected.\nImportant: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.\nIf a user tries to give an explicit operator that is not supported, reject the filter entirely and let them know that the operator is not supported.\n\nBasic Comparison Operators:\n- $eq: Exact match (default when using field: value)\n Example: { \"category\": \"electronics\" }\n- $ne: Not equal\n Example: { \"category\": { \"$ne\": \"electronics\" } }\n- $gt: Greater than\n Example: { \"price\": { \"$gt\": 100 } }\n- $gte: Greater than or equal\n Example: { \"price\": { \"$gte\": 100 } }\n- $lt: Less than\n Example: { \"price\": { \"$lt\": 100 } }\n- $lte: Less than or equal\n Example: { \"price\": { \"$lte\": 100 } }\n\nArray Operators:\n- $in: Match any value in array\n Example: { \"category\": { \"$in\": [\"electronics\", \"books\"] } }\n- $nin: Does not match any value in array\n Example: { \"category\": { \"$nin\": [\"electronics\", \"books\"] } }\n\nLogical Operators:\n- $and: Logical AND (can be implicit or explicit)\n Implicit Example: { \"price\": { \"$gt\": 100 }, \"category\": \"electronics\" }\n Explicit Example: { \"$and\": [{ \"price\": { \"$gt\": 100 } }, { \"category\": \"electronics\" }] }\n- $or: Logical OR\n Example: { \"$or\": [{ \"price\": { \"$lt\": 50 } }, { \"category\": \"books\" }] }\n\nElement Operators:\n- $exists: Check if field exists\n Example: { \"rating\": { \"$exists\": true } }\n\nRestrictions:\n- Regex patterns are not supported\n- Only $and and $or logical operators are supported at the top level\n- Empty arrays in $in/$nin will return no results\n- Nested fields are supported using dot notation\n- Multiple conditions on the same field are supported with both implicit and explicit $and\n- At least one key-value pair is required in filter object\n- Empty objects and undefined values are treated as no filter\n- Invalid types in comparison operators will throw errors\n- All non-logical operators must be used within a field condition\n Valid: { \"field\": { \"$gt\": 100 } }\n Valid: { \"$and\": [...] }\n Invalid: { \"$gt\": 100 }\n- Logical operators must contain field conditions, not direct operators\n Valid: { \"$and\": [{ \"field\": { \"$gt\": 100 } }] }\n Invalid: { \"$and\": [{ \"$gt\": 100 }] }\n- Logical operators ($and, $or):\n - Can only be used at top level or nested within other logical operators\n - Can not be used on a field level, or be nested inside a field\n - Can not be used inside an operator\n - Valid: { \"$and\": [{ \"field\": { \"$gt\": 100 } }] }\n - Valid: { \"$or\": [{ \"$and\": [{ \"field\": { \"$gt\": 100 } }] }] }\n - Invalid: { \"field\": { \"$and\": [{ \"$gt\": 100 }] } }\n - Invalid: { \"field\": { \"$or\": [{ \"$gt\": 100 }] } }\n - Invalid: { \"field\": { \"$gt\": { \"$and\": [{...}] } } }\n\nExample Complex Query:\n{\n \"$and\": [\n { \"category\": { \"$in\": [\"electronics\", \"computers\"] } },\n { \"price\": { \"$gte\": 100, \"$lte\": 1000 } },\n { \"rating\": { \"$exists\": true, \"$gt\": 4 } },\n { \"$or\": [\n { \"stock\": { \"$gt\": 0 } },\n { \"preorder\": true }\n ]}\n ]\n}";
|
|
26
|
+
export { UPSTASH_PROMPT }
|
|
27
|
+
export { UPSTASH_PROMPT as UPSTASH_PROMPT_alias_1 }
|
|
28
|
+
|
|
19
29
|
declare interface UpstashConfig {
|
|
20
30
|
url: string;
|
|
21
31
|
token: string;
|
|
@@ -107,23 +117,21 @@ declare class UpstashStore extends MastraStorage {
|
|
|
107
117
|
workflowName: string;
|
|
108
118
|
runId: string;
|
|
109
119
|
}): Promise<WorkflowRunState | null>;
|
|
110
|
-
|
|
120
|
+
private parseWorkflowRun;
|
|
121
|
+
getWorkflowRuns({ namespace, workflowName, fromDate, toDate, limit, offset, resourceId, }?: {
|
|
111
122
|
namespace: string;
|
|
112
123
|
workflowName?: string;
|
|
113
124
|
fromDate?: Date;
|
|
114
125
|
toDate?: Date;
|
|
115
126
|
limit?: number;
|
|
116
127
|
offset?: number;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}>;
|
|
125
|
-
total: number;
|
|
126
|
-
}>;
|
|
128
|
+
resourceId?: string;
|
|
129
|
+
}): Promise<WorkflowRuns>;
|
|
130
|
+
getWorkflowRunById({ namespace, runId, workflowName, }: {
|
|
131
|
+
namespace: string;
|
|
132
|
+
runId: string;
|
|
133
|
+
workflowName?: string;
|
|
134
|
+
}): Promise<WorkflowRun | null>;
|
|
127
135
|
close(): Promise<void>;
|
|
128
136
|
}
|
|
129
137
|
export { UpstashStore }
|
|
@@ -14,8 +14,18 @@ import type { StorageThreadType } from '@mastra/core/memory';
|
|
|
14
14
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
15
15
|
import type { UpsertVectorParams } from '@mastra/core/vector';
|
|
16
16
|
import type { VectorFilter } from '@mastra/core/vector/filter';
|
|
17
|
+
import type { WorkflowRun } from '@mastra/core/storage';
|
|
18
|
+
import type { WorkflowRuns } from '@mastra/core/storage';
|
|
17
19
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Vector store specific prompt that details supported operators and examples.
|
|
23
|
+
* This prompt helps users construct valid filters for Upstash Vector.
|
|
24
|
+
*/
|
|
25
|
+
declare const UPSTASH_PROMPT = "When querying Upstash Vector, you can ONLY use the operators listed below. Any other operators will be rejected.\nImportant: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.\nIf a user tries to give an explicit operator that is not supported, reject the filter entirely and let them know that the operator is not supported.\n\nBasic Comparison Operators:\n- $eq: Exact match (default when using field: value)\n Example: { \"category\": \"electronics\" }\n- $ne: Not equal\n Example: { \"category\": { \"$ne\": \"electronics\" } }\n- $gt: Greater than\n Example: { \"price\": { \"$gt\": 100 } }\n- $gte: Greater than or equal\n Example: { \"price\": { \"$gte\": 100 } }\n- $lt: Less than\n Example: { \"price\": { \"$lt\": 100 } }\n- $lte: Less than or equal\n Example: { \"price\": { \"$lte\": 100 } }\n\nArray Operators:\n- $in: Match any value in array\n Example: { \"category\": { \"$in\": [\"electronics\", \"books\"] } }\n- $nin: Does not match any value in array\n Example: { \"category\": { \"$nin\": [\"electronics\", \"books\"] } }\n\nLogical Operators:\n- $and: Logical AND (can be implicit or explicit)\n Implicit Example: { \"price\": { \"$gt\": 100 }, \"category\": \"electronics\" }\n Explicit Example: { \"$and\": [{ \"price\": { \"$gt\": 100 } }, { \"category\": \"electronics\" }] }\n- $or: Logical OR\n Example: { \"$or\": [{ \"price\": { \"$lt\": 50 } }, { \"category\": \"books\" }] }\n\nElement Operators:\n- $exists: Check if field exists\n Example: { \"rating\": { \"$exists\": true } }\n\nRestrictions:\n- Regex patterns are not supported\n- Only $and and $or logical operators are supported at the top level\n- Empty arrays in $in/$nin will return no results\n- Nested fields are supported using dot notation\n- Multiple conditions on the same field are supported with both implicit and explicit $and\n- At least one key-value pair is required in filter object\n- Empty objects and undefined values are treated as no filter\n- Invalid types in comparison operators will throw errors\n- All non-logical operators must be used within a field condition\n Valid: { \"field\": { \"$gt\": 100 } }\n Valid: { \"$and\": [...] }\n Invalid: { \"$gt\": 100 }\n- Logical operators must contain field conditions, not direct operators\n Valid: { \"$and\": [{ \"field\": { \"$gt\": 100 } }] }\n Invalid: { \"$and\": [{ \"$gt\": 100 }] }\n- Logical operators ($and, $or):\n - Can only be used at top level or nested within other logical operators\n - Can not be used on a field level, or be nested inside a field\n - Can not be used inside an operator\n - Valid: { \"$and\": [{ \"field\": { \"$gt\": 100 } }] }\n - Valid: { \"$or\": [{ \"$and\": [{ \"field\": { \"$gt\": 100 } }] }] }\n - Invalid: { \"field\": { \"$and\": [{ \"$gt\": 100 }] } }\n - Invalid: { \"field\": { \"$or\": [{ \"$gt\": 100 }] } }\n - Invalid: { \"field\": { \"$gt\": { \"$and\": [{...}] } } }\n\nExample Complex Query:\n{\n \"$and\": [\n { \"category\": { \"$in\": [\"electronics\", \"computers\"] } },\n { \"price\": { \"$gte\": 100, \"$lte\": 1000 } },\n { \"rating\": { \"$exists\": true, \"$gt\": 4 } },\n { \"$or\": [\n { \"stock\": { \"$gt\": 0 } },\n { \"preorder\": true }\n ]}\n ]\n}";
|
|
26
|
+
export { UPSTASH_PROMPT }
|
|
27
|
+
export { UPSTASH_PROMPT as UPSTASH_PROMPT_alias_1 }
|
|
28
|
+
|
|
19
29
|
declare interface UpstashConfig {
|
|
20
30
|
url: string;
|
|
21
31
|
token: string;
|
|
@@ -107,23 +117,21 @@ declare class UpstashStore extends MastraStorage {
|
|
|
107
117
|
workflowName: string;
|
|
108
118
|
runId: string;
|
|
109
119
|
}): Promise<WorkflowRunState | null>;
|
|
110
|
-
|
|
120
|
+
private parseWorkflowRun;
|
|
121
|
+
getWorkflowRuns({ namespace, workflowName, fromDate, toDate, limit, offset, resourceId, }?: {
|
|
111
122
|
namespace: string;
|
|
112
123
|
workflowName?: string;
|
|
113
124
|
fromDate?: Date;
|
|
114
125
|
toDate?: Date;
|
|
115
126
|
limit?: number;
|
|
116
127
|
offset?: number;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}>;
|
|
125
|
-
total: number;
|
|
126
|
-
}>;
|
|
128
|
+
resourceId?: string;
|
|
129
|
+
}): Promise<WorkflowRuns>;
|
|
130
|
+
getWorkflowRunById({ namespace, runId, workflowName, }: {
|
|
131
|
+
namespace: string;
|
|
132
|
+
runId: string;
|
|
133
|
+
workflowName?: string;
|
|
134
|
+
}): Promise<WorkflowRun | null>;
|
|
127
135
|
close(): Promise<void>;
|
|
128
136
|
}
|
|
129
137
|
export { UpstashStore }
|
package/dist/index.cjs
CHANGED
|
@@ -207,7 +207,8 @@ var UpstashStore = class extends storage.MastraStorage {
|
|
|
207
207
|
key = this.getKey(tableName, {
|
|
208
208
|
namespace: record.namespace || "workflows",
|
|
209
209
|
workflow_name: record.workflow_name,
|
|
210
|
-
run_id: record.run_id
|
|
210
|
+
run_id: record.run_id,
|
|
211
|
+
...record.resourceId ? { resourceId: record.resourceId } : {}
|
|
211
212
|
});
|
|
212
213
|
} else if (tableName === storage.TABLE_EVALS) {
|
|
213
214
|
key = this.getKey(tableName, { id: record.run_id });
|
|
@@ -372,48 +373,90 @@ var UpstashStore = class extends storage.MastraStorage {
|
|
|
372
373
|
if (!data) return null;
|
|
373
374
|
return data.snapshot;
|
|
374
375
|
}
|
|
376
|
+
parseWorkflowRun(row) {
|
|
377
|
+
let parsedSnapshot = row.snapshot;
|
|
378
|
+
if (typeof parsedSnapshot === "string") {
|
|
379
|
+
try {
|
|
380
|
+
parsedSnapshot = JSON.parse(row.snapshot);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
workflowName: row.workflow_name,
|
|
387
|
+
runId: row.run_id,
|
|
388
|
+
snapshot: parsedSnapshot,
|
|
389
|
+
createdAt: this.ensureDate(row.createdAt),
|
|
390
|
+
updatedAt: this.ensureDate(row.updatedAt),
|
|
391
|
+
resourceId: row.resourceId
|
|
392
|
+
};
|
|
393
|
+
}
|
|
375
394
|
async getWorkflowRuns({
|
|
376
395
|
namespace,
|
|
377
396
|
workflowName,
|
|
378
397
|
fromDate,
|
|
379
398
|
toDate,
|
|
380
399
|
limit,
|
|
381
|
-
offset
|
|
400
|
+
offset,
|
|
401
|
+
resourceId
|
|
382
402
|
} = { namespace: "workflows" }) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
403
|
+
try {
|
|
404
|
+
let pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ":*";
|
|
405
|
+
if (workflowName && resourceId) {
|
|
406
|
+
pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, {
|
|
407
|
+
namespace,
|
|
408
|
+
workflow_name: workflowName,
|
|
409
|
+
run_id: "*",
|
|
410
|
+
resourceId
|
|
411
|
+
});
|
|
412
|
+
} else if (workflowName) {
|
|
413
|
+
pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ":*";
|
|
414
|
+
} else if (resourceId) {
|
|
415
|
+
pattern = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: "*", run_id: "*", resourceId });
|
|
416
|
+
}
|
|
417
|
+
const keys = await this.redis.keys(pattern);
|
|
418
|
+
const workflows = await Promise.all(
|
|
419
|
+
keys.map(async (key) => {
|
|
420
|
+
const data = await this.redis.get(key);
|
|
421
|
+
return data;
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
let runs = workflows.filter((w) => w !== null).map((w) => this.parseWorkflowRun(w)).filter((w) => {
|
|
425
|
+
if (fromDate && w.createdAt < fromDate) return false;
|
|
426
|
+
if (toDate && w.createdAt > toDate) return false;
|
|
427
|
+
return true;
|
|
428
|
+
}).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
429
|
+
const total = runs.length;
|
|
430
|
+
if (limit !== void 0 && offset !== void 0) {
|
|
431
|
+
runs = runs.slice(offset, offset + limit);
|
|
399
432
|
}
|
|
400
|
-
return {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
433
|
+
return { runs, total };
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error("Error getting workflow runs:", error);
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async getWorkflowRunById({
|
|
440
|
+
namespace = "workflows",
|
|
441
|
+
runId,
|
|
442
|
+
workflowName
|
|
443
|
+
}) {
|
|
444
|
+
try {
|
|
445
|
+
const key = this.getKey(storage.TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + "*";
|
|
446
|
+
const keys = await this.redis.keys(key);
|
|
447
|
+
const workflows = await Promise.all(
|
|
448
|
+
keys.map(async (key2) => {
|
|
449
|
+
const data2 = await this.redis.get(key2);
|
|
450
|
+
return data2;
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
const data = workflows.find((w) => w?.run_id === runId && w?.workflow_name === workflowName);
|
|
454
|
+
if (!data) return null;
|
|
455
|
+
return this.parseWorkflowRun(data);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error("Error getting workflow run by ID:", error);
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
417
460
|
}
|
|
418
461
|
async close() {
|
|
419
462
|
}
|
|
@@ -713,5 +756,81 @@ var UpstashVector = class extends vector.MastraVector {
|
|
|
713
756
|
}
|
|
714
757
|
};
|
|
715
758
|
|
|
759
|
+
// src/vector/prompt.ts
|
|
760
|
+
var UPSTASH_PROMPT = `When querying Upstash Vector, you can ONLY use the operators listed below. Any other operators will be rejected.
|
|
761
|
+
Important: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.
|
|
762
|
+
If a user tries to give an explicit operator that is not supported, reject the filter entirely and let them know that the operator is not supported.
|
|
763
|
+
|
|
764
|
+
Basic Comparison Operators:
|
|
765
|
+
- $eq: Exact match (default when using field: value)
|
|
766
|
+
Example: { "category": "electronics" }
|
|
767
|
+
- $ne: Not equal
|
|
768
|
+
Example: { "category": { "$ne": "electronics" } }
|
|
769
|
+
- $gt: Greater than
|
|
770
|
+
Example: { "price": { "$gt": 100 } }
|
|
771
|
+
- $gte: Greater than or equal
|
|
772
|
+
Example: { "price": { "$gte": 100 } }
|
|
773
|
+
- $lt: Less than
|
|
774
|
+
Example: { "price": { "$lt": 100 } }
|
|
775
|
+
- $lte: Less than or equal
|
|
776
|
+
Example: { "price": { "$lte": 100 } }
|
|
777
|
+
|
|
778
|
+
Array Operators:
|
|
779
|
+
- $in: Match any value in array
|
|
780
|
+
Example: { "category": { "$in": ["electronics", "books"] } }
|
|
781
|
+
- $nin: Does not match any value in array
|
|
782
|
+
Example: { "category": { "$nin": ["electronics", "books"] } }
|
|
783
|
+
|
|
784
|
+
Logical Operators:
|
|
785
|
+
- $and: Logical AND (can be implicit or explicit)
|
|
786
|
+
Implicit Example: { "price": { "$gt": 100 }, "category": "electronics" }
|
|
787
|
+
Explicit Example: { "$and": [{ "price": { "$gt": 100 } }, { "category": "electronics" }] }
|
|
788
|
+
- $or: Logical OR
|
|
789
|
+
Example: { "$or": [{ "price": { "$lt": 50 } }, { "category": "books" }] }
|
|
790
|
+
|
|
791
|
+
Element Operators:
|
|
792
|
+
- $exists: Check if field exists
|
|
793
|
+
Example: { "rating": { "$exists": true } }
|
|
794
|
+
|
|
795
|
+
Restrictions:
|
|
796
|
+
- Regex patterns are not supported
|
|
797
|
+
- Only $and and $or logical operators are supported at the top level
|
|
798
|
+
- Empty arrays in $in/$nin will return no results
|
|
799
|
+
- Nested fields are supported using dot notation
|
|
800
|
+
- Multiple conditions on the same field are supported with both implicit and explicit $and
|
|
801
|
+
- At least one key-value pair is required in filter object
|
|
802
|
+
- Empty objects and undefined values are treated as no filter
|
|
803
|
+
- Invalid types in comparison operators will throw errors
|
|
804
|
+
- All non-logical operators must be used within a field condition
|
|
805
|
+
Valid: { "field": { "$gt": 100 } }
|
|
806
|
+
Valid: { "$and": [...] }
|
|
807
|
+
Invalid: { "$gt": 100 }
|
|
808
|
+
- Logical operators must contain field conditions, not direct operators
|
|
809
|
+
Valid: { "$and": [{ "field": { "$gt": 100 } }] }
|
|
810
|
+
Invalid: { "$and": [{ "$gt": 100 }] }
|
|
811
|
+
- Logical operators ($and, $or):
|
|
812
|
+
- Can only be used at top level or nested within other logical operators
|
|
813
|
+
- Can not be used on a field level, or be nested inside a field
|
|
814
|
+
- Can not be used inside an operator
|
|
815
|
+
- Valid: { "$and": [{ "field": { "$gt": 100 } }] }
|
|
816
|
+
- Valid: { "$or": [{ "$and": [{ "field": { "$gt": 100 } }] }] }
|
|
817
|
+
- Invalid: { "field": { "$and": [{ "$gt": 100 }] } }
|
|
818
|
+
- Invalid: { "field": { "$or": [{ "$gt": 100 }] } }
|
|
819
|
+
- Invalid: { "field": { "$gt": { "$and": [{...}] } } }
|
|
820
|
+
|
|
821
|
+
Example Complex Query:
|
|
822
|
+
{
|
|
823
|
+
"$and": [
|
|
824
|
+
{ "category": { "$in": ["electronics", "computers"] } },
|
|
825
|
+
{ "price": { "$gte": 100, "$lte": 1000 } },
|
|
826
|
+
{ "rating": { "$exists": true, "$gt": 4 } },
|
|
827
|
+
{ "$or": [
|
|
828
|
+
{ "stock": { "$gt": 0 } },
|
|
829
|
+
{ "preorder": true }
|
|
830
|
+
]}
|
|
831
|
+
]
|
|
832
|
+
}`;
|
|
833
|
+
|
|
834
|
+
exports.UPSTASH_PROMPT = UPSTASH_PROMPT;
|
|
716
835
|
exports.UpstashStore = UpstashStore;
|
|
717
836
|
exports.UpstashVector = UpstashVector;
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -205,7 +205,8 @@ var UpstashStore = class extends MastraStorage {
|
|
|
205
205
|
key = this.getKey(tableName, {
|
|
206
206
|
namespace: record.namespace || "workflows",
|
|
207
207
|
workflow_name: record.workflow_name,
|
|
208
|
-
run_id: record.run_id
|
|
208
|
+
run_id: record.run_id,
|
|
209
|
+
...record.resourceId ? { resourceId: record.resourceId } : {}
|
|
209
210
|
});
|
|
210
211
|
} else if (tableName === TABLE_EVALS) {
|
|
211
212
|
key = this.getKey(tableName, { id: record.run_id });
|
|
@@ -370,48 +371,90 @@ var UpstashStore = class extends MastraStorage {
|
|
|
370
371
|
if (!data) return null;
|
|
371
372
|
return data.snapshot;
|
|
372
373
|
}
|
|
374
|
+
parseWorkflowRun(row) {
|
|
375
|
+
let parsedSnapshot = row.snapshot;
|
|
376
|
+
if (typeof parsedSnapshot === "string") {
|
|
377
|
+
try {
|
|
378
|
+
parsedSnapshot = JSON.parse(row.snapshot);
|
|
379
|
+
} catch (e) {
|
|
380
|
+
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
workflowName: row.workflow_name,
|
|
385
|
+
runId: row.run_id,
|
|
386
|
+
snapshot: parsedSnapshot,
|
|
387
|
+
createdAt: this.ensureDate(row.createdAt),
|
|
388
|
+
updatedAt: this.ensureDate(row.updatedAt),
|
|
389
|
+
resourceId: row.resourceId
|
|
390
|
+
};
|
|
391
|
+
}
|
|
373
392
|
async getWorkflowRuns({
|
|
374
393
|
namespace,
|
|
375
394
|
workflowName,
|
|
376
395
|
fromDate,
|
|
377
396
|
toDate,
|
|
378
397
|
limit,
|
|
379
|
-
offset
|
|
398
|
+
offset,
|
|
399
|
+
resourceId
|
|
380
400
|
} = { namespace: "workflows" }) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
401
|
+
try {
|
|
402
|
+
let pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ":*";
|
|
403
|
+
if (workflowName && resourceId) {
|
|
404
|
+
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
|
|
405
|
+
namespace,
|
|
406
|
+
workflow_name: workflowName,
|
|
407
|
+
run_id: "*",
|
|
408
|
+
resourceId
|
|
409
|
+
});
|
|
410
|
+
} else if (workflowName) {
|
|
411
|
+
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ":*";
|
|
412
|
+
} else if (resourceId) {
|
|
413
|
+
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: "*", run_id: "*", resourceId });
|
|
414
|
+
}
|
|
415
|
+
const keys = await this.redis.keys(pattern);
|
|
416
|
+
const workflows = await Promise.all(
|
|
417
|
+
keys.map(async (key) => {
|
|
418
|
+
const data = await this.redis.get(key);
|
|
419
|
+
return data;
|
|
420
|
+
})
|
|
421
|
+
);
|
|
422
|
+
let runs = workflows.filter((w) => w !== null).map((w) => this.parseWorkflowRun(w)).filter((w) => {
|
|
423
|
+
if (fromDate && w.createdAt < fromDate) return false;
|
|
424
|
+
if (toDate && w.createdAt > toDate) return false;
|
|
425
|
+
return true;
|
|
426
|
+
}).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
427
|
+
const total = runs.length;
|
|
428
|
+
if (limit !== void 0 && offset !== void 0) {
|
|
429
|
+
runs = runs.slice(offset, offset + limit);
|
|
397
430
|
}
|
|
398
|
-
return {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
431
|
+
return { runs, total };
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error("Error getting workflow runs:", error);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async getWorkflowRunById({
|
|
438
|
+
namespace = "workflows",
|
|
439
|
+
runId,
|
|
440
|
+
workflowName
|
|
441
|
+
}) {
|
|
442
|
+
try {
|
|
443
|
+
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + "*";
|
|
444
|
+
const keys = await this.redis.keys(key);
|
|
445
|
+
const workflows = await Promise.all(
|
|
446
|
+
keys.map(async (key2) => {
|
|
447
|
+
const data2 = await this.redis.get(key2);
|
|
448
|
+
return data2;
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
const data = workflows.find((w) => w?.run_id === runId && w?.workflow_name === workflowName);
|
|
452
|
+
if (!data) return null;
|
|
453
|
+
return this.parseWorkflowRun(data);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
console.error("Error getting workflow run by ID:", error);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
415
458
|
}
|
|
416
459
|
async close() {
|
|
417
460
|
}
|
|
@@ -711,4 +754,79 @@ var UpstashVector = class extends MastraVector {
|
|
|
711
754
|
}
|
|
712
755
|
};
|
|
713
756
|
|
|
714
|
-
|
|
757
|
+
// src/vector/prompt.ts
|
|
758
|
+
var UPSTASH_PROMPT = `When querying Upstash Vector, you can ONLY use the operators listed below. Any other operators will be rejected.
|
|
759
|
+
Important: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.
|
|
760
|
+
If a user tries to give an explicit operator that is not supported, reject the filter entirely and let them know that the operator is not supported.
|
|
761
|
+
|
|
762
|
+
Basic Comparison Operators:
|
|
763
|
+
- $eq: Exact match (default when using field: value)
|
|
764
|
+
Example: { "category": "electronics" }
|
|
765
|
+
- $ne: Not equal
|
|
766
|
+
Example: { "category": { "$ne": "electronics" } }
|
|
767
|
+
- $gt: Greater than
|
|
768
|
+
Example: { "price": { "$gt": 100 } }
|
|
769
|
+
- $gte: Greater than or equal
|
|
770
|
+
Example: { "price": { "$gte": 100 } }
|
|
771
|
+
- $lt: Less than
|
|
772
|
+
Example: { "price": { "$lt": 100 } }
|
|
773
|
+
- $lte: Less than or equal
|
|
774
|
+
Example: { "price": { "$lte": 100 } }
|
|
775
|
+
|
|
776
|
+
Array Operators:
|
|
777
|
+
- $in: Match any value in array
|
|
778
|
+
Example: { "category": { "$in": ["electronics", "books"] } }
|
|
779
|
+
- $nin: Does not match any value in array
|
|
780
|
+
Example: { "category": { "$nin": ["electronics", "books"] } }
|
|
781
|
+
|
|
782
|
+
Logical Operators:
|
|
783
|
+
- $and: Logical AND (can be implicit or explicit)
|
|
784
|
+
Implicit Example: { "price": { "$gt": 100 }, "category": "electronics" }
|
|
785
|
+
Explicit Example: { "$and": [{ "price": { "$gt": 100 } }, { "category": "electronics" }] }
|
|
786
|
+
- $or: Logical OR
|
|
787
|
+
Example: { "$or": [{ "price": { "$lt": 50 } }, { "category": "books" }] }
|
|
788
|
+
|
|
789
|
+
Element Operators:
|
|
790
|
+
- $exists: Check if field exists
|
|
791
|
+
Example: { "rating": { "$exists": true } }
|
|
792
|
+
|
|
793
|
+
Restrictions:
|
|
794
|
+
- Regex patterns are not supported
|
|
795
|
+
- Only $and and $or logical operators are supported at the top level
|
|
796
|
+
- Empty arrays in $in/$nin will return no results
|
|
797
|
+
- Nested fields are supported using dot notation
|
|
798
|
+
- Multiple conditions on the same field are supported with both implicit and explicit $and
|
|
799
|
+
- At least one key-value pair is required in filter object
|
|
800
|
+
- Empty objects and undefined values are treated as no filter
|
|
801
|
+
- Invalid types in comparison operators will throw errors
|
|
802
|
+
- All non-logical operators must be used within a field condition
|
|
803
|
+
Valid: { "field": { "$gt": 100 } }
|
|
804
|
+
Valid: { "$and": [...] }
|
|
805
|
+
Invalid: { "$gt": 100 }
|
|
806
|
+
- Logical operators must contain field conditions, not direct operators
|
|
807
|
+
Valid: { "$and": [{ "field": { "$gt": 100 } }] }
|
|
808
|
+
Invalid: { "$and": [{ "$gt": 100 }] }
|
|
809
|
+
- Logical operators ($and, $or):
|
|
810
|
+
- Can only be used at top level or nested within other logical operators
|
|
811
|
+
- Can not be used on a field level, or be nested inside a field
|
|
812
|
+
- Can not be used inside an operator
|
|
813
|
+
- Valid: { "$and": [{ "field": { "$gt": 100 } }] }
|
|
814
|
+
- Valid: { "$or": [{ "$and": [{ "field": { "$gt": 100 } }] }] }
|
|
815
|
+
- Invalid: { "field": { "$and": [{ "$gt": 100 }] } }
|
|
816
|
+
- Invalid: { "field": { "$or": [{ "$gt": 100 }] } }
|
|
817
|
+
- Invalid: { "field": { "$gt": { "$and": [{...}] } } }
|
|
818
|
+
|
|
819
|
+
Example Complex Query:
|
|
820
|
+
{
|
|
821
|
+
"$and": [
|
|
822
|
+
{ "category": { "$in": ["electronics", "computers"] } },
|
|
823
|
+
{ "price": { "$gte": 100, "$lte": 1000 } },
|
|
824
|
+
{ "rating": { "$exists": true, "$gt": 4 } },
|
|
825
|
+
{ "$or": [
|
|
826
|
+
{ "stock": { "$gt": 0 } },
|
|
827
|
+
{ "preorder": true }
|
|
828
|
+
]}
|
|
829
|
+
]
|
|
830
|
+
}`;
|
|
831
|
+
|
|
832
|
+
export { UPSTASH_PROMPT, UpstashStore, UpstashVector };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/upstash",
|
|
3
|
-
"version": "0.3.1-alpha.
|
|
3
|
+
"version": "0.3.1-alpha.5",
|
|
4
4
|
"description": "Upstash provider for Mastra - includes both vector and db storage capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@upstash/redis": "^1.34.5",
|
|
24
24
|
"@upstash/vector": "^1.2.1",
|
|
25
|
-
"@mastra/core": "^0.9.1-alpha.
|
|
25
|
+
"@mastra/core": "^0.9.1-alpha.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@microsoft/api-extractor": "^7.52.
|
|
28
|
+
"@microsoft/api-extractor": "^7.52.5",
|
|
29
29
|
"@types/node": "^20.17.27",
|
|
30
30
|
"dotenv": "^16.4.7",
|
|
31
31
|
"eslint": "^9.23.0",
|
|
32
32
|
"tsup": "^8.4.0",
|
|
33
33
|
"typescript": "^5.8.2",
|
|
34
|
-
"vitest": "^3.
|
|
34
|
+
"vitest": "^3.1.2",
|
|
35
35
|
"@internal/lint": "0.0.2"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
package/src/index.ts
CHANGED
package/src/storage/index.ts
CHANGED
|
@@ -8,7 +8,14 @@ import {
|
|
|
8
8
|
TABLE_EVALS,
|
|
9
9
|
TABLE_TRACES,
|
|
10
10
|
} from '@mastra/core/storage';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
TABLE_NAMES,
|
|
13
|
+
StorageColumn,
|
|
14
|
+
StorageGetMessagesArg,
|
|
15
|
+
EvalRow,
|
|
16
|
+
WorkflowRuns,
|
|
17
|
+
WorkflowRun,
|
|
18
|
+
} from '@mastra/core/storage';
|
|
12
19
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
13
20
|
import { Redis } from '@upstash/redis';
|
|
14
21
|
|
|
@@ -298,6 +305,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
298
305
|
namespace: record.namespace || 'workflows',
|
|
299
306
|
workflow_name: record.workflow_name,
|
|
300
307
|
run_id: record.run_id,
|
|
308
|
+
...(record.resourceId ? { resourceId: record.resourceId } : {}),
|
|
301
309
|
});
|
|
302
310
|
} else if (tableName === TABLE_EVALS) {
|
|
303
311
|
key = this.getKey(tableName, { id: record.run_id });
|
|
@@ -531,6 +539,27 @@ export class UpstashStore extends MastraStorage {
|
|
|
531
539
|
return data.snapshot;
|
|
532
540
|
}
|
|
533
541
|
|
|
542
|
+
private parseWorkflowRun(row: any): WorkflowRun {
|
|
543
|
+
let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
|
|
544
|
+
if (typeof parsedSnapshot === 'string') {
|
|
545
|
+
try {
|
|
546
|
+
parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
|
|
547
|
+
} catch (e) {
|
|
548
|
+
// If parsing fails, return the raw snapshot string
|
|
549
|
+
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
workflowName: row.workflow_name,
|
|
555
|
+
runId: row.run_id,
|
|
556
|
+
snapshot: parsedSnapshot,
|
|
557
|
+
createdAt: this.ensureDate(row.createdAt)!,
|
|
558
|
+
updatedAt: this.ensureDate(row.updatedAt)!,
|
|
559
|
+
resourceId: row.resourceId,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
534
563
|
async getWorkflowRuns(
|
|
535
564
|
{
|
|
536
565
|
namespace,
|
|
@@ -539,6 +568,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
539
568
|
toDate,
|
|
540
569
|
limit,
|
|
541
570
|
offset,
|
|
571
|
+
resourceId,
|
|
542
572
|
}: {
|
|
543
573
|
namespace: string;
|
|
544
574
|
workflowName?: string;
|
|
@@ -546,74 +576,98 @@ export class UpstashStore extends MastraStorage {
|
|
|
546
576
|
toDate?: Date;
|
|
547
577
|
limit?: number;
|
|
548
578
|
offset?: number;
|
|
579
|
+
resourceId?: string;
|
|
549
580
|
} = { namespace: 'workflows' },
|
|
550
|
-
): Promise<{
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
581
|
+
): Promise<WorkflowRuns> {
|
|
582
|
+
try {
|
|
583
|
+
// Get all workflow keys
|
|
584
|
+
let pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ':*';
|
|
585
|
+
if (workflowName && resourceId) {
|
|
586
|
+
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
|
|
587
|
+
namespace,
|
|
588
|
+
workflow_name: workflowName,
|
|
589
|
+
run_id: '*',
|
|
590
|
+
resourceId,
|
|
591
|
+
});
|
|
592
|
+
} else if (workflowName) {
|
|
593
|
+
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ':*';
|
|
594
|
+
} else if (resourceId) {
|
|
595
|
+
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: '*', run_id: '*', resourceId });
|
|
596
|
+
}
|
|
597
|
+
const keys = await this.redis.keys(pattern);
|
|
564
598
|
|
|
565
|
-
|
|
599
|
+
// Get all workflow data
|
|
600
|
+
const workflows = await Promise.all(
|
|
601
|
+
keys.map(async key => {
|
|
602
|
+
const data = await this.redis.get<{
|
|
603
|
+
workflow_name: string;
|
|
604
|
+
run_id: string;
|
|
605
|
+
snapshot: WorkflowRunState | string;
|
|
606
|
+
createdAt: string | Date;
|
|
607
|
+
updatedAt: string | Date;
|
|
608
|
+
resourceId: string;
|
|
609
|
+
}>(key);
|
|
610
|
+
return data;
|
|
611
|
+
}),
|
|
612
|
+
);
|
|
566
613
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
614
|
+
// Filter and transform results
|
|
615
|
+
let runs = workflows
|
|
616
|
+
.filter(w => w !== null)
|
|
617
|
+
.map(w => this.parseWorkflowRun(w!))
|
|
618
|
+
.filter(w => {
|
|
619
|
+
if (fromDate && w.createdAt < fromDate) return false;
|
|
620
|
+
if (toDate && w.createdAt > toDate) return false;
|
|
621
|
+
return true;
|
|
622
|
+
})
|
|
623
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
624
|
+
|
|
625
|
+
const total = runs.length;
|
|
626
|
+
|
|
627
|
+
// Apply pagination if requested
|
|
628
|
+
if (limit !== undefined && offset !== undefined) {
|
|
629
|
+
runs = runs.slice(offset, offset + limit);
|
|
630
|
+
}
|
|
580
631
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
.
|
|
584
|
-
|
|
585
|
-
let parsedSnapshot: WorkflowRunState | string = w!.snapshot as string;
|
|
586
|
-
if (typeof parsedSnapshot === 'string') {
|
|
587
|
-
try {
|
|
588
|
-
parsedSnapshot = JSON.parse(w!.snapshot as string) as WorkflowRunState;
|
|
589
|
-
} catch {
|
|
590
|
-
// If parsing fails, return the raw snapshot string
|
|
591
|
-
console.warn(`Failed to parse snapshot for workflow ${w!.workflow_name}:`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
workflowName: w!.workflow_name,
|
|
596
|
-
runId: w!.run_id,
|
|
597
|
-
snapshot: parsedSnapshot,
|
|
598
|
-
createdAt: this.ensureDate(w!.createdAt)!,
|
|
599
|
-
updatedAt: this.ensureDate(w!.updatedAt)!,
|
|
600
|
-
};
|
|
601
|
-
})
|
|
602
|
-
.filter(w => {
|
|
603
|
-
if (fromDate && w.createdAt < fromDate) return false;
|
|
604
|
-
if (toDate && w.createdAt > toDate) return false;
|
|
605
|
-
return true;
|
|
606
|
-
})
|
|
607
|
-
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
608
|
-
|
|
609
|
-
const total = runs.length;
|
|
610
|
-
|
|
611
|
-
// Apply pagination if requested
|
|
612
|
-
if (limit !== undefined && offset !== undefined) {
|
|
613
|
-
runs = runs.slice(offset, offset + limit);
|
|
632
|
+
return { runs, total };
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.error('Error getting workflow runs:', error);
|
|
635
|
+
throw error;
|
|
614
636
|
}
|
|
637
|
+
}
|
|
615
638
|
|
|
616
|
-
|
|
639
|
+
async getWorkflowRunById({
|
|
640
|
+
namespace = 'workflows',
|
|
641
|
+
runId,
|
|
642
|
+
workflowName,
|
|
643
|
+
}: {
|
|
644
|
+
namespace: string;
|
|
645
|
+
runId: string;
|
|
646
|
+
workflowName?: string;
|
|
647
|
+
}): Promise<WorkflowRun | null> {
|
|
648
|
+
try {
|
|
649
|
+
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + '*';
|
|
650
|
+
const keys = await this.redis.keys(key);
|
|
651
|
+
const workflows = await Promise.all(
|
|
652
|
+
keys.map(async key => {
|
|
653
|
+
const data = await this.redis.get<{
|
|
654
|
+
workflow_name: string;
|
|
655
|
+
run_id: string;
|
|
656
|
+
snapshot: WorkflowRunState | string;
|
|
657
|
+
createdAt: string | Date;
|
|
658
|
+
updatedAt: string | Date;
|
|
659
|
+
resourceId: string;
|
|
660
|
+
}>(key);
|
|
661
|
+
return data;
|
|
662
|
+
}),
|
|
663
|
+
);
|
|
664
|
+
const data = workflows.find(w => w?.run_id === runId && w?.workflow_name === workflowName) as WorkflowRun | null;
|
|
665
|
+
if (!data) return null;
|
|
666
|
+
return this.parseWorkflowRun(data);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error('Error getting workflow run by ID:', error);
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
617
671
|
}
|
|
618
672
|
|
|
619
673
|
async close(): Promise<void> {
|
|
@@ -25,27 +25,26 @@ const createSampleThread = (date?: Date) => ({
|
|
|
25
25
|
metadata: { key: 'value' },
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
const createSampleMessage = (threadId: string, content: string = 'Hello') =>
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
const createSampleMessage = (threadId: string, content: string = 'Hello'): MessageType => ({
|
|
29
|
+
id: `msg-${randomUUID()}`,
|
|
30
|
+
role: 'user',
|
|
31
|
+
type: 'text',
|
|
32
|
+
threadId,
|
|
33
|
+
content: [{ type: 'text', text: content }],
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
resourceId: `resource-${randomUUID()}`,
|
|
36
|
+
});
|
|
37
37
|
|
|
38
38
|
const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
|
|
39
39
|
const runId = `run-${randomUUID()}`;
|
|
40
40
|
const stepId = `step-${randomUUID()}`;
|
|
41
41
|
const timestamp = createdAt || new Date();
|
|
42
|
-
const snapshot = {
|
|
43
|
-
result: { success: true },
|
|
42
|
+
const snapshot: WorkflowRunState = {
|
|
44
43
|
value: {},
|
|
45
44
|
context: {
|
|
46
45
|
steps: {
|
|
47
46
|
[stepId]: {
|
|
48
|
-
status,
|
|
47
|
+
status: status as WorkflowRunState['context']['steps'][string]['status'],
|
|
49
48
|
payload: {},
|
|
50
49
|
error: undefined,
|
|
51
50
|
},
|
|
@@ -54,9 +53,10 @@ const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
|
|
|
54
53
|
attempts: {},
|
|
55
54
|
},
|
|
56
55
|
activePaths: [],
|
|
56
|
+
suspendedPaths: {},
|
|
57
57
|
runId,
|
|
58
58
|
timestamp: timestamp.getTime(),
|
|
59
|
-
}
|
|
59
|
+
};
|
|
60
60
|
return { snapshot, runId, stepId };
|
|
61
61
|
};
|
|
62
62
|
|
|
@@ -94,6 +94,13 @@ const createSampleEval = (agentName: string, isTest = false) => {
|
|
|
94
94
|
};
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
|
|
98
|
+
if (typeof snapshot === 'string') {
|
|
99
|
+
throw new Error('Expected WorkflowRunState, got string');
|
|
100
|
+
}
|
|
101
|
+
expect(snapshot.context?.steps[stepId]?.status).toBe(status);
|
|
102
|
+
};
|
|
103
|
+
|
|
97
104
|
describe('UpstashStore', () => {
|
|
98
105
|
let store: UpstashStore;
|
|
99
106
|
const testTableName = 'test_table';
|
|
@@ -303,17 +310,17 @@ describe('UpstashStore', () => {
|
|
|
303
310
|
});
|
|
304
311
|
|
|
305
312
|
it('should save and retrieve messages in order', async () => {
|
|
306
|
-
const messages = [
|
|
313
|
+
const messages: MessageType[] = [
|
|
307
314
|
createSampleMessage(threadId, 'First'),
|
|
308
315
|
createSampleMessage(threadId, 'Second'),
|
|
309
316
|
createSampleMessage(threadId, 'Third'),
|
|
310
317
|
];
|
|
311
318
|
|
|
312
|
-
await store.saveMessages({ messages: messages
|
|
319
|
+
await store.saveMessages({ messages: messages });
|
|
313
320
|
|
|
314
|
-
const retrievedMessages = await store.getMessages({ threadId });
|
|
321
|
+
const retrievedMessages = await store.getMessages<MessageType[]>({ threadId });
|
|
315
322
|
expect(retrievedMessages).toHaveLength(3);
|
|
316
|
-
expect(retrievedMessages.map(m => m.content[0].text)).toEqual(['First', 'Second', 'Third']);
|
|
323
|
+
expect(retrievedMessages.map((m: any) => m.content[0].text)).toEqual(['First', 'Second', 'Third']);
|
|
317
324
|
});
|
|
318
325
|
|
|
319
326
|
it('should handle empty message array', async () => {
|
|
@@ -335,11 +342,11 @@ describe('UpstashStore', () => {
|
|
|
335
342
|
],
|
|
336
343
|
createdAt: new Date(),
|
|
337
344
|
},
|
|
338
|
-
];
|
|
345
|
+
] as MessageType[];
|
|
339
346
|
|
|
340
|
-
await store.saveMessages({ messages
|
|
347
|
+
await store.saveMessages({ messages });
|
|
341
348
|
|
|
342
|
-
const retrievedMessages = await store.getMessages({ threadId });
|
|
349
|
+
const retrievedMessages = await store.getMessages<MessageType>({ threadId });
|
|
343
350
|
expect(retrievedMessages[0].content).toEqual(messages[0].content);
|
|
344
351
|
});
|
|
345
352
|
});
|
|
@@ -451,13 +458,15 @@ describe('UpstashStore', () => {
|
|
|
451
458
|
stepResults: {
|
|
452
459
|
step1: { status: 'success', payload: { result: 'done' } },
|
|
453
460
|
},
|
|
461
|
+
steps: {},
|
|
454
462
|
attempts: {},
|
|
455
463
|
triggerData: {},
|
|
456
464
|
},
|
|
457
465
|
runId: testRunId,
|
|
458
466
|
activePaths: [],
|
|
467
|
+
suspendedPaths: {},
|
|
459
468
|
timestamp: Date.now(),
|
|
460
|
-
}
|
|
469
|
+
};
|
|
461
470
|
|
|
462
471
|
await store.persistWorkflowSnapshot({
|
|
463
472
|
namespace: testNamespace,
|
|
@@ -556,8 +565,8 @@ describe('UpstashStore', () => {
|
|
|
556
565
|
const workflowName1 = 'default_test_1';
|
|
557
566
|
const workflowName2 = 'default_test_2';
|
|
558
567
|
|
|
559
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('
|
|
560
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
568
|
+
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
569
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
|
|
561
570
|
|
|
562
571
|
await store.persistWorkflowSnapshot({
|
|
563
572
|
namespace: testNamespace,
|
|
@@ -578,17 +587,17 @@ describe('UpstashStore', () => {
|
|
|
578
587
|
expect(total).toBe(2);
|
|
579
588
|
expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
|
|
580
589
|
expect(runs[1]!.workflowName).toBe(workflowName1);
|
|
581
|
-
const firstSnapshot = runs[0]!.snapshot
|
|
582
|
-
const secondSnapshot = runs[1]!.snapshot
|
|
583
|
-
|
|
584
|
-
|
|
590
|
+
const firstSnapshot = runs[0]!.snapshot;
|
|
591
|
+
const secondSnapshot = runs[1]!.snapshot;
|
|
592
|
+
checkWorkflowSnapshot(firstSnapshot, stepId2, 'waiting');
|
|
593
|
+
checkWorkflowSnapshot(secondSnapshot, stepId1, 'success');
|
|
585
594
|
});
|
|
586
595
|
|
|
587
596
|
it('filters by workflow name', async () => {
|
|
588
597
|
const workflowName1 = 'filter_test_1';
|
|
589
598
|
const workflowName2 = 'filter_test_2';
|
|
590
599
|
|
|
591
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('
|
|
600
|
+
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
592
601
|
const { snapshot: workflow2, runId: runId2 } = createSampleWorkflowSnapshot('failed');
|
|
593
602
|
|
|
594
603
|
await store.persistWorkflowSnapshot({
|
|
@@ -609,8 +618,11 @@ describe('UpstashStore', () => {
|
|
|
609
618
|
expect(runs).toHaveLength(1);
|
|
610
619
|
expect(total).toBe(1);
|
|
611
620
|
expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
612
|
-
const snapshot = runs[0]!.snapshot
|
|
613
|
-
|
|
621
|
+
const snapshot = runs[0]!.snapshot;
|
|
622
|
+
if (typeof snapshot === 'string') {
|
|
623
|
+
throw new Error('Expected WorkflowRunState, got string');
|
|
624
|
+
}
|
|
625
|
+
expect(snapshot.context?.steps[stepId1]?.status).toBe('success');
|
|
614
626
|
});
|
|
615
627
|
|
|
616
628
|
it('filters by date range', async () => {
|
|
@@ -621,9 +633,9 @@ describe('UpstashStore', () => {
|
|
|
621
633
|
const workflowName2 = 'date_test_2';
|
|
622
634
|
const workflowName3 = 'date_test_3';
|
|
623
635
|
|
|
624
|
-
const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('
|
|
625
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
626
|
-
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('
|
|
636
|
+
const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('success');
|
|
637
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
|
|
638
|
+
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
|
|
627
639
|
|
|
628
640
|
await store.insert({
|
|
629
641
|
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
@@ -668,10 +680,10 @@ describe('UpstashStore', () => {
|
|
|
668
680
|
expect(runs).toHaveLength(2);
|
|
669
681
|
expect(runs[0]!.workflowName).toBe(workflowName3);
|
|
670
682
|
expect(runs[1]!.workflowName).toBe(workflowName2);
|
|
671
|
-
const firstSnapshot = runs[0]!.snapshot
|
|
672
|
-
const secondSnapshot = runs[1]!.snapshot
|
|
673
|
-
|
|
674
|
-
|
|
683
|
+
const firstSnapshot = runs[0]!.snapshot;
|
|
684
|
+
const secondSnapshot = runs[1]!.snapshot;
|
|
685
|
+
checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
|
|
686
|
+
checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
|
|
675
687
|
});
|
|
676
688
|
|
|
677
689
|
it('handles pagination', async () => {
|
|
@@ -679,9 +691,9 @@ describe('UpstashStore', () => {
|
|
|
679
691
|
const workflowName2 = 'page_test_2';
|
|
680
692
|
const workflowName3 = 'page_test_3';
|
|
681
693
|
|
|
682
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('
|
|
683
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
684
|
-
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('
|
|
694
|
+
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
695
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
|
|
696
|
+
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
|
|
685
697
|
|
|
686
698
|
await store.persistWorkflowSnapshot({
|
|
687
699
|
namespace: testNamespace,
|
|
@@ -714,10 +726,10 @@ describe('UpstashStore', () => {
|
|
|
714
726
|
expect(page1.total).toBe(3); // Total count of all records
|
|
715
727
|
expect(page1.runs[0]!.workflowName).toBe(workflowName3);
|
|
716
728
|
expect(page1.runs[1]!.workflowName).toBe(workflowName2);
|
|
717
|
-
const firstSnapshot = page1.runs[0]!.snapshot
|
|
718
|
-
const secondSnapshot = page1.runs[1]!.snapshot
|
|
719
|
-
|
|
720
|
-
|
|
729
|
+
const firstSnapshot = page1.runs[0]!.snapshot;
|
|
730
|
+
const secondSnapshot = page1.runs[1]!.snapshot;
|
|
731
|
+
checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
|
|
732
|
+
checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
|
|
721
733
|
|
|
722
734
|
// Get second page
|
|
723
735
|
const page2 = await store.getWorkflowRuns({
|
|
@@ -728,8 +740,118 @@ describe('UpstashStore', () => {
|
|
|
728
740
|
expect(page2.runs).toHaveLength(1);
|
|
729
741
|
expect(page2.total).toBe(3);
|
|
730
742
|
expect(page2.runs[0]!.workflowName).toBe(workflowName1);
|
|
731
|
-
const snapshot = page2.runs[0]!.snapshot
|
|
732
|
-
|
|
743
|
+
const snapshot = page2.runs[0]!.snapshot;
|
|
744
|
+
checkWorkflowSnapshot(snapshot, stepId1, 'success');
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
describe('getWorkflowRunById', () => {
|
|
748
|
+
const testNamespace = 'test-workflows-id';
|
|
749
|
+
const workflowName = 'workflow-id-test';
|
|
750
|
+
let runId: string;
|
|
751
|
+
let stepId: string;
|
|
752
|
+
|
|
753
|
+
beforeAll(async () => {
|
|
754
|
+
// Insert a workflow run for positive test
|
|
755
|
+
const sample = createSampleWorkflowSnapshot('success');
|
|
756
|
+
runId = sample.runId;
|
|
757
|
+
stepId = sample.stepId;
|
|
758
|
+
await store.insert({
|
|
759
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
760
|
+
record: {
|
|
761
|
+
namespace: testNamespace,
|
|
762
|
+
workflow_name: workflowName,
|
|
763
|
+
run_id: runId,
|
|
764
|
+
resourceId: 'resource-abc',
|
|
765
|
+
snapshot: sample.snapshot,
|
|
766
|
+
createdAt: new Date(),
|
|
767
|
+
updatedAt: new Date(),
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('should retrieve a workflow run by ID', async () => {
|
|
773
|
+
const found = await store.getWorkflowRunById({
|
|
774
|
+
namespace: testNamespace,
|
|
775
|
+
runId,
|
|
776
|
+
workflowName,
|
|
777
|
+
});
|
|
778
|
+
expect(found).not.toBeNull();
|
|
779
|
+
expect(found?.runId).toBe(runId);
|
|
780
|
+
const snapshot = found?.snapshot;
|
|
781
|
+
checkWorkflowSnapshot(snapshot!, stepId, 'success');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('should return null for non-existent workflow run ID', async () => {
|
|
785
|
+
const notFound = await store.getWorkflowRunById({
|
|
786
|
+
namespace: testNamespace,
|
|
787
|
+
runId: 'non-existent-id',
|
|
788
|
+
workflowName,
|
|
789
|
+
});
|
|
790
|
+
expect(notFound).toBeNull();
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
describe('getWorkflowRuns with resourceId', () => {
|
|
794
|
+
const testNamespace = 'test-workflows-id';
|
|
795
|
+
const workflowName = 'workflow-id-test';
|
|
796
|
+
let resourceId: string;
|
|
797
|
+
let runIds: string[] = [];
|
|
798
|
+
|
|
799
|
+
beforeAll(async () => {
|
|
800
|
+
// Insert multiple workflow runs for the same resourceId
|
|
801
|
+
resourceId = 'resource-shared';
|
|
802
|
+
for (const status of ['success', 'waiting']) {
|
|
803
|
+
const sample = createSampleWorkflowSnapshot(status);
|
|
804
|
+
runIds.push(sample.runId);
|
|
805
|
+
await store.insert({
|
|
806
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
807
|
+
record: {
|
|
808
|
+
namespace: testNamespace,
|
|
809
|
+
workflow_name: workflowName,
|
|
810
|
+
run_id: sample.runId,
|
|
811
|
+
resourceId,
|
|
812
|
+
snapshot: sample.snapshot,
|
|
813
|
+
createdAt: new Date(),
|
|
814
|
+
updatedAt: new Date(),
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
// Insert a run with a different resourceId
|
|
819
|
+
const other = createSampleWorkflowSnapshot('waiting');
|
|
820
|
+
await store.insert({
|
|
821
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
822
|
+
record: {
|
|
823
|
+
namespace: testNamespace,
|
|
824
|
+
workflow_name: workflowName,
|
|
825
|
+
run_id: other.runId,
|
|
826
|
+
resourceId: 'resource-other',
|
|
827
|
+
snapshot: other.snapshot,
|
|
828
|
+
createdAt: new Date(),
|
|
829
|
+
updatedAt: new Date(),
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should retrieve all workflow runs by resourceId', async () => {
|
|
835
|
+
const { runs } = await store.getWorkflowRuns({
|
|
836
|
+
namespace: testNamespace,
|
|
837
|
+
resourceId,
|
|
838
|
+
workflowName,
|
|
839
|
+
});
|
|
840
|
+
expect(Array.isArray(runs)).toBe(true);
|
|
841
|
+
expect(runs.length).toBeGreaterThanOrEqual(2);
|
|
842
|
+
for (const run of runs) {
|
|
843
|
+
expect(run.resourceId).toBe(resourceId);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('should return an empty array if no workflow runs match resourceId', async () => {
|
|
848
|
+
const { runs } = await store.getWorkflowRuns({
|
|
849
|
+
namespace: testNamespace,
|
|
850
|
+
resourceId: 'non-existent-resource',
|
|
851
|
+
workflowName,
|
|
852
|
+
});
|
|
853
|
+
expect(Array.isArray(runs)).toBe(true);
|
|
854
|
+
expect(runs.length).toBe(0);
|
|
733
855
|
});
|
|
734
856
|
});
|
|
735
857
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector store specific prompt that details supported operators and examples.
|
|
3
|
+
* This prompt helps users construct valid filters for Upstash Vector.
|
|
4
|
+
*/
|
|
5
|
+
export const UPSTASH_PROMPT = `When querying Upstash Vector, you can ONLY use the operators listed below. Any other operators will be rejected.
|
|
6
|
+
Important: Don't explain how to construct the filter - use the specified operators and fields to search the content and return relevant results.
|
|
7
|
+
If a user tries to give an explicit operator that is not supported, reject the filter entirely and let them know that the operator is not supported.
|
|
8
|
+
|
|
9
|
+
Basic Comparison Operators:
|
|
10
|
+
- $eq: Exact match (default when using field: value)
|
|
11
|
+
Example: { "category": "electronics" }
|
|
12
|
+
- $ne: Not equal
|
|
13
|
+
Example: { "category": { "$ne": "electronics" } }
|
|
14
|
+
- $gt: Greater than
|
|
15
|
+
Example: { "price": { "$gt": 100 } }
|
|
16
|
+
- $gte: Greater than or equal
|
|
17
|
+
Example: { "price": { "$gte": 100 } }
|
|
18
|
+
- $lt: Less than
|
|
19
|
+
Example: { "price": { "$lt": 100 } }
|
|
20
|
+
- $lte: Less than or equal
|
|
21
|
+
Example: { "price": { "$lte": 100 } }
|
|
22
|
+
|
|
23
|
+
Array Operators:
|
|
24
|
+
- $in: Match any value in array
|
|
25
|
+
Example: { "category": { "$in": ["electronics", "books"] } }
|
|
26
|
+
- $nin: Does not match any value in array
|
|
27
|
+
Example: { "category": { "$nin": ["electronics", "books"] } }
|
|
28
|
+
|
|
29
|
+
Logical Operators:
|
|
30
|
+
- $and: Logical AND (can be implicit or explicit)
|
|
31
|
+
Implicit Example: { "price": { "$gt": 100 }, "category": "electronics" }
|
|
32
|
+
Explicit Example: { "$and": [{ "price": { "$gt": 100 } }, { "category": "electronics" }] }
|
|
33
|
+
- $or: Logical OR
|
|
34
|
+
Example: { "$or": [{ "price": { "$lt": 50 } }, { "category": "books" }] }
|
|
35
|
+
|
|
36
|
+
Element Operators:
|
|
37
|
+
- $exists: Check if field exists
|
|
38
|
+
Example: { "rating": { "$exists": true } }
|
|
39
|
+
|
|
40
|
+
Restrictions:
|
|
41
|
+
- Regex patterns are not supported
|
|
42
|
+
- Only $and and $or logical operators are supported at the top level
|
|
43
|
+
- Empty arrays in $in/$nin will return no results
|
|
44
|
+
- Nested fields are supported using dot notation
|
|
45
|
+
- Multiple conditions on the same field are supported with both implicit and explicit $and
|
|
46
|
+
- At least one key-value pair is required in filter object
|
|
47
|
+
- Empty objects and undefined values are treated as no filter
|
|
48
|
+
- Invalid types in comparison operators will throw errors
|
|
49
|
+
- All non-logical operators must be used within a field condition
|
|
50
|
+
Valid: { "field": { "$gt": 100 } }
|
|
51
|
+
Valid: { "$and": [...] }
|
|
52
|
+
Invalid: { "$gt": 100 }
|
|
53
|
+
- Logical operators must contain field conditions, not direct operators
|
|
54
|
+
Valid: { "$and": [{ "field": { "$gt": 100 } }] }
|
|
55
|
+
Invalid: { "$and": [{ "$gt": 100 }] }
|
|
56
|
+
- Logical operators ($and, $or):
|
|
57
|
+
- Can only be used at top level or nested within other logical operators
|
|
58
|
+
- Can not be used on a field level, or be nested inside a field
|
|
59
|
+
- Can not be used inside an operator
|
|
60
|
+
- Valid: { "$and": [{ "field": { "$gt": 100 } }] }
|
|
61
|
+
- Valid: { "$or": [{ "$and": [{ "field": { "$gt": 100 } }] }] }
|
|
62
|
+
- Invalid: { "field": { "$and": [{ "$gt": 100 }] } }
|
|
63
|
+
- Invalid: { "field": { "$or": [{ "$gt": 100 }] } }
|
|
64
|
+
- Invalid: { "field": { "$gt": { "$and": [{...}] } } }
|
|
65
|
+
|
|
66
|
+
Example Complex Query:
|
|
67
|
+
{
|
|
68
|
+
"$and": [
|
|
69
|
+
{ "category": { "$in": ["electronics", "computers"] } },
|
|
70
|
+
{ "price": { "$gte": 100, "$lte": 1000 } },
|
|
71
|
+
{ "rating": { "$exists": true, "$gt": 4 } },
|
|
72
|
+
{ "$or": [
|
|
73
|
+
{ "stock": { "$gt": 0 } },
|
|
74
|
+
{ "preorder": true }
|
|
75
|
+
]}
|
|
76
|
+
]
|
|
77
|
+
}`;
|