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
+213
-141
@@ -3,11 +3,11 @@ import Router from "@koa/router";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as db from "./db.ts";
|
||||
import * as apiFunctions from "../api-functions.ts";
|
||||
import { evaluate, and, extractParams } from "../common/expression/util.ts";
|
||||
import { parse } from "../common/expression/parser.ts";
|
||||
import Expression, { extractPaths } from "../common/expression.ts";
|
||||
import Path from "../common/path.ts";
|
||||
import * as logger from "../logger.ts";
|
||||
import { getConfig } from "../ui/local-cache.ts";
|
||||
import { Expression, Task } from "../types.ts";
|
||||
import { Task } from "../types.ts";
|
||||
import { generateSalt, hashPassword } from "../auth.ts";
|
||||
import { del } from "../cache.ts";
|
||||
import Authorizer from "../common/authorizer.ts";
|
||||
@@ -67,11 +67,14 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
||||
id: ctx.params.id,
|
||||
};
|
||||
|
||||
const filter = and(authorizer.getFilter("devices", 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS.devices],
|
||||
ctx.params.id,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter("devices", 2),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS.devices)),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!authorizer.hasAccess("devices", 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
@@ -89,36 +92,51 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
||||
);
|
||||
|
||||
const lines: string[] = [
|
||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp",
|
||||
"Parameter,Object,Writable,Value,Value type,Timestamp,Value timestamp,Notification,Access list,Attributes timestamp",
|
||||
];
|
||||
|
||||
for (const k of Object.keys(device).sort()) {
|
||||
const p = device[k];
|
||||
let value = "";
|
||||
let type = "";
|
||||
if (p.value) {
|
||||
value = p.value[0];
|
||||
type = p.value[1];
|
||||
if (type === "xsd:dateTime" && typeof value === "number")
|
||||
value = new Date(value).toJSON();
|
||||
}
|
||||
const keys = Object.keys(device).sort();
|
||||
let prevParam = "";
|
||||
let attrs: Record<string, string | number | boolean | null> = {};
|
||||
|
||||
function flushRow(): void {
|
||||
if (!prevParam) return;
|
||||
let value: string | number | boolean | null = attrs["value"] ?? "";
|
||||
if (attrs["type"] === "xsd:dateTime" && typeof value === "number")
|
||||
value = new Date(value).toJSON();
|
||||
|
||||
const row = [
|
||||
k,
|
||||
p.object,
|
||||
new Date(p.objectTimestamp).toJSON(),
|
||||
p.writable,
|
||||
new Date(p.writableTimestamp).toJSON(),
|
||||
prevParam,
|
||||
attrs["object"] ?? "",
|
||||
attrs["writable"] ?? "",
|
||||
`"${String(value).replace(/"/g, '""')}"`,
|
||||
type,
|
||||
new Date(p.valueTimestamp).toJSON(),
|
||||
p.notification,
|
||||
new Date(p.notificationTimestamp).toJSON(),
|
||||
p.accessList ? p.accessList.join(", ") : "",
|
||||
new Date(p.accessListTimestamp).toJSON(),
|
||||
attrs["type"] ?? "",
|
||||
attrs["timestamp"] != null ? new Date(+attrs["timestamp"]).toJSON() : "",
|
||||
attrs["valueTimestamp"] != null
|
||||
? new Date(+attrs["valueTimestamp"]).toJSON()
|
||||
: "",
|
||||
attrs["notification"] ?? "",
|
||||
attrs["accessList"] ?? "",
|
||||
attrs["attributesTimestamp"] != null
|
||||
? new Date(+attrs["attributesTimestamp"]).toJSON()
|
||||
: "",
|
||||
];
|
||||
lines.push(row.map((r) => (r != null ? r : "")).join(","));
|
||||
}
|
||||
|
||||
for (const k of keys) {
|
||||
const colonIdx = k.lastIndexOf(":");
|
||||
const param = colonIdx === -1 ? k : k.slice(0, colonIdx);
|
||||
const attr = colonIdx === -1 ? "value" : k.slice(colonIdx + 1);
|
||||
|
||||
if (param !== prevParam) {
|
||||
flushRow();
|
||||
prevParam = param;
|
||||
attrs = {};
|
||||
}
|
||||
attrs[attr] = device[k];
|
||||
}
|
||||
flushRow();
|
||||
ctx.body = lines.join("\n");
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
@@ -128,7 +146,10 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
let filter: Expression = authorizer.getFilter(resource, 1);
|
||||
if (ctx.request.query.filter)
|
||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
Expression.parse(singleParam(ctx.request.query.filter)),
|
||||
);
|
||||
|
||||
const log = {
|
||||
message: `Count ${resource}`,
|
||||
@@ -144,11 +165,18 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = and(filter, [
|
||||
"OR",
|
||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
||||
["IS NULL", ["PARAM", "expiry"]],
|
||||
]);
|
||||
const p = new Expression.Parameter(Path.parse("expiry"));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
Expression.or(
|
||||
new Expression.Binary(
|
||||
">=",
|
||||
p,
|
||||
new Expression.Literal(Date.now() + 60000),
|
||||
),
|
||||
new Expression.Unary("IS NULL", p),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const count = await db.count(resource, filter);
|
||||
@@ -165,7 +193,10 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
const options: Parameters<typeof db.query>[2] = {};
|
||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||
if (ctx.request.query.filter)
|
||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
Expression.parse(singleParam(ctx.request.query.filter)),
|
||||
);
|
||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||
if (ctx.request.query.sort)
|
||||
@@ -193,11 +224,18 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = and(filter, [
|
||||
"OR",
|
||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
||||
["IS NULL", ["PARAM", "expiry"]],
|
||||
]);
|
||||
const p = new Expression.Parameter(Path.parse("expiry"));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
Expression.or(
|
||||
new Expression.Binary(
|
||||
">=",
|
||||
p,
|
||||
new Expression.Literal(Date.now() + 60000),
|
||||
),
|
||||
new Expression.Unary("IS NULL", p),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
logger.accessInfo(log);
|
||||
@@ -219,7 +257,10 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
const options: Parameters<typeof db.query>[2] = { projection: {} };
|
||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||
if (ctx.request.query.filter)
|
||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
Expression.parse(singleParam(ctx.request.query.filter)),
|
||||
);
|
||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||
if (ctx.request.query.sort)
|
||||
@@ -239,25 +280,38 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const columns: Record<string, Expression> = JSON.parse(
|
||||
const columnsStr: Record<string, string> = JSON.parse(
|
||||
singleParam(ctx.request.query.columns),
|
||||
);
|
||||
const now = Date.now();
|
||||
|
||||
for (const [k, v] of Object.entries(columns)) {
|
||||
const e = evaluate(parse(v as string), null, now);
|
||||
columns[k] = e;
|
||||
for (const p of extractParams(e))
|
||||
if (typeof p === "string") options.projection[p] = 1;
|
||||
}
|
||||
const now = Date.now();
|
||||
const columns: Record<string, Expression> = Object.fromEntries(
|
||||
Object.entries(columnsStr).map(([k, v]) => {
|
||||
let exp = Expression.parse(v);
|
||||
exp = exp.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall && e.name === "NOW")
|
||||
return new Expression.Literal(now);
|
||||
return e;
|
||||
});
|
||||
for (const p of extractPaths(exp)) options.projection[p.toString()] = 1;
|
||||
return [k, exp];
|
||||
}),
|
||||
);
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = and(filter, [
|
||||
"OR",
|
||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
||||
["IS NULL", ["PARAM", "expiry"]],
|
||||
]);
|
||||
const p = new Expression.Parameter(Path.parse("expiry"));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
Expression.or(
|
||||
new Expression.Binary(
|
||||
">=",
|
||||
p,
|
||||
new Expression.Literal(Date.now() + 60000),
|
||||
),
|
||||
new Expression.Unary("IS NULL", p),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
logger.accessInfo(log);
|
||||
@@ -272,45 +326,40 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
"\n";
|
||||
for await (const obj of db.query(resource, filter, options)) {
|
||||
const arr = Object.values(columns).map((exp) => {
|
||||
const v = evaluate(exp, obj, null, (e) => {
|
||||
if (Array.isArray(e)) {
|
||||
if (e[0] === "PARAM") {
|
||||
if (resource === "devices") {
|
||||
if (e[1] === "Tags") {
|
||||
const tags = [];
|
||||
for (const p in obj)
|
||||
if (p.startsWith("Tags."))
|
||||
tags.push(decodeTag(p.slice(5)));
|
||||
|
||||
return tags.join(", ");
|
||||
}
|
||||
if (e === exp) {
|
||||
const p = obj[e[1]];
|
||||
if (
|
||||
p &&
|
||||
p.value &&
|
||||
p.value[1] === "xsd:dateTime" &&
|
||||
typeof p.value[0] === "number"
|
||||
)
|
||||
return new Date(p.value[0]).toJSON();
|
||||
}
|
||||
} else if (resource === "faults") {
|
||||
if (e[1] === "detail") return yamlStringify(obj["detail"]);
|
||||
}
|
||||
} else if (e[0] === "FUNC") {
|
||||
if (e[1] === "DATE_STRING") {
|
||||
if (e[2] && !Array.isArray(e[2]))
|
||||
return new Date(e[2]).toJSON();
|
||||
}
|
||||
return exp.evaluate((e) => {
|
||||
if (e instanceof Expression.Literal) return e;
|
||||
else if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "NOW") return new Expression.Literal(now);
|
||||
if (e.name === "DATE_STRING") {
|
||||
if (e.args[0] instanceof Expression.Literal)
|
||||
return new Expression.Literal(
|
||||
new Date(e.args[0].value as number).toJSON(),
|
||||
);
|
||||
}
|
||||
} else if (e instanceof Expression.Parameter) {
|
||||
let v = obj[e.path.toString()];
|
||||
if (resource === "devices") {
|
||||
if (e.path.toString() === "Tags") {
|
||||
const tags = [];
|
||||
for (const p in obj)
|
||||
if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
|
||||
tags.push(decodeTag(p.slice(5)));
|
||||
v = tags.join(", ");
|
||||
}
|
||||
if (e === exp) {
|
||||
const type = obj[e.path.toString() + ":type"];
|
||||
if (type === "xsd:dateTime" && typeof v === "number")
|
||||
v = new Date(v).toJSON();
|
||||
}
|
||||
} else if (resource === "faults") {
|
||||
if (e.path.toString() === "detail") v = yamlStringify(v);
|
||||
}
|
||||
|
||||
if (typeof v === "string") v = `"${v.replace(/"/g, '""')}"`;
|
||||
if (v != null) return new Expression.Literal(v);
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
|
||||
if (Array.isArray(v) || v == null) return "";
|
||||
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
|
||||
return v;
|
||||
return new Expression.Literal(null);
|
||||
}).value;
|
||||
});
|
||||
yield arr.join(",") + "\n";
|
||||
}
|
||||
@@ -326,11 +375,15 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
||||
};
|
||||
|
||||
const filter = and(authorizer.getFilter(resource, 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter(resource, 2),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
@@ -352,11 +405,14 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
||||
};
|
||||
|
||||
const filter = and(authorizer.getFilter(resource, 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter(resource, 2),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
@@ -379,11 +435,14 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
id: ctx.params.id,
|
||||
};
|
||||
|
||||
const filter = and(authorizer.getFilter(resource, 3), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter(resource, 3),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
@@ -467,11 +526,14 @@ router.get("/blob/files/:id", async (ctx) => {
|
||||
id: id,
|
||||
};
|
||||
|
||||
const filter = and(authorizer.getFilter(resource, 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter(resource, 2),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
@@ -566,11 +628,14 @@ router.post("/devices/:id/tasks", async (ctx) => {
|
||||
let statuses: { _id: string; status: string }[];
|
||||
|
||||
try {
|
||||
const filter = and(authorizer.getFilter("devices", 3), [
|
||||
"=",
|
||||
["PARAM", "DeviceID.ID"],
|
||||
deviceId,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter("devices", 3),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
device = (await db.query("devices", filter).next()).value;
|
||||
if (!device) return void (ctx.status = 404);
|
||||
|
||||
@@ -596,30 +661,31 @@ router.post("/devices/:id/tasks", async (ctx) => {
|
||||
await releaseLock(`cwmp_session_${deviceId}`, token);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const onlineThreshold = getConfig(
|
||||
ctx.state.configSnapshot,
|
||||
"cwmp.deviceOnlineThreshold",
|
||||
{},
|
||||
Date.now(),
|
||||
4000,
|
||||
(exp) => {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
if (exp[0] === "PARAM") {
|
||||
const p = device[exp[1]];
|
||||
if (p?.value) return p.value[0];
|
||||
} else if (exp[0] === "FUNC") {
|
||||
if (exp[1] === "REMOTE_ADDRESS") {
|
||||
if (exp instanceof Expression.Literal) return exp;
|
||||
else if (exp instanceof Expression.Parameter) {
|
||||
const p = device[exp.path.toString()];
|
||||
if (p != null) return new Expression.Literal(p);
|
||||
} else if (exp instanceof Expression.FunctionCall) {
|
||||
if (exp.name === "NOW") return new Expression.Literal(now);
|
||||
if (exp.name === "REMOTE_ADDRESS") {
|
||||
for (const root of ["InternetGatewayDevice", "Device"]) {
|
||||
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
||||
if (p?.value) return new URL(p.value[0]).hostname;
|
||||
if (p != null) return new Expression.Literal(new URL(p).hostname);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return exp;
|
||||
return new Expression.Literal(null);
|
||||
},
|
||||
) as number;
|
||||
);
|
||||
|
||||
const lastInform = device["Events.Inform"].value[0] as number;
|
||||
const lastInform = device["Events.Inform"] as number;
|
||||
|
||||
let status = await apiFunctions.connectionRequest(deviceId, device);
|
||||
if (!status) {
|
||||
@@ -667,11 +733,14 @@ router.post("/devices/:id/tags", async (ctx) => {
|
||||
tags: ctx.request.body,
|
||||
};
|
||||
|
||||
const filter = and(authorizer.getFilter("devices", 3), [
|
||||
"=",
|
||||
["PARAM", "DeviceID.ID"],
|
||||
ctx.params.id,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter("devices", 3),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
|
||||
new Expression.Literal(ctx.params.id),
|
||||
),
|
||||
);
|
||||
if (!authorizer.hasAccess("devices", 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 403);
|
||||
@@ -745,11 +814,14 @@ router.put("/users/:id/password", async (ctx) => {
|
||||
const newPassword = ctx.request.body.newPassword;
|
||||
|
||||
if (ctx.state.user) {
|
||||
const filter = and(authorizer.getFilter("users", 3), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS.users],
|
||||
username,
|
||||
]);
|
||||
const filter = Expression.and(
|
||||
authorizer.getFilter("users", 3),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(RESOURCE_IDS["users"])),
|
||||
new Expression.Literal(username),
|
||||
),
|
||||
);
|
||||
const { value: res } = await db.query("users", filter).next();
|
||||
if (!res) return void (ctx.status = 404);
|
||||
const validate = authorizer.getValidator("users", res);
|
||||
|
||||
+66
-136
@@ -2,12 +2,11 @@ import { Script } from "node:vm";
|
||||
import { Readable } from "node:stream";
|
||||
import { Collection, ObjectId, WithoutId } from "mongodb";
|
||||
import { encodeTag } from "../util.ts";
|
||||
import { evaluate } from "../common/expression/util.ts";
|
||||
import { Expression, Fault, Task } from "../types.ts";
|
||||
import { Fault, Task } from "../types.ts";
|
||||
import { collections, filesBucket } from "../db/db.ts";
|
||||
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
|
||||
import * as MongoTypes from "../db/types.ts";
|
||||
import { parse, parseList, stringify } from "../common/expression/parser.ts";
|
||||
import Expression, { parseList, Value } from "../common/expression.ts";
|
||||
import { toMongoQuery } from "../db/synth.ts";
|
||||
|
||||
function processDeviceProjection(
|
||||
@@ -61,21 +60,8 @@ function parseDate(d: Date): number | string {
|
||||
return isNaN(n) ? "" + d : n;
|
||||
}
|
||||
|
||||
interface FlatAttributes {
|
||||
object?: boolean;
|
||||
objectTimestamp?: number;
|
||||
writable?: boolean;
|
||||
writableTimestamp?: number;
|
||||
value?: [string | number | boolean, string];
|
||||
valueTimestamp?: number;
|
||||
notification?: number;
|
||||
notificationTimestamp?: number;
|
||||
accessList?: string[];
|
||||
accessListTimestamp?: number;
|
||||
}
|
||||
|
||||
export interface FlatDevice {
|
||||
[param: string]: FlatAttributes;
|
||||
[param: string]: Value;
|
||||
}
|
||||
|
||||
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
||||
@@ -88,100 +74,38 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
||||
for (const [name, tree] of Object.entries(input)) {
|
||||
if (!root) {
|
||||
if (name === "_lastInform") {
|
||||
output["Events.Inform"] = {
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["Events.Inform"] = parseDate(tree as Date);
|
||||
output["Events.Inform:type"] = "xsd:dateTime";
|
||||
} else if (name === "_registered") {
|
||||
output["Events.Registered"] = {
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["Events.Registered"] = parseDate(tree as Date);
|
||||
output["Events.Registered:type"] = "xsd:dateTime";
|
||||
} else if (name === "_lastBoot") {
|
||||
output["Events.1_BOOT"] = {
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["Events.1_BOOT"] = parseDate(tree as Date);
|
||||
output["Events.1_BOOT:type"] = "xsd:dateTime";
|
||||
} else if (name === "_lastBootstrap") {
|
||||
output["Events.0_BOOTSTRAP"] = {
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["Events.0_BOOTSTRAP"] = parseDate(tree as Date);
|
||||
output["Events.0_BOOTSTRAP:type"] = "xsd:dateTime";
|
||||
} else if (name === "_id") {
|
||||
output["DeviceID.ID"] = {
|
||||
value: [tree as string, "xsd:string"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.ID"] = tree as string;
|
||||
output["DeviceID.ID:type"] = "xsd:dateTime";
|
||||
} else if (name === "_deviceId") {
|
||||
output["DeviceID.Manufacturer"] = {
|
||||
value: [tree["_Manufacturer"], "xsd:string"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.OUI"] = {
|
||||
value: [tree["_OUI"], "xsd:string"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.ProductClass"] = {
|
||||
value: [tree["_ProductClass"], "xsd:string"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.SerialNumber"] = {
|
||||
value: [tree["_SerialNumber"], "xsd:string"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.Manufacturer"] = tree["_Manufacturer"];
|
||||
output["DeviceID.Manufacturer:type"] = "xsd:string";
|
||||
output["DeviceID.OUI"] = tree["_OUI"];
|
||||
output["DeviceID.OUI:type"] = "xsd:string";
|
||||
output["DeviceID.ProductClass"] = tree["_ProductClass"];
|
||||
output["DeviceID.ProductClass:type"] = "xsd:string";
|
||||
output["DeviceID.SerialNumber"] = tree["_SerialNumber"];
|
||||
output["DeviceID.SerialNumber:type"] = "xsd:string";
|
||||
} else if (name === "_tags") {
|
||||
output["Tags"] = {
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: true,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["Tags:object"] = true;
|
||||
output["Tags:writable"] = true;
|
||||
|
||||
for (const t of tree as string[]) {
|
||||
output[`Tags.${encodeTag(t)}`] = {
|
||||
value: [true, "xsd:boolean"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: true,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
const et = encodeTag(t);
|
||||
output[`Tags.${et}`] = true;
|
||||
output[`Tags.${et}:type`] = "xsd:boolean";
|
||||
output[`Tags.${et}:writable`] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,40 +118,37 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
||||
else if (+input["_timestamp"] > timestamp)
|
||||
childrenTimestamp = +input["_timestamp"];
|
||||
|
||||
const attrs: FlatAttributes = {};
|
||||
const r = root ? `${root}.${name}` : name;
|
||||
|
||||
if (tree["_value"] != null) {
|
||||
attrs.value = [
|
||||
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
||||
tree["_type"],
|
||||
];
|
||||
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
|
||||
attrs.object = false;
|
||||
attrs.objectTimestamp = childrenTimestamp;
|
||||
output[r] =
|
||||
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"];
|
||||
output[`${r}:type`] = tree["_type"];
|
||||
output[`${r}:timestamp`] = childrenTimestamp;
|
||||
output[`${r}:valueTimestamp`] = +(
|
||||
tree["_timestamp"] || childrenTimestamp
|
||||
);
|
||||
} else if (tree["_object"] != null) {
|
||||
attrs.object = tree["_object"];
|
||||
attrs.objectTimestamp = childrenTimestamp;
|
||||
output[`${r}:object`] = tree["_object"];
|
||||
output[`${r}:timestamp`] = childrenTimestamp;
|
||||
}
|
||||
|
||||
if (tree["_writable"] != null) {
|
||||
attrs.writable = tree["_writable"];
|
||||
attrs.writableTimestamp = childrenTimestamp;
|
||||
output[`${r}:writable`] = tree["_writable"];
|
||||
output[`${r}:timestamp`] = childrenTimestamp;
|
||||
}
|
||||
|
||||
if (tree["_notification"] != null) {
|
||||
attrs.notification = tree["_notification"];
|
||||
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||
output[`${r}:notification`] = tree["_notification"];
|
||||
output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
|
||||
}
|
||||
|
||||
if (tree["_accessList"] != null) {
|
||||
attrs.accessList = tree["_accessList"];
|
||||
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||
output[`${r}:accessList`] = tree["_accessList"].join(",");
|
||||
output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
|
||||
}
|
||||
|
||||
const r = root ? `${root}.${name}` : name;
|
||||
output[r] = attrs;
|
||||
|
||||
if (attrs.object || tree["object"] == null)
|
||||
recursive(tree, r, output, childrenTimestamp);
|
||||
recursive(tree, r, output, childrenTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,14 +180,12 @@ function flattenPreset(
|
||||
if (p.precondition) {
|
||||
try {
|
||||
// Try parse to check expression validity
|
||||
parse(p.precondition as string);
|
||||
Expression.parse(p.precondition as string);
|
||||
} catch {
|
||||
p.precondition = convertOldPrecondition(
|
||||
JSON.parse(p.precondition as string),
|
||||
);
|
||||
p.precondition = (p.precondition as string).length
|
||||
? stringify(p.precondition as Expression)
|
||||
: "";
|
||||
const e = convertOldPrecondition(JSON.parse(p.precondition as string));
|
||||
if (e instanceof Expression.Literal && e.value)
|
||||
p.precondition = e.toString();
|
||||
else p.precondition = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +204,7 @@ function flattenPreset(
|
||||
) {
|
||||
p.provision = provision.name;
|
||||
p.provisionArgs = provision.args
|
||||
? provision.args.map((a) => stringify(a)).join(", ")
|
||||
? provision.args.map((a) => a.toString()).join(", ")
|
||||
: "";
|
||||
}
|
||||
|
||||
@@ -309,8 +228,7 @@ function preProcessPreset(data: Record<string, unknown>): MongoTypes.Preset {
|
||||
const preset = Object.assign({}, data);
|
||||
|
||||
if (!preset.precondition) preset.precondition = "";
|
||||
// Try parse to check expression validity
|
||||
parse(preset.precondition as string);
|
||||
else Expression.parse(preset.precondition as string); // Try parse to check validity
|
||||
|
||||
preset.weight = parseInt(preset.weight as string) || 0;
|
||||
|
||||
@@ -361,7 +279,13 @@ export async function* query(
|
||||
options?: QueryOptions,
|
||||
): AsyncGenerator<any, void, undefined> {
|
||||
options = options || {};
|
||||
filter = evaluate(filter, null, Date.now());
|
||||
const now = Date.now();
|
||||
filter = filter.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "NOW") return new Expression.Literal(now);
|
||||
}
|
||||
return e;
|
||||
});
|
||||
const q = toMongoQuery(filter, resource);
|
||||
if (!q) return;
|
||||
|
||||
@@ -408,7 +332,13 @@ export async function* query(
|
||||
|
||||
export function count(resource: string, filter: Expression): Promise<number> {
|
||||
const collection = collections[resource] as Collection<any>;
|
||||
filter = evaluate(filter, null, Date.now());
|
||||
const now = Date.now();
|
||||
filter = filter.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "NOW") return new Expression.Literal(now);
|
||||
}
|
||||
return e;
|
||||
});
|
||||
const q = toMongoQuery(filter, resource);
|
||||
if (!q) return Promise.resolve(0);
|
||||
return collection.countDocuments(q);
|
||||
|
||||
+39
-66
@@ -1,9 +1,7 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as config from "../config.ts";
|
||||
import { collections } from "../db/db.ts";
|
||||
import * as expression from "../common/expression/util.ts";
|
||||
import { parse } from "../common/expression/parser.ts";
|
||||
import { Expression, Users, Permissions, Config, UiConfig } from "../types.ts";
|
||||
import Expression, { Value } from "../common/expression.ts";
|
||||
import { Users, Permissions, Config, UiConfig } from "../types.ts";
|
||||
import { LocalCache } from "../local-cache.ts";
|
||||
|
||||
interface Snapshot {
|
||||
@@ -26,12 +24,14 @@ async function fetchPermissions(): Promise<[string, Permissions]> {
|
||||
if (!permissions[p.role]) permissions[p.role] = {};
|
||||
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
|
||||
|
||||
let validate: Expression;
|
||||
if (p.validate) validate = Expression.parse(p.validate);
|
||||
else validate = new Expression.Literal(true);
|
||||
permissions[p.role][p.access][p.resource] = {
|
||||
access: p.access,
|
||||
filter: parse(p.filter || "true"),
|
||||
filter: Expression.parse(p.filter || "true"),
|
||||
validate,
|
||||
};
|
||||
if (p.validate)
|
||||
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
|
||||
}
|
||||
|
||||
return [h, permissions];
|
||||
@@ -62,33 +62,18 @@ async function fetchConfig(): Promise<[string, Config, UiConfig]> {
|
||||
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
|
||||
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
|
||||
|
||||
const ui = {
|
||||
filters: {},
|
||||
device: {},
|
||||
index: {},
|
||||
overview: {},
|
||||
pageSize: null,
|
||||
};
|
||||
const ui: UiConfig = {};
|
||||
|
||||
const _config = {};
|
||||
|
||||
for (const c of conf) {
|
||||
// Evaluate expressions to simplify them
|
||||
const val = expression.evaluate(parse(c.value));
|
||||
_config[c._id] = val;
|
||||
if (c._id.startsWith("ui.")) {
|
||||
const keys = c._id.split(".");
|
||||
if (!(keys[1] in ui)) continue;
|
||||
// remove the first key(ui)
|
||||
keys.shift();
|
||||
let ref = ui;
|
||||
while (keys.length > 1) {
|
||||
const k = keys.shift();
|
||||
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
||||
ref = ref[k];
|
||||
}
|
||||
ref[keys[0]] = val;
|
||||
ui[c._id.slice(3)] = c.value;
|
||||
continue;
|
||||
}
|
||||
// Evaluate expressions to simplify them
|
||||
const val = Expression.parse(c.value).evaluate((e) => e);
|
||||
_config[c._id] = val;
|
||||
}
|
||||
|
||||
return [h, _config, ui];
|
||||
@@ -123,47 +108,35 @@ export async function getRevision(): Promise<string> {
|
||||
export function getConfig(
|
||||
revision: string,
|
||||
key: string,
|
||||
context: Record<string, unknown>,
|
||||
now: number,
|
||||
cb?: (e: Expression) => Expression,
|
||||
): string | number | boolean | null {
|
||||
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(revision: string, key: string): Expression {
|
||||
|
||||
Reference in new issue
Block a user