From 72e6177580e7a9020ad273c1a2e4570b73915e61 Mon Sep 17 00:00:00 2001 From: Anirudh Parmar Date: Sun, 16 Aug 2026 19:04:59 +0530 Subject: [PATCH 1/2] fix: improve filter schema --- packages/lib/api.ts | 2 ++ packages/validation/api.ts | 18 +++++----- packages/validation/schemas.ts | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/lib/api.ts b/packages/lib/api.ts index 7cf8755f9..6833504c3 100644 --- a/packages/lib/api.ts +++ b/packages/lib/api.ts @@ -28,6 +28,7 @@ import { SettingsRequestSchema, UpdateContainerTagSettingsRequestSchema, } from "../validation/api" +import { SearchFiltersSchema } from "../validation/schemas" const UpdateSettingsResponseSchema = z.object({ orgId: z.string(), @@ -252,6 +253,7 @@ export const apiSchema = createSchema({ page: z.number().optional(), status: z.string().optional(), containerTags: z.array(z.string()).optional(), + filters: SearchFiltersSchema.optional(), }) .optional(), output: ListMemoriesResponseSchema, diff --git a/packages/validation/api.ts b/packages/validation/api.ts index f066bfcd4..ae46f6dd4 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -6,17 +6,11 @@ import { MemoryEntrySchema, OrganizationSettingsSchema, RequestTypeEnum, + SearchFiltersSchema, } from "./schemas" export const MetadataSchema = BaseMetadataSchema -export const SearchFiltersSchema = z - .object({ - AND: z.array(z.unknown()).optional(), - OR: z.array(z.unknown()).optional(), - }) - .or(z.record(z.unknown())) - const exampleMetadata: Record = { category: "technology", isPublic: true, @@ -247,10 +241,18 @@ export const ListMemoriesQuerySchema = z "Optional tags this memory should be containerized by. This can be an ID for your user, a project ID, or any other identifier you wish to use to group memories.", example: ["user_123", "project_123"], }), - // TODO: Improve filter schema filters: z .string() .optional() + .transform((raw,ctx) => { + if (!raw) return undefined + try { + return SearchFiltersSchema.parse(JSON.parse(raw)) + } catch { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid filters JSON" }) + return z.NEVER + } + }) .openapi({ description: "Optional filters to apply to the search", example: JSON.stringify({ diff --git a/packages/validation/schemas.ts b/packages/validation/schemas.ts index b9c41c5f1..e6462bc7e 100644 --- a/packages/validation/schemas.ts +++ b/packages/validation/schemas.ts @@ -8,6 +8,70 @@ export type Metadata = z.infer export const VisibilityEnum = z.enum(["public", "private", "unlisted"]) export type Visibility = z.infer +const MetadataKeySchema = z + .string() + .min(1) + .max(64) + .regex(/^[a-zA-Z0-9_.-]+$/, "Invalid metadata key") + +const MetadataValueSchema = z.union([z.string(), z.number(), z.boolean()]) + +const StringEqualityFilterSchema = z.object({ + key: MetadataKeySchema, + value: MetadataValueSchema, + negate: z.boolean().optional(), +}) + +const StringContainsFilterSchema = z.object({ + filterType: z.literal("string_contains"), + key: MetadataKeySchema, + value: z.string(), + negate: z.boolean().optional(), + ignoreCase: z.boolean().optional(), +}) + +const NumericOperatorSchema = z.enum(["=", "!=", "<", "<=", ">", ">="]) + +const NumericFilterSchema = z.object({ + filterType: z.literal("numeric"), + key: MetadataKeySchema, + value: z.string(), + numericOperator: NumericOperatorSchema, + negate: z.boolean().optional(), +}) + +const ArrayContainsFilterSchema = z.object({ + filterType: z.literal("array_contains"), + key: MetadataKeySchema, + value: z.string(), + negate: z.boolean().optional(), +}) + +const FilterConditionSchema = z.discriminatedUnion("filterType", [ + StringContainsFilterSchema, + NumericFilterSchema, + ArrayContainsFilterSchema, +]).or(StringEqualityFilterSchema) + +type FilterExpression = + | z.infer + | { AND: FilterExpression[] } + | { OR: FilterExpression[] } + +export const FilterExpressionSchema: z.ZodType = z.lazy(() => + z.union([ + FilterConditionSchema, + z.object({ AND: z.array(FilterExpressionSchema).min(1) }), + z.object({ OR: z.array(FilterExpressionSchema).min(1) }), + ]) +) + +export const SearchFiltersSchema = z.union([ + z.object({ AND: z.array(FilterExpressionSchema).min(1) }), + z.object({ OR: z.array(FilterExpressionSchema).min(1) }), +]) + + export const DocumentTypeEnum = z.enum([ "text", "pdf", From 60f373a7a9211714d0cdb2a61bb1858a8a07392f Mon Sep 17 00:00:00 2001 From: Anirudh Parmar Date: Tue, 18 Aug 2026 20:20:22 +0530 Subject: [PATCH 2/2] fix: improve search filter schema --- packages/validation/schemas.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/validation/schemas.ts b/packages/validation/schemas.ts index e6462bc7e..4d89db7f0 100644 --- a/packages/validation/schemas.ts +++ b/packages/validation/schemas.ts @@ -30,7 +30,7 @@ const StringContainsFilterSchema = z.object({ ignoreCase: z.boolean().optional(), }) -const NumericOperatorSchema = z.enum(["=", "!=", "<", "<=", ">", ">="]) +const NumericOperatorSchema = z.union([z.literal("="), z.literal("!="), z.literal("<"), z.literal("<="), z.literal(">"), z.literal(">=")]) const NumericFilterSchema = z.object({ filterType: z.literal("numeric"), @@ -67,6 +67,7 @@ export const FilterExpressionSchema: z.ZodType = z.lazy(() => ) export const SearchFiltersSchema = z.union([ + FilterExpressionSchema, z.object({ AND: z.array(FilterExpressionSchema).min(1) }), z.object({ OR: z.array(FilterExpressionSchema).min(1) }), ])