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

+6 -9
View File
@@ -2,13 +2,13 @@ import test from "node:test";
import assert from "node:assert";
import { EJSON } from "bson";
import { Filter } from "mongodb";
import { stringify, parse } from "../lib/common/expression/parser.ts";
import Expression from "../lib/common/expression.ts";
import { convertOldPrecondition } from "../lib/db/util.ts";
import { toMongoQuery } from "../lib/db/synth.ts";
void test("convertOldPrecondition", () => {
const tests = [
[{}, "true"],
[{}, "TRUE"],
[{ test: "test" }, 'test = "test"'],
[{ test: { $eq: "test" } }, 'test = "test"'],
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
@@ -31,7 +31,7 @@ void test("convertOldPrecondition", () => {
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
[
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
'test = "test" OR (test <> "test" OR test IS NULL)',
'test = "test" OR test <> "test" OR test IS NULL',
],
[
{ test: { $gte: "test1", $ne: "test2" } },
@@ -54,7 +54,7 @@ void test("convertOldPrecondition", () => {
for (const t of tests) {
assert.strictEqual(
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)),
convertOldPrecondition(t[0] as Record<string, unknown>).toString(),
t[1],
);
}
@@ -86,7 +86,6 @@ void test("toMongoQuery", async () => {
"Param1 <> 1657844103524",
{
"Param1._value": { $ne: 1657844103524 },
$and: [
{
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
@@ -114,7 +113,6 @@ void test("toMongoQuery", async () => {
},
},
],
[
"LOWER(Param1) LIKE 'value'",
{
@@ -146,7 +144,7 @@ void test("toMongoQuery", async () => {
];
for (const [expStr, expect] of queries) {
const exp = parse(expStr);
const exp = Expression.parse(expStr);
let query = toMongoQuery(exp, "devices");
if (query) query = EJSON.serialize(query);
assert.deepStrictEqual(query, expect);
@@ -156,11 +154,10 @@ void test("toMongoQuery", async () => {
["Param1 = Param2", "Right-hand operand must be a literal value"],
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
["NOW() = 1", "Left-hand operand must be a parameter"],
["param{param2} = 1", "Left-hand operand must be a parameter"],
];
for (const [expStr, err] of failQueries) {
const exp = parse(expStr);
const exp = Expression.parse(expStr);
assert.throws(() => toMongoQuery(exp, "devices"), {
message: err,
});