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
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+101 -41
View File
@@ -1,4 +1,5 @@
import { Expression } from "../types.ts";
import Expression, { Value } from "../common/expression.ts";
import Path from "../common/path.ts";
import { encodeTag } from "../util.ts";
// Optimize projection by removing overlaps
@@ -26,44 +27,62 @@ export function optimizeProjection(obj: { [path: string]: 1 }): {
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
function recursive(_query): Expression {
const expressions: Expression[] = [];
let res: Expression = new Expression.Literal(true);
for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") {
if (k === "$and") {
const and: Expression = ["AND"];
for (const vv of Object.values(v)) and.push(recursive(vv));
expressions.push(and);
for (const vv of Object.values(v))
res = Expression.and(res, recursive(vv));
} else if (k === "$or") {
const or: Expression = ["OR"];
for (const vv of Object.values(v)) or.push(recursive(vv));
expressions.push(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`);
const conjs: Expression[] = [];
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");
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
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");
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
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`);
}
}
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else {
expressions.push([
"IS NOT NULL",
["PARAM", `Tags.${encodeTag(v as string)}`],
]);
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;
@@ -73,51 +92,92 @@ export function convertOldPrecondition(q: Record<string, unknown>): Expression {
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`);
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
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`);
const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) {
if (kk === "$eq") {
exps.push(["=", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$ne") {
exps.push([
"OR",
["<>", ["PARAM", k], vv],
["IS NULL", ["PARAM", k]],
]);
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") {
exps.push(["<", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
"<",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$lte") {
exps.push(["<=", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
"<=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$gt") {
exps.push([">", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
">",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$gte") {
exps.push([">=", ["PARAM", k], vv]);
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`);
}
if (exps.length === 1) {
expressions.push(exps[0]);
} else if (exps.length > 1) {
const and: Expression = ["AND"];
expressions.push(and.concat(exps));
}
} else {
expressions.push(["=", ["PARAM", k], v]);
res = Expression.and(
res,
new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(v as Value),
),
);
}
}
if (expressions.length === 1) return expressions[0];
if (expressions.length === 0) return true;
return ["AND", ...expressions];
return res;
}
// empty filter
if (!Object.keys(q).length) return true;
if (!Object.keys(q).length) return new Expression.Literal(true);
return recursive(q);
}