mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
import type { Filter, FilterNode, FilterOp, FilterType } from "~/types";
|
|
|
|
export interface LeafForm {
|
|
type: FilterType;
|
|
path: string;
|
|
op: FilterOp;
|
|
values: string[];
|
|
exclude: boolean;
|
|
}
|
|
|
|
export type NodeKind = "leaf" | "all" | "any" | "not";
|
|
|
|
export interface NodeForm {
|
|
kind: NodeKind;
|
|
leaf: LeafForm;
|
|
children: NodeForm[];
|
|
child: NodeForm | null;
|
|
}
|
|
|
|
export function blankLeafForm(): LeafForm {
|
|
return { type: "event", path: "", op: "eq", values: [], exclude: false };
|
|
}
|
|
|
|
export function blankNode(kind: NodeKind): NodeForm {
|
|
return {
|
|
kind,
|
|
leaf: blankLeafForm(),
|
|
children: kind === "all" || kind === "any" ? [blankNode("leaf")] : [],
|
|
child: kind === "not" ? blankNode("leaf") : null,
|
|
};
|
|
}
|
|
|
|
function matchToValues(match: string | string[] | undefined): string[] {
|
|
if (match === undefined || match === null) return [];
|
|
return Array.isArray(match) ? [...match] : [match];
|
|
}
|
|
|
|
function leafToForm(f: Filter): LeafForm {
|
|
return {
|
|
type: f.type,
|
|
path: f.path ?? "",
|
|
op: f.op ?? "eq",
|
|
values: matchToValues(f.match),
|
|
exclude: !!f.exclude,
|
|
};
|
|
}
|
|
|
|
export function nodeToForm(node: FilterNode): NodeForm {
|
|
if ("all" in node) {
|
|
return { kind: "all", leaf: blankLeafForm(), children: node.all.map(nodeToForm), child: null };
|
|
}
|
|
if ("any" in node) {
|
|
return { kind: "any", leaf: blankLeafForm(), children: node.any.map(nodeToForm), child: null };
|
|
}
|
|
if ("not" in node) {
|
|
return { kind: "not", leaf: blankLeafForm(), children: [], child: nodeToForm(node.not) };
|
|
}
|
|
return { kind: "leaf", leaf: leafToForm(node), children: [], child: null };
|
|
}
|
|
|
|
export function formToLeaf(lf: LeafForm): Filter | null {
|
|
const values = lf.values.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
if (lf.op !== "exists" && values.length === 0) return null;
|
|
if (lf.type === "field" && lf.path.trim().length === 0) return null;
|
|
const filter: Filter = { type: lf.type };
|
|
if (lf.type === "field") filter.path = lf.path.trim();
|
|
if (lf.op !== "eq") filter.op = lf.op;
|
|
if (lf.op !== "exists") filter.match = values.length === 1 ? values[0] : values;
|
|
if (lf.exclude) filter.exclude = true;
|
|
return filter;
|
|
}
|
|
|
|
export function formToNode(nf: NodeForm): FilterNode | null {
|
|
if (nf.kind === "leaf") return formToLeaf(nf.leaf);
|
|
if (nf.kind === "all" || nf.kind === "any") {
|
|
const children = nf.children.map(formToNode).filter((c): c is FilterNode => c !== null);
|
|
if (children.length === 0) return null;
|
|
return nf.kind === "all" ? { all: children } : { any: children };
|
|
}
|
|
if (nf.kind === "not") {
|
|
const child = nf.child ? formToNode(nf.child) : null;
|
|
if (!child) return null;
|
|
return { not: child };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function isTrivialNode(nf: NodeForm): boolean {
|
|
if (nf.kind === "leaf") {
|
|
return nf.leaf.type !== "field" && nf.leaf.op === "eq" && !nf.leaf.exclude;
|
|
}
|
|
if (nf.kind === "all") return nf.children.every(isTrivialNode);
|
|
return false;
|
|
}
|
|
|
|
export function flattenLeaves(nf: NodeForm): Filter[] {
|
|
const out: Filter[] = [];
|
|
const walk = (n: NodeForm): void => {
|
|
if (n.kind === "leaf") {
|
|
const f = formToLeaf(n.leaf);
|
|
if (f) out.push(f);
|
|
return;
|
|
}
|
|
if (n.kind === "all" || n.kind === "any") n.children.forEach(walk);
|
|
else if (n.kind === "not" && n.child) walk(n.child);
|
|
};
|
|
walk(nf);
|
|
return out;
|
|
}
|
|
|
|
export function nodeFormToRouteFilters(nf: NodeForm): { filters: Filter[]; ast?: FilterNode } {
|
|
if (isTrivialNode(nf)) {
|
|
return { filters: flattenLeaves(nf) };
|
|
}
|
|
const ast = formToNode(nf);
|
|
return { filters: [], ast: ast ?? undefined };
|
|
}
|
|
|
|
export function collectEvents(node: FilterNode): string[] {
|
|
const out: string[] = [];
|
|
const walk = (n: FilterNode): void => {
|
|
if ("all" in n) {
|
|
n.all.forEach(walk);
|
|
return;
|
|
}
|
|
if ("any" in n) {
|
|
n.any.forEach(walk);
|
|
return;
|
|
}
|
|
if ("not" in n) {
|
|
walk(n.not);
|
|
return;
|
|
}
|
|
if (n.type === "event") {
|
|
const m = n.match;
|
|
if (Array.isArray(m)) out.push(...m);
|
|
else if (typeof m === "string" && m) out.push(m);
|
|
}
|
|
};
|
|
walk(node);
|
|
return out;
|
|
}
|
|
|
|
export function describeLeaf(f: Filter, t: (key: string) => string): string {
|
|
const label = f.type === "field" ? (f.path ?? f.type) : t("filter." + f.type);
|
|
const op = f.op ?? "eq";
|
|
const match = f.match;
|
|
const values = match === undefined ? [] : Array.isArray(match) ? match : [match];
|
|
const value = values.map((v) => JSON.stringify(v)).join(` ${t("filterNode.or")} `) || "\u2205";
|
|
let base: string;
|
|
if (f.type === "keyword") {
|
|
base = `${label} ${t("filterNode.matches")} ${value}`;
|
|
} else if (op === "exists") {
|
|
base = `${label} ${t("filterOp.exists")}`;
|
|
} else if (op === "eq") {
|
|
base = `${label} ${t("filterNode.is")} ${value}`;
|
|
} else {
|
|
base = `${label} ${t("filterOp." + op)} ${value}`;
|
|
}
|
|
return f.exclude ? `${t("filterNode.not")} (${base})` : base;
|
|
}
|
|
|
|
export function describeNode(node: FilterNode, t: (key: string) => string): string {
|
|
if ("all" in node) {
|
|
return node.all
|
|
.map((c) => describeNode(c, t))
|
|
.filter((s) => s.length)
|
|
.join(` ${t("filterNode.and")} `);
|
|
}
|
|
if ("any" in node) {
|
|
const parts = node.any.map((c) => describeNode(c, t)).filter((s) => s.length);
|
|
return parts.length ? `(${parts.join(` ${t("filterNode.or")} `)})` : "";
|
|
}
|
|
if ("not" in node) {
|
|
return `${t("filterNode.not")} (${describeNode(node.not, t)})`;
|
|
}
|
|
return describeLeaf(node, t);
|
|
}
|