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:
1 parent
75908e624f
commit
6f3ce6f7c8
71 files changed
+4045
-3449
No files matched your search
+21
-26
@@ -7,7 +7,6 @@ import {
|
||||
Task,
|
||||
Operation,
|
||||
} from "../types.ts";
|
||||
import Path from "../common/path.ts";
|
||||
import { collections } from "../db/db.ts";
|
||||
import { optimizeProjection } from "../db/util.ts";
|
||||
import * as MongoTypes from "../db/types.ts";
|
||||
@@ -23,21 +22,17 @@ function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||
export async function fetchDevice(
|
||||
id: string,
|
||||
timestamp: number,
|
||||
): Promise<[Path, number, Attributes?][]> {
|
||||
const res: [Path, number, Attributes?][] = [
|
||||
): Promise<[string, number, Attributes?][]> {
|
||||
const res: [string, number, Attributes?][] = [
|
||||
["Events", timestamp, { object: [timestamp, 1], writable: [timestamp, 0] }],
|
||||
[
|
||||
Path.parse("Events"),
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||
],
|
||||
[
|
||||
Path.parse("DeviceID"),
|
||||
"DeviceID",
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||
],
|
||||
];
|
||||
|
||||
const device = await collections.devices.findOne({ _id: id });
|
||||
const device = (await collections.devices.findOne({ _id: id })) as any;
|
||||
if (!device) return null;
|
||||
|
||||
function storeParams(
|
||||
@@ -77,7 +72,7 @@ export async function fetchDevice(
|
||||
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||
|
||||
try {
|
||||
res.push([Path.parse(path), t, attrs]);
|
||||
res.push([path, t, attrs]);
|
||||
} catch {
|
||||
// The path parser is now more strict so we might be in a situation where
|
||||
// the database contains invalid paths from before this change So here we
|
||||
@@ -86,7 +81,7 @@ export async function fetchDevice(
|
||||
splits[splits.length - 1] =
|
||||
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
|
||||
path = splits.join(".");
|
||||
res.push([Path.parse(path), t, attrs]);
|
||||
res.push([path, t, attrs]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,17 +93,17 @@ export async function fetchDevice(
|
||||
}
|
||||
|
||||
if (obj["_object"] && obj["_timestamp"])
|
||||
res.push([Path.parse(path + ".*"), obj["_timestamp"]]);
|
||||
res.push([path + ".*", obj["_timestamp"]]);
|
||||
}
|
||||
|
||||
const ts: number = +device["_timestamp"] || 0;
|
||||
if (ts) res.push([Path.parse("*"), ts]);
|
||||
if (ts) res.push(["*", ts]);
|
||||
|
||||
for (const [k, v] of Object.entries(device)) {
|
||||
switch (k) {
|
||||
case "_lastInform":
|
||||
res.push([
|
||||
Path.parse("Events.Inform"),
|
||||
"Events.Inform",
|
||||
+v,
|
||||
{
|
||||
object: [+v, 0],
|
||||
@@ -119,7 +114,7 @@ export async function fetchDevice(
|
||||
break;
|
||||
case "_lastBoot":
|
||||
res.push([
|
||||
Path.parse("Events.1_BOOT"),
|
||||
"Events.1_BOOT",
|
||||
+v,
|
||||
{
|
||||
object: [+v, 0],
|
||||
@@ -130,7 +125,7 @@ export async function fetchDevice(
|
||||
break;
|
||||
case "_lastBootstrap":
|
||||
res.push([
|
||||
Path.parse("Events.0_BOOTSTRAP"),
|
||||
"Events.0_BOOTSTRAP",
|
||||
+v,
|
||||
{
|
||||
object: [+v, 0],
|
||||
@@ -142,7 +137,7 @@ export async function fetchDevice(
|
||||
case "_registered":
|
||||
// Use current timestamp for registered event attribute timestamps
|
||||
res.push([
|
||||
Path.parse("Events.Registered"),
|
||||
"Events.Registered",
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -153,7 +148,7 @@ export async function fetchDevice(
|
||||
break;
|
||||
case "_id":
|
||||
res.push([
|
||||
Path.parse("DeviceID.ID"),
|
||||
"DeviceID.ID",
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -165,7 +160,7 @@ export async function fetchDevice(
|
||||
case "_tags":
|
||||
if ((v as string[]).length) {
|
||||
res.push([
|
||||
Path.parse("Tags"),
|
||||
"Tags",
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||
]);
|
||||
@@ -173,7 +168,7 @@ export async function fetchDevice(
|
||||
|
||||
for (const t of v as string[]) {
|
||||
res.push([
|
||||
Path.parse("Tags." + encodeTag(t)),
|
||||
"Tags." + encodeTag(t),
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -186,7 +181,7 @@ export async function fetchDevice(
|
||||
case "_deviceId":
|
||||
if (v["_Manufacturer"] != null) {
|
||||
res.push([
|
||||
Path.parse("DeviceID.Manufacturer"),
|
||||
"DeviceID.Manufacturer",
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -198,7 +193,7 @@ export async function fetchDevice(
|
||||
|
||||
if (v["_OUI"] != null) {
|
||||
res.push([
|
||||
Path.parse("DeviceID.OUI"),
|
||||
"DeviceID.OUI",
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -210,7 +205,7 @@ export async function fetchDevice(
|
||||
|
||||
if (v["_ProductClass"] != null) {
|
||||
res.push([
|
||||
Path.parse("DeviceID.ProductClass"),
|
||||
"DeviceID.ProductClass",
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -222,7 +217,7 @@ export async function fetchDevice(
|
||||
|
||||
if (v["_SerialNumber"] != null) {
|
||||
res.push([
|
||||
Path.parse("DeviceID.SerialNumber"),
|
||||
"DeviceID.SerialNumber",
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
@@ -257,7 +252,7 @@ export async function saveDevice(
|
||||
)
|
||||
continue;
|
||||
|
||||
const parent = deviceData.paths.get(diff[0].slice(0, -1));
|
||||
const parent = deviceData.paths.get(diff[0].slice(0, -1).toString());
|
||||
|
||||
// Param timestamps may be greater than session timestamp to track revisions
|
||||
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
|
||||
|
||||
+72
-56
@@ -1,15 +1,12 @@
|
||||
import * as vm from "node:vm";
|
||||
import * as crypto from "node:crypto";
|
||||
import * as config from "../config.ts";
|
||||
import { collections } from "../db/db.ts";
|
||||
import { convertOldPrecondition } from "../db/util.ts";
|
||||
import * as logger from "../logger.ts";
|
||||
import * as scheduling from "../scheduling.ts";
|
||||
import * as expression from "../common/expression/util.ts";
|
||||
import { parse } from "../common/expression/parser.ts";
|
||||
import Expression, { Value } from "../common/expression.ts";
|
||||
import {
|
||||
Preset,
|
||||
Expression,
|
||||
Provisions,
|
||||
VirtualParameters,
|
||||
Files,
|
||||
@@ -93,10 +90,10 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
||||
}
|
||||
|
||||
const events = preset["events"] || {};
|
||||
let precondition = true as Expression;
|
||||
let precondition: Expression = new Expression.Literal(true);
|
||||
if (preset["precondition"]) {
|
||||
try {
|
||||
precondition = parse(preset["precondition"]);
|
||||
precondition = Expression.parse(preset["precondition"]);
|
||||
} catch {
|
||||
precondition = convertOldPrecondition(
|
||||
JSON.parse(preset["precondition"]),
|
||||
@@ -104,7 +101,7 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
||||
}
|
||||
|
||||
// Simplify expression
|
||||
precondition = expression.evaluate(precondition);
|
||||
precondition = precondition.evaluate((e) => e);
|
||||
}
|
||||
|
||||
const _provisions: Preset["provisions"] = [];
|
||||
@@ -113,23 +110,42 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
||||
for (const c of preset.configurations) {
|
||||
switch (c.type) {
|
||||
case "age":
|
||||
_provisions.push(["refresh", c.name, +c.age]);
|
||||
_provisions.push([
|
||||
"refresh",
|
||||
new Expression.Literal(c.name),
|
||||
new Expression.Literal(+c.age),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "value":
|
||||
_provisions.push(["value", c.name, c.value]);
|
||||
_provisions.push([
|
||||
"value",
|
||||
new Expression.Literal(c.name),
|
||||
new Expression.Literal(c.value),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "add_tag":
|
||||
_provisions.push(["tag", c.tag, true]);
|
||||
_provisions.push([
|
||||
"tag",
|
||||
new Expression.Literal(c.tag),
|
||||
new Expression.Literal(true),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "delete_tag":
|
||||
_provisions.push(["tag", c.tag, false]);
|
||||
_provisions.push([
|
||||
"tag",
|
||||
new Expression.Literal(c.tag),
|
||||
new Expression.Literal(false),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "provision":
|
||||
_provisions.push([c.name, ...(c.args || [])]);
|
||||
_provisions.push([
|
||||
c.name,
|
||||
...(c.args || []).map((a) => Expression.parse(a)),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "add_object":
|
||||
@@ -139,11 +155,19 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
||||
.join(",");
|
||||
const p = `${c.name}.[${alias}]`;
|
||||
_provisions.push(["instances", p, 1]);
|
||||
_provisions.push([
|
||||
"instances",
|
||||
new Expression.Literal(p),
|
||||
new Expression.Literal(1),
|
||||
]);
|
||||
|
||||
for (const k in obj) {
|
||||
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
|
||||
_provisions.push(["value", `${p}.${k}`, obj[k]]);
|
||||
_provisions.push([
|
||||
"value",
|
||||
new Expression.Literal(`${p}.${k}`),
|
||||
new Expression.Literal(obj[k]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,7 +181,11 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
||||
.join(",");
|
||||
const p = `${c.name}.[${alias}]`;
|
||||
_provisions.push(["instances", p, 0]);
|
||||
_provisions.push([
|
||||
"instances",
|
||||
new Expression.Literal(p),
|
||||
new Expression.Literal(0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +279,7 @@ async function fetchConfig(): Promise<[string, Config]> {
|
||||
|
||||
for (const c of conf) {
|
||||
// Evaluate expressions to simplify them
|
||||
_config[c._id] = expression.evaluate(parse(c.value));
|
||||
_config[c._id] = Expression.parse(c.value).evaluate((e) => e);
|
||||
}
|
||||
|
||||
return [h, _config];
|
||||
@@ -303,49 +331,37 @@ export function getFiles(revision: string): Files {
|
||||
}
|
||||
|
||||
export function getConfig(
|
||||
snapshotKey: string,
|
||||
revision: string,
|
||||
key: string,
|
||||
context: Record<string, unknown>,
|
||||
now: number,
|
||||
cb?: (e: Expression) => Expression,
|
||||
): string | number | boolean | null {
|
||||
const snapshot = localCache.get(snapshotKey);
|
||||
dflt: string,
|
||||
fn: (e: Expression) => Expression.Literal,
|
||||
): string;
|
||||
export function getConfig(
|
||||
revision: string,
|
||||
key: string,
|
||||
dflt: number,
|
||||
fn: (e: Expression) => Expression.Literal,
|
||||
): number;
|
||||
export function getConfig(
|
||||
revision: string,
|
||||
key: string,
|
||||
dflt: boolean,
|
||||
fn: (e: Expression) => Expression.Literal,
|
||||
): boolean;
|
||||
export function getConfig(
|
||||
revision: string,
|
||||
key: string,
|
||||
dflt: Value,
|
||||
fn: (e: Expression) => Expression.Literal,
|
||||
): Value {
|
||||
const snapshot = localCache.get(revision);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
|
||||
const oldOpts = {
|
||||
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
|
||||
"cwmp.debug": "DEBUG",
|
||||
"cwmp.retryDelay": "RETRY_DELAY",
|
||||
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
|
||||
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
|
||||
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
|
||||
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
|
||||
"cwmp.cookiesPath": "COOKIES_PATH",
|
||||
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
|
||||
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
|
||||
"cwmp.connectionRequestAllowBasicAuth":
|
||||
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
||||
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
||||
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
|
||||
};
|
||||
|
||||
if (!(key in snapshot.config)) {
|
||||
if (key in oldOpts) {
|
||||
let id;
|
||||
if (context?.["id"]) {
|
||||
id = context["id"];
|
||||
} else if (cb) {
|
||||
id = cb(["PARAM", "DeviceID.ID"]);
|
||||
if (Array.isArray(id)) id = null;
|
||||
}
|
||||
return config.get(oldOpts[key], id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const v = expression.evaluate(snapshot.config[key], context, now, cb);
|
||||
return Array.isArray(v) ? null : v;
|
||||
const e = snapshot.config[key];
|
||||
if (!e) return dflt;
|
||||
const v = e.evaluate(fn).value;
|
||||
if (typeof v !== typeof dflt) return dflt;
|
||||
return v;
|
||||
}
|
||||
|
||||
export function getConfigExpression(
|
||||
|
||||
Reference in new issue
Block a user