diff --git a/api/src/models/utils/filters.ts b/api/src/models/utils/filters.ts index 63ac4252..7e468d81 100644 --- a/api/src/models/utils/filters.ts +++ b/api/src/models/utils/filters.ts @@ -1,4 +1,13 @@ -import { digit, float, int, noCharOf, type Parjser, string } from "parjs"; +import { + anyStringOf, + digit, + float, + int, + letter, + noCharOf, + type Parjser, + string, +} from "parjs"; import { exactly, many, @@ -8,7 +17,11 @@ import { stringify, then, thenq, + qthen, + later, + between, } from "parjs/combinators"; +import type { KError } from "../error"; export type Filter = { [key: string]: any; @@ -19,12 +32,14 @@ type Value = | { type: "int"; value: number } | { type: "float"; value: number } | { type: "date"; value: string } - | { type: "string"; value: string }; -type Operator = "eq" | "ne" | "gt" | "ge" | "lt" | "le" | "has" | "in"; -type Expression = + | { type: "string"; value: string } + | { type: "enum"; value: string }; +const operators = ["eq", "ne", "gt", "ge", "lt", "le", "has", "in"] as const; +type Operator = (typeof operators)[number]; +export type Expression = | { type: "op"; operator: Operator; property: Property; value: Value } - | { type: "and"; first: Expression; second: Expression } - | { type: "or"; first: Expression; second: Expression } + | { type: "and"; lhs: Expression; rhs: Expression } + | { type: "or"; lhs: Expression; rhs: Expression } | { type: "not"; expression: Expression }; function t(parser: Parjser): Parjser { @@ -32,6 +47,8 @@ function t(parser: Parjser): Parjser { } const str = t(noCharOf(" ")).pipe(many1(), stringify()).expects("a string"); +const enumP = t(letter()).pipe(many1(), stringify()).expects("an enum value"); + const property = str.expects("a property"); const intVal = t(int().pipe(map((i) => ({ type: "int" as const, value: i })))); @@ -52,11 +69,52 @@ const dateVal = t( })), ), ); -const strVal = t(str.pipe(map((s) => ({ type: "string" as const, value: s })))); +const strVal = str.pipe(map((s) => ({ type: "string" as const, value: s }))); +const enumVal = enumP.pipe(map((e) => ({ type: "enum" as const, value: e }))); const value = intVal - .pipe(or(floatVal, dateVal, strVal)) + .pipe(or(floatVal, dateVal, strVal, enumVal)) .expects("a valid value"); -const operator = null; +const operator = t(anyStringOf(...operators)).expects("an operator"); -export const parseFilter = (filter: string, config: Filter) => {}; +const operation = property + .pipe( + then(operator, value), + map(([property, operator, value]) => ({ + type: "op" as const, + property, + operator, + value, + })), + ) + .expects("an operation"); + +export const expression = later(); + +const not = t(string("not")).pipe( + qthen(expression), + map((expression) => ({ type: "not" as const, expression })), +); + +const andor = operation.pipe( + then(anyStringOf("and", "or").pipe(then(expression), many())), + map(([first, expr]) => + expr.reduce( + (lhs, [op, rhs]) => ({ type: op, lhs, rhs }), + first, + ), + ), +); + +expression.init( + not.pipe(or(operation, expression.pipe(or(andor), between("(", ")")))), +); + +export const parseFilter = ( + filter: string, + config: Filter, +): Expression | KError => { + const ret = expression.parse(filter); + if (ret.isOk) return ret.value; + return { status: 422, message: `Invalid filter: ${filter}.`, details: ret }; +};