Files
genieacs/lib/db/util.ts
T
Zaid Humaimidi 6f3ce6f7c8 refactor: replace array-based expressions with typed class hierarchy
Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
2026-03-12 14:58:20 -07:00

184 lines
5.8 KiB
TypeScript

import Expression, { Value } from "../common/expression.ts";
import Path from "../common/path.ts";
import { encodeTag } from "../util.ts";
// Optimize projection by removing overlaps
// This can modify the object
export function optimizeProjection(obj: { [path: string]: 1 }): {
[path: string]: 1;
} {
if (obj[""]) return { "": obj[""] };
const keys = Object.keys(obj).sort();
if (keys.length <= 1) return obj;
for (let i = 1; i < keys.length; ++i) {
const a = keys[i - 1];
const b = keys[i];
if (b.startsWith(a)) {
if (b.charAt(a.length) === "." || b.charAt(a.length - 1) === ".") {
delete obj[b];
keys.splice(i--, 1);
}
}
}
return obj;
}
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
function recursive(_query): Expression {
let res: Expression = new Expression.Literal(true);
for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") {
if (k === "$and") {
for (const vv of Object.values(v))
res = Expression.and(res, recursive(vv));
} else if (k === "$or") {
let or: Expression = new Expression.Literal(false);
for (const vv of Object.values(v))
or = Expression.or(or, recursive(vv));
res = Expression.and(res, or);
} else {
throw new Error(`Operator ${k} not supported`);
}
} else if (k === "_tags") {
if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`);
for (const [op, val] of Object.entries(v)) {
if (op === "$ne") {
if (typeof v["$ne"] !== "string")
throw new Error("Only string values are allowed for _tags");
res = Expression.and(
res,
new Expression.Unary(
"IS NULL",
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(val)}`),
),
),
);
} else if (op === "$eq") {
if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags");
res = Expression.and(
res,
new Expression.Unary(
"IS NOT NULL",
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(val)}`),
),
),
);
} else {
throw new Error(`Invalid tag query`);
}
}
} else {
res = Expression.and(
res,
new Expression.Unary(
"IS NOT NULL",
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(v as string)}`),
),
),
);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
if (typeof v === "boolean") exists = v;
else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"];
else if (v.hasOwnProperty("$ne")) exists = !v["$ne"];
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`);
res = Expression.and(
res,
new Expression.Unary(
exists ? "IS NOT NULL" : "IS NULL",
new Expression.Parameter(Path.parse(k)),
),
);
} else if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`);
for (const [kk, vv] of Object.entries(v)) {
if (kk === "$eq") {
res = Expression.and(
res,
new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$ne") {
const p = new Expression.Parameter(Path.parse(k));
res = Expression.and(
res,
Expression.or(
new Expression.Binary("<>", p, new Expression.Literal(vv)),
new Expression.Unary("IS NULL", p),
),
);
} else if (kk === "$lt") {
res = Expression.and(
res,
new Expression.Binary(
"<",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$lte") {
res = Expression.and(
res,
new Expression.Binary(
"<=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$gt") {
res = Expression.and(
res,
new Expression.Binary(
">",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$gte") {
res = Expression.and(
res,
new Expression.Binary(
">=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else {
throw new Error(`Operator ${kk} not supported`);
}
if (!["string", "number", "boolean"].includes(typeof vv))
throw new Error(`Invalid value for ${kk} operator`);
}
} else {
res = Expression.and(
res,
new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(v as Value),
),
);
}
}
return res;
}
// empty filter
if (!Object.keys(q).length) return new Expression.Literal(true);
return recursive(q);
}