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..4d89db7f0 100644 --- a/packages/validation/schemas.ts +++ b/packages/validation/schemas.ts @@ -8,6 +8,71 @@ 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.union([z.literal("="), z.literal("!="), z.literal("<"), z.literal("<="), z.literal(">"), z.literal(">=")]) + +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([ + FilterExpressionSchema, + z.object({ AND: z.array(FilterExpressionSchema).min(1) }), + z.object({ OR: z.array(FilterExpressionSchema).min(1) }), +]) + + export const DocumentTypeEnum = z.enum([ "text", "pdf",