Add filter to drizzle converter

This commit is contained in:
Zoe Roux
2024-12-22 21:50:51 +01:00
parent c71da66bb6
commit c14d4e0911
5 changed files with 177 additions and 28 deletions
+1 -1
View File
@@ -3,6 +3,6 @@ import { t } from "elysia";
export const KError = t.Object({
status: t.Integer(),
message: t.String(),
details: t.Any(),
details: t.Optional(t.Any()),
});
export type KError = typeof KError.static;
+133
View File
@@ -0,0 +1,133 @@
import {
and,
type Column,
eq,
gt,
gte,
lt,
lte,
ne,
not,
or,
type SQL,
sql,
} from "drizzle-orm";
import { comment } from "~/utils";
import type { KError } from "../error";
import { type Expression, expression, type Operator } from "./filters";
export type Filter = {
[key: string]:
| {
column: Column;
type: "int" | "float" | "date" | "string";
isArray?: boolean;
}
| { column: Column; type: "enum"; values: string[]; isArray?: boolean };
};
export const parseFilters = (filter: string | undefined, config: Filter) => {
if (!filter) return undefined;
const ret = expression.parse(filter);
if (!ret.isOk) {
throw new Error("todo");
// return { status: 422, message: `Invalid filter: ${filter}.`, details: ret }
}
return toDrizzle(ret.value, config);
};
const opMap: Record<Operator, typeof eq> = {
eq: eq,
ne: ne,
gt: gt,
ge: gte,
lt: lt,
le: lte,
has: eq,
};
const toDrizzle = (expr: Expression, config: Filter): SQL | KError => {
switch (expr.type) {
case "op": {
const where = `${expr.property} ${expr.operator} ${expr.value}`;
const prop = config[expr.property];
if (!prop) {
return {
status: 422,
message: comment`
Invalid property: ${expr.property}.
Expected one of ${Object.keys(config).join(", ")}.
`,
details: { in: where },
};
}
if (prop.type !== expr.value.type) {
return {
status: 422,
message: comment`
Invalid value for property ${expr.property}.
Got ${expr.value.type} but expected ${prop.type}.
`,
details: { in: where },
};
}
if (
prop.type === "enum" &&
(expr.value.type === "enum" || expr.value.type === "string") &&
!prop.values.includes(expr.value.value)
) {
return {
status: 422,
message: comment`
Invalid value ${expr.value.value} for property ${expr.property}.
Expected one of ${prop.values.join(", ")} but got ${expr.value.value}.
`,
details: { in: where },
};
}
if (prop.isArray) {
if (expr.operator !== "has" && expr.operator !== "eq") {
return {
status: 422,
message: comment`
Property ${expr.property} is an array but you wanted to use the
operator ${expr.operator}. Only "has" is supported ("eq" is also aliased to "has")
`,
details: { in: where },
};
}
return sql`${expr.value.value} = any(${prop.column})`;
}
return opMap[expr.operator](prop.column, expr.value.value);
}
case "and": {
const lhs = toDrizzle(expr.lhs, config);
const rhs = toDrizzle(expr.rhs, config);
if ("status" in lhs) return lhs;
if ("status" in rhs) return rhs;
return and(lhs, rhs)!;
}
case "or": {
const lhs = toDrizzle(expr.lhs, config);
const rhs = toDrizzle(expr.rhs, config);
if ("status" in lhs) return lhs;
if ("status" in rhs) return rhs;
return or(lhs, rhs)!;
}
case "not": {
const lhs = toDrizzle(expr.expression, config);
if ("status" in lhs) return lhs;
return not(lhs);
}
default:
return exhaustiveCheck(expr);
}
};
function exhaustiveCheck(v: never): never {
return v;
}
+4 -18
View File
@@ -22,21 +22,16 @@ import {
between,
recover,
} from "parjs/combinators";
import type { KError } from "../error";
export type Filter = {
[key: string]: any;
};
type Property = string;
type Value =
export type Property = string;
export type Value =
| { type: "int"; value: number }
| { type: "float"; value: number }
| { type: "date"; value: string }
| { 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];
const operators = ["eq", "ne", "gt", "ge", "lt", "le", "has"] as const;
export type Operator = (typeof operators)[number];
export type Expression =
| { type: "op"; operator: Operator; property: Property; value: Value }
| { type: "and"; lhs: Expression; rhs: Expression }
@@ -133,12 +128,3 @@ const not = t(string("not")).pipe(
const brackets = expression.pipe(between("(", ")"));
expr.init(not.pipe(or(brackets, operation)));
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 };
};