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

+47 -29
View File
@@ -46,6 +46,7 @@ connect to MongoDB and start an HTTP(S) server.
bin/ Service entry points (5 executables) bin/ Service entry points (5 executables)
lib/ All backend logic lib/ All backend logic
common/ Shared utilities (expression engine, Path, errors) common/ Shared utilities (expression engine, Path, errors)
expression.ts Expression class hierarchy (base + subclasses)
expression/ Expression parser, evaluator, normalizer, minimizer expression/ Expression parser, evaluator, normalizer, minimizer
cwmp/ CWMP-service-specific DB and caching cwmp/ CWMP-service-specific DB and caching
db/ Database layer (MongoDB collections, query synthesis) db/ Database layer (MongoDB collections, query synthesis)
@@ -78,15 +79,18 @@ MongoDB and start the HTTP server in each worker.
by the extensions subsystem. It communicates with its parent via IPC, by the extensions subsystem. It communicates with its parent via IPC,
executing user-defined extension scripts in isolation. executing user-defined extension scripts in isolation.
### The Expression System (`lib/common/expression/`) ### The Expression System (`lib/common/expression.ts`, `lib/common/expression/`)
The `Expression` type (`string | number | boolean | null | any[]`) is the most The `Expression` class (defined in `lib/common/expression.ts`) is the most
important abstraction in the codebase. It is a recursive, Lisp-like S-expression important abstraction in the codebase. It uses a typed class hierarchy:
where scalars are literal values and arrays are operations (the operator at `Expression.Literal` (wraps `string | number | boolean | null`),
index 0, operands following). For example: `Expression.Parameter` (wraps a `Path`), `Expression.Binary` (operator +
left/right), `Expression.Unary` (operator + operand), `Expression.FunctionCall`
(name + args), and `Expression.Conditional` (condition/then/otherwise). For
example, in the SQL-like text syntax:
``` ```
["AND", ["=", ["PARAM", "Device.ModelName"], "BrandX"], [">", ["PARAM", "Events.Inform"], 1000]] Device.ModelName = "BrandX" AND Events.Inform > 1000
``` ```
Expressions are used pervasively: Expressions are used pervasively:
@@ -101,23 +105,28 @@ Expressions are used pervasively:
The expression pipeline flows through four modules: The expression pipeline flows through four modules:
1. `parser.ts` -- Parses a SQL-like text syntax into the AST (uses Parsimmon). 1. `parser.ts` -- Parses a SQL-like text syntax into the AST using a hand-rolled
Also provides `map()` / `mapAsync()` tree-walking primitives and recursive descent parser with a `Cursor`-based scanner. Also provides
`stringify()` for serialization. `stringifyExpression()` for serialization. The `map()` / `mapAsync()`
tree-walking primitives are abstract methods on the `Expression` class, and
`stringify()` is now `Expression.toString()`.
2. `normalize.ts` -- Algebraic normalization using exact rational polynomial 2. `normalize.ts` -- Algebraic normalization using exact rational polynomial
arithmetic over bigints. Ensures equivalent expressions have the same arithmetic over native `bigint` values. The `Polynomial` class extends
canonical form (e.g., `a + 2 > b` and `a > b - 2` normalize identically). `Expression` as an intermediate representation. Ensures equivalent
expressions have the same canonical form (e.g., `a + 2 > b` and `a > b - 2`
normalize identically).
3. `synth.ts` -- Boolean logic minimization via the Espresso algorithm 3. `synth.ts` -- Boolean logic minimization via the Espresso algorithm
(espresso-iisojs). Converts expressions to minimal sum-of-products form with (espresso-iisojs). Converts expressions to minimal sum-of-products form with
three-valued logic (true/false/null). Handles domain-specific constraints three-valued logic (true/false/null). Handles domain-specific constraints
like comparison ordering and LIKE pattern relationships. like comparison ordering and LIKE pattern relationships.
4. `util.ts` -- Runtime evaluation. Resolves `PARAM` references against concrete 4. `evaluate.ts` -- Runtime evaluation. The `reduce()` function evaluates
data, evaluates operators, and supports partial evaluation (returns a reduced operators on literal values and supports partial evaluation (returns a
expression if some values are unknown). reduced expression if some values are unknown). Parameter resolution is
handled by the `Expression.evaluate()` method (in `lib/common/expression.ts`)
which calls `reduce()` after mapping children via a user-supplied callback.
`bigint.ts` provides a named-function wrapper around native bigint operations `pagination.ts` implements cursor-based pagination by generating filter
for readability. `pagination.ts` implements cursor-based pagination by expressions from sort-key bookmarks.
generating filter expressions from sort-key bookmarks.
### The Path System (`lib/common/path.ts`, `lib/common/path-set.ts`) ### The Path System (`lib/common/path.ts`, `lib/common/path-set.ts`)
@@ -127,17 +136,22 @@ query-based addressing.
Key design decisions: Key design decisions:
- **Interned/flyweight** -- All `Path` instances are cached. `Path.parse()` is - **Cached** -- `Path.parse()` caches instances in a two-generation LRU cache
the only way to create them; the constructor is protected. Two-generation LRU rotated every 120 seconds. The constructor is public (used directly by the
cache rotated every 120 seconds. parser and by methods like `slice()`, `concat()`, and `stripAlias()`).
- **Bitmask encoding** -- The `wildcard` and `alias` fields are bitfields for - **Bitmask encoding** -- The `wildcard` and `alias` fields are bitfields for
O(1) segment-type checking. This limits paths to 32 segments. O(1) segment-type checking. This limits paths to 32 segments. A `colon` field
tracks the number of attribute path segments (after a `:` separator), enabling
`paramLength` and `attrLength` accessors.
- **Immutable** -- Segment arrays are `Object.freeze()`-d. - **Immutable** -- Segment arrays are `Object.freeze()`-d.
`PathSet` is a multi-indexed collection of paths supporting pattern-matching `PathSet` is a multi-indexed collection of paths supporting pattern-matching
queries. It indexes paths by length, by segment-at-position, and by string queries. It maintains separate `paramSegmentIndex` and `attrSegmentIndex` arrays
representation. The `find()` method uses set intersection to efficiently match (one `Map<string, Set<Path>>` per position), plus a `stringIndex` map. The
wildcard patterns. `find()` method takes bitmasks to control which segments require exact matches
vs. wildcard compatibility, then uses set intersection across the smallest index
sets. A higher-level `findCompat()` method computes the appropriate bitmasks for
superset/subset matching.
### CWMP Protocol Layer (`lib/cwmp.ts`, `lib/soap.ts`, `lib/xml-parser.ts`) ### CWMP Protocol Layer (`lib/cwmp.ts`, `lib/soap.ts`, `lib/xml-parser.ts`)
@@ -272,13 +286,17 @@ and CSV export.
`lib/ui/db.ts` translates between the UI's flat parameter representation and `lib/ui/db.ts` translates between the UI's flat parameter representation and
MongoDB's nested document structure. The `flattenDevice()` function is the key MongoDB's nested document structure. The `flattenDevice()` function is the key
transformation: it recursively walks the nested device document and produces a transformation: it recursively walks the nested device document and produces a
flat key-value map (e.g., flat key-value map with colon-delimited attribute keys (e.g.,
`Device.WiFi.SSID.1.Name -> {value: [...], writable: true}`). `"Device.WiFi.SSID.1.Name" -> value`,
`"Device.WiFi.SSID.1.Name:type" -> "xsd:string"`,
`"Device.WiFi.SSID.1.Name:writable" -> true`). The `FlatDevice` type is
`Record<string, Value>` where `Value = string | number | boolean | null`.
`lib/ui/local-cache.ts` caches permissions, users, and config in-process with `lib/ui/local-cache.ts` caches permissions, users, and config in-process with
hash-based revision tracking. The `getConfig()` function resolves config hash-based revision tracking. The `getConfig()` function uses typed overloads --
expressions with dynamic context and backward compatibility for legacy config it takes a typed default value (`string`, `number`, or `boolean`) and an
keys. expression evaluation callback, returning a value of the same type as the
default.
### Database Layer (`lib/db/`) ### Database Layer (`lib/db/`)
+1 -7
View File
@@ -107,13 +107,7 @@ const packageDotJsonPlugin = {
const inlineDepsPlugin = { const inlineDepsPlugin = {
name: "inlineDeps", name: "inlineDeps",
setup(build) { setup(build) {
const deps = [ const deps = ["espresso-iisojs", "@codemirror", "mithril", "yaml"];
"parsimmon",
"espresso-iisojs",
"@codemirror",
"mithril",
"yaml",
];
const depFiles = new Set(); const depFiles = new Set();
build.onResolve({ filter: /^[^.]/ }, async (args) => { build.onResolve({ filter: /^[^.]/ }, async (args) => {
if (args.pluginData === "inlineDeps") return undefined; if (args.pluginData === "inlineDeps") return undefined;
+1 -1
View File
@@ -23,7 +23,7 @@ export default defineConfig(
}, },
}, },
rules: { rules: {
"no-shadow": ["error", { allow: ["err"] }], "@typescript-eslint/no-shadow": ["error", { allow: ["err"] }],
"handle-callback-err": "error", "handle-callback-err": "error",
"prefer-arrow-callback": "error", "prefer-arrow-callback": "error",
"no-buffer-constructor": "error", "no-buffer-constructor": "error",
+65 -74
View File
@@ -31,8 +31,8 @@ import {
udpConnectionRequest, udpConnectionRequest,
xmppConnectionRequest, xmppConnectionRequest,
} from "./connection-request.ts"; } from "./connection-request.ts";
import { Expression, Task } from "./types.ts"; import { Task } from "./types.ts";
import { evaluate } from "./common/expression/util.ts"; import Expression, { Value } from "./common/expression.ts";
import { hashPassword } from "./auth.ts"; import { hashPassword } from "./auth.ts";
import { flattenDevice } from "./ui/db.ts"; import { flattenDevice } from "./ui/db.ts";
import { ResourceLockedError } from "./common/errors.ts"; import { ResourceLockedError } from "./common/errors.ts";
@@ -42,7 +42,7 @@ const XMPP_CONFIGURED = !!config.get("XMPP_JID");
export async function connectionRequest( export async function connectionRequest(
deviceId: string, deviceId: string,
device?: Record<string, { value?: [boolean | number | string, string] }>, device?: Record<string, Value>,
): Promise<string> { ): Promise<string> {
if (!device) { if (!device) {
const res = await collections.devices.findOne({ _id: deviceId }); const res = await collections.devices.findOne({ _id: deviceId });
@@ -58,47 +58,36 @@ export async function connectionRequest(
password; password;
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) { if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
connectionRequestUrl = (device[ connectionRequestUrl =
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL" device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"] ||
].value || [""])[0]; "";
udpConnectionRequestAddress = (( udpConnectionRequestAddress =
device[ device[
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress" "InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
] || {} ] || "";
).value || [""])[0]; stunEnable =
stunEnable = (( device["InternetGatewayDevice.ManagementServer.STUNEnable"] || "";
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || {} connReqJabberId =
).value || [""])[0]; device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || "";
connReqJabberId = (( username =
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {}
).value || [""])[0];
username = ((
device[ device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername" "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
] || {} ] || "";
).value || [""])[0]; password =
password = ((
device[ device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword" "InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
] || {} ] || "";
).value || [""])[0];
} else { } else {
connectionRequestUrl = (device[ connectionRequestUrl =
"Device.ManagementServer.ConnectionRequestURL" device["Device.ManagementServer.ConnectionRequestURL"] || "";
].value || [""])[0]; udpConnectionRequestAddress =
udpConnectionRequestAddress = (( device["Device.ManagementServer.UDPConnectionRequestAddress"] || "";
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {} stunEnable = device["Device.ManagementServer.STUNEnable"] || "";
).value || [""])[0]; connReqJabberId = device["Device.ManagementServer.ConnReqJabberID"] || "";
stunEnable = ((device["Device.ManagementServer.STUNEnable"] || {}) username =
.value || [""])[0]; device["Device.ManagementServer.ConnectionRequestUsername"] || "";
connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {}) password =
.value || [""])[0]; device["Device.ManagementServer.ConnectionRequestPassword"] || "";
username = ((
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
).value || [""])[0];
password = ((
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
).value || [""])[0];
} }
let remoteAddress; let remoteAddress;
try { try {
@@ -107,68 +96,70 @@ export async function connectionRequest(
return "Invalid connection request URL"; return "Invalid connection request URL";
} }
const evalCallback = (exp): Expression => { const snapshot = await getRevision();
if (!Array.isArray(exp)) return exp; const now = Date.now();
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
let name = exp[1]; const evalCallback = (exp: Expression): Expression => {
if (exp instanceof Expression.Parameter) {
let name = exp.path.toString();
if (name === "id") name = "DeviceID.ID"; if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber"; else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass"; else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI"; else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress") return remoteAddress; else if (name === "remoteAddress")
else if (name === "username") return username; return new Expression.Literal(remoteAddress);
else if (name === "password") return password; else if (name === "username") return new Expression.Literal(username);
else if (name === "password") return new Expression.Literal(password);
const p = device[name]; return new Expression.Literal(device[name] ?? null);
if (p?.value) return p.value[0]; } else if (exp instanceof Expression.FunctionCall) {
} else if (exp[0] === "FUNC") { if (exp.name === "NOW") return new Expression.Literal(now);
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress; else if (exp.name === "REMOTE_ADDRESS")
else if (exp[1] === "USERNAME") return username; return new Expression.Literal(remoteAddress);
else if (exp[1] === "PASSWORD") return password; else if (exp.name === "USERNAME") return new Expression.Literal(username);
else if (exp.name === "PASSWORD") return new Expression.Literal(password);
} }
return exp; return exp;
}; };
const snapshot = await getRevision(); const configCallback = (exp: Expression): Expression.Literal => {
const now = Date.now(); const e = evalCallback(exp);
const UDP_CONNECTION_REQUEST_PORT = +getConfig( if (e instanceof Expression.Literal) return e;
return new Expression.Literal(null);
};
const UDP_CONNECTION_REQUEST_PORT = getConfig(
snapshot, snapshot,
"cwmp.udpConnectionRequestPort", "cwmp.udpConnectionRequestPort",
{}, 0,
now, configCallback,
evalCallback,
); );
const CONNECTION_REQUEST_TIMEOUT = +getConfig( const CONNECTION_REQUEST_TIMEOUT = getConfig(
snapshot, snapshot,
"cwmp.connectionRequestTimeout", "cwmp.connectionRequestTimeout",
{}, 2000,
now, configCallback,
evalCallback,
); );
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig( const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = getConfig(
snapshot, snapshot,
"cwmp.connectionRequestAllowBasicAuth", "cwmp.connectionRequestAllowBasicAuth",
{}, false,
now, configCallback,
evalCallback,
); );
let authExp: Expression = getConfigExpression( let authExp: Expression = getConfigExpression(
snapshot, snapshot,
"cwmp.connectionRequestAuth", "cwmp.connectionRequestAuth",
); );
if (authExp === undefined) { if (!authExp) {
authExp = [ authExp = new Expression.FunctionCall("AUTH", [
"FUNC", new Expression.FunctionCall("USERNAME", []),
"AUTH", new Expression.FunctionCall("PASSWORD", []),
["PARAM", "username"], ]);
["PARAM", "password"],
] as Expression;
} }
authExp = evaluate(authExp, {}, now, evalCallback); authExp = authExp.evaluate(evalCallback);
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback); const debug = getConfig(snapshot, "cwmp.debug", false, configCallback);
let udpProm = Promise.resolve(false); let udpProm = Promise.resolve(false);
if (udpConnectionRequestAddress && +stunEnable) { if (udpConnectionRequestAddress && +stunEnable) {
+30 -30
View File
@@ -1,5 +1,5 @@
import { PermissionSet, Expression } from "../types.ts"; import { PermissionSet } from "../types.ts";
import { evaluate, or } from "./expression/util.ts"; import Expression from "./expression.ts";
export default class Authorizer { export default class Authorizer {
declare private permissionSets: PermissionSet[]; declare private permissionSets: PermissionSet[];
@@ -43,12 +43,12 @@ export default class Authorizer {
if (this.getFilterCache.has(cacheKey)) if (this.getFilterCache.has(cacheKey))
return this.getFilterCache.get(cacheKey); return this.getFilterCache.get(cacheKey);
let filter: Expression = null; let filter: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) { for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) { for (const perm of permissionSet) {
if (perm[resourceType]) { if (perm[resourceType]) {
if (perm[resourceType].access >= access) if (perm[resourceType].access >= access)
filter = or(filter, perm[resourceType].filter); filter = Expression.or(filter, perm[resourceType].filter);
} }
} }
} }
@@ -64,7 +64,7 @@ export default class Authorizer {
if (this.validatorCache.has(resource)) if (this.validatorCache.has(resource))
return this.validatorCache.get(resource); return this.validatorCache.get(resource);
const validators: Expression[] = []; let validators: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) { for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) { for (const perm of permissionSet) {
@@ -73,7 +73,7 @@ export default class Authorizer {
perm[resourceType].access >= 3 && perm[resourceType].access >= 3 &&
perm[resourceType].validate perm[resourceType].validate
) )
validators.push(perm[resourceType].validate); validators = Expression.or(validators, perm[resourceType].validate);
} }
} }
@@ -82,8 +82,6 @@ export default class Authorizer {
mutation: any, mutation: any,
any: any, any: any,
): boolean => { ): boolean => {
if (!validators.length) return false;
const object = { const object = {
mutationType, mutationType,
mutation, mutation,
@@ -92,31 +90,33 @@ export default class Authorizer {
options: any, options: any,
}; };
const valueFunction = (paramName): any => { const now = Date.now();
const entry = paramName.split(".", 1)[0]; const res = validators.evaluate((exp) => {
paramName = paramName.slice(entry.length + 1); if (exp instanceof Expression.Literal) return exp;
let value = null; if (exp instanceof Expression.Parameter) {
if (["mutation", "options"].includes(entry)) { if (exp.path.colon) return new Expression.Literal(null);
value = object[entry]; const entry = exp.path.segments[0] as string;
for (const seg of paramName.split(".")) { const paramName = exp.path.slice(1);
if (value == null) break; let value = null;
if (typeof value !== "object") value = null; if (["mutation", "options"].includes(entry)) {
else value = value[seg]; value = object[entry];
for (const seg of paramName.segments) {
if (value == null) break;
if (typeof value !== "object") value = null;
else value = value[seg as string];
}
} else if (object[entry]) {
if (paramName.length) value = object[entry][paramName.toString()];
else value = object[entry];
} }
} else if (object[entry]) { return new Expression.Literal(value);
if (paramName) value = object[entry][paramName]; } else if (exp instanceof Expression.FunctionCall) {
else value = object[entry]; if (exp.name === "NOW") return new Expression.Literal(now);
} }
return new Expression.Literal(null);
}).value;
return value; return !!res;
};
const res = evaluate(
validators.length > 1 ? ["OR", validators] : validators[0],
valueFunction,
Date.now(),
);
return !Array.isArray(res) && !!res;
}; };
this.validatorCache.set(resource, validator); this.validatorCache.set(resource, validator);
+291
View File
@@ -0,0 +1,291 @@
import {
Cursor,
parseExpression,
stringifyExpression,
} from "./expression/parser.ts";
import Path from "./path.ts";
import { reduce } from "./expression/evaluate.ts";
export type Value = string | number | boolean | null;
export abstract class Expression {
private _string: string;
abstract map(fn: (e: Expression, i: number) => Expression): Expression;
abstract mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Expression>;
toString(): string {
if (!this._string) this._string = stringifyExpression(this);
return this._string;
}
evaluate<T extends Expression>(fn: (e: Expression) => T = (e) => e as T): T {
return fn(reduce(this.map((e) => e.evaluate(fn))));
}
async evaluateAsync<T extends Expression>(
fn: (e: Expression) => Promise<T>,
): Promise<T> {
return await fn(
reduce(await this.mapAsync(async (e) => await e.evaluateAsync(fn))),
);
}
static parse(input: string): Expression {
const cursor = new Cursor(input);
const exp = parseExpression(cursor);
if (cursor.charCode) throw new Error("Unexpected character");
return exp;
}
static and(left: Expression, right: Expression): Expression {
// Flatten same-operator tree into operands
const operands: Expression[] = [];
const stack: Expression[] = [right, left];
while (stack.length) {
const e = stack.pop()!;
if (e instanceof Expression.Binary && e.operator === "AND")
stack.push(e.right, e.left);
else operands.push(e);
}
// Fold literals using three-valued AND logic
let folded: boolean | null = true;
let i = 0;
while (i < operands.length) {
const e = operands[i];
if (e instanceof Expression.Literal) {
operands.splice(i, 1);
if (e.value == null) folded = folded === false ? false : null;
else if (!e.value) return new Expression.Literal(false);
// truthy is identity for AND; discard
} else {
i++;
}
}
// Rebuild: folded value + remaining non-literal operands
if (!operands.length) return new Expression.Literal(folded);
let result = operands.reduce((a, b) => new Expression.Binary("AND", a, b));
if (folded === null)
result = new Expression.Binary(
"AND",
new Expression.Literal(null),
result,
);
return result;
}
static or(left: Expression, right: Expression): Expression {
// Flatten same-operator tree into operands
const operands: Expression[] = [];
const stack: Expression[] = [right, left];
while (stack.length) {
const e = stack.pop()!;
if (e instanceof Expression.Binary && e.operator === "OR")
stack.push(e.right, e.left);
else operands.push(e);
}
// Fold literals using three-valued OR logic
let folded: boolean | null = false;
let i = 0;
while (i < operands.length) {
const e = operands[i];
if (e instanceof Expression.Literal) {
operands.splice(i, 1);
if (e.value == null) folded = folded === true ? true : null;
else if (e.value) return new Expression.Literal(true);
// falsy is identity for OR; discard
} else {
i++;
}
}
// Rebuild: folded value + remaining non-literal operands
if (!operands.length) return new Expression.Literal(folded);
let result = operands.reduce((a, b) => new Expression.Binary("OR", a, b));
if (folded === null)
result = new Expression.Binary(
"OR",
new Expression.Literal(null),
result,
);
return result;
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Expression {
export class Literal extends Expression {
constructor(public readonly value: Value) {
super();
}
map(): Literal {
return this;
}
async mapAsync(): Promise<Literal> {
return this;
}
}
export class Parameter extends Expression {
constructor(public readonly path: Path) {
super();
}
map(): Parameter {
return this;
}
async mapAsync(): Promise<Parameter> {
return this;
}
toString(): string {
return this.path.toString();
}
}
export class Unary extends Expression {
constructor(
public readonly operator: string,
public readonly operand: Expression,
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): Unary {
const operand = fn(this.operand, 0);
if (operand === this.operand) return this;
return Reflect.construct(this.constructor, [this.operator, operand]);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Unary> {
const operand = await fn(this.operand, 0);
if (operand === this.operand) return this;
return Reflect.construct(this.constructor, [this.operator, operand]);
}
}
export class Binary extends Expression {
constructor(
public readonly operator: string,
public readonly left: Expression,
public readonly right: Expression,
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): Binary {
const left = fn(this.left, 0);
const right = fn(this.right, 1);
if (left === this.left && right === this.right) return this;
return Reflect.construct(this.constructor, [this.operator, left, right]);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Binary> {
const left = await fn(this.left, 0);
const right = await fn(this.right, 1);
if (left === this.left && right === this.right) return this;
return Reflect.construct(this.constructor, [this.operator, left, right]);
}
}
export class FunctionCall extends Expression {
constructor(
public readonly name: string,
public readonly args: Expression[],
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): FunctionCall {
const args = this.args.map(fn);
if (args.every((arg, i) => arg === this.args[i])) return this;
return new FunctionCall(this.name, args);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<FunctionCall> {
const args = await Promise.all(this.args.map(fn));
if (args.every((arg, i) => arg === this.args[i])) return this;
return new FunctionCall(this.name, args);
}
}
export class Conditional extends Expression {
constructor(
public readonly condition: Expression,
public readonly then: Expression,
public readonly otherwise: Expression,
) {
super();
}
map(fn: (e: Expression, i: number) => Expression): Conditional {
const condition = fn(this.condition, 0);
const then = fn(this.then, 1);
const otherwise = fn(this.otherwise, 2);
if (
condition === this.condition &&
then === this.then &&
otherwise === this.otherwise
)
return this;
return new Conditional(condition, then, otherwise);
}
async mapAsync(
fn: (e: Expression, i: number) => Promise<Expression>,
): Promise<Conditional> {
const condition = await fn(this.condition, 0);
const then = await fn(this.then, 1);
const otherwise = await fn(this.otherwise, 2);
if (
condition === this.condition &&
then === this.then &&
otherwise === this.otherwise
)
return this;
return new Conditional(condition, then, otherwise);
}
}
}
export function extractPaths(exp: Expression): Path[] {
if (exp instanceof Expression.Parameter) return [exp.path];
const paths: Path[] = [];
exp.map((e) => {
if (e instanceof Expression.Parameter) paths.push(e.path);
else paths.push(...extractPaths(e));
return e;
});
return paths;
}
export function parseList(input: string): Expression[] {
const CHAR_COMMA = 44;
const res: Expression[] = [];
const cursor = new Cursor(input);
cursor.skipwhitespace();
if (!cursor.charCode) return res;
res.push(parseExpression(cursor));
while (cursor.charCode === CHAR_COMMA) {
cursor.step();
res.push(parseExpression(cursor));
}
if (cursor.charCode) throw new Error("Unexpected character");
return res;
}
export default Expression;
-87
View File
@@ -1,87 +0,0 @@
export function and(a: bigint, b: bigint): bigint {
return a & b;
}
export function or(a: bigint, b: bigint): bigint {
return a | b;
}
export function xor(a: bigint, b: bigint): bigint {
return a ^ b;
}
export function not(a: bigint): bigint {
return ~a;
}
export function lshift(a: bigint, b: bigint): bigint {
return a << b;
}
export function rshift(a: bigint, b: bigint): bigint {
return a >> b;
}
export function add(a: bigint, b: bigint): bigint {
return a + b;
}
export function sub(a: bigint, b: bigint): bigint {
return a - b;
}
export function mul(a: bigint, b: bigint): bigint {
return a * b;
}
export function div(a: bigint, b: bigint): bigint {
return a / b;
}
export function exp(a: bigint, b: bigint): bigint {
return a ** b;
}
export function rem(a: bigint, b: bigint): bigint {
return a % b;
}
export function toNumber(a: bigint): number {
return Number(a);
}
export function eq(a: bigint, b: bigint): boolean {
return a === b;
}
export function ne(a: bigint, b: bigint): boolean {
return a !== b;
}
export function lt(a: bigint, b: bigint): boolean {
return a < b;
}
export function lte(a: bigint, b: bigint): boolean {
return a <= b;
}
export function gt(a: bigint, b: bigint): boolean {
return a > b;
}
export function gte(a: bigint, b: bigint): boolean {
return a >= b;
}
export function asUintN(a: number, b: bigint): bigint {
return BigInt.asUintN(a, b);
}
export function asIntN(a: number, b: bigint): bigint {
return BigInt.asIntN(a, b);
}
const _BigInt = BigInt;
type _bigint = bigint;
export { _BigInt as BigInt, type _bigint as bigint };
+218
View File
@@ -0,0 +1,218 @@
import Expression from "../expression.ts";
import { likePatternToRegExp } from "./parser.ts";
function compare(
a: boolean | number | string,
b: boolean | number | string,
): number {
if (typeof a === "boolean") a = +a;
if (typeof b === "boolean") b = +b;
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
return a > b ? 1 : a < b ? -1 : 0;
}
function toNumber(a: boolean | number | string): number {
switch (typeof a) {
case "number":
return a;
case "boolean":
return +a;
case "string":
return parseFloat(a) || 0;
}
}
function toString(a: boolean | number | string): string {
switch (typeof a) {
case "string":
return a;
case "number":
return a.toString();
case "boolean":
return (+a).toString();
}
}
const regExpCache: WeakMap<Expression.Literal, RegExp> = new WeakMap();
export function reduce(exp: Expression): Expression {
if (exp instanceof Expression.Literal) return exp;
if (exp instanceof Expression.Unary) {
if (exp.operator === "NOT") {
if (exp.operand instanceof Expression.Literal) {
if (exp.operand.value == null) return exp.operand;
return new Expression.Literal(!exp.operand.value);
} else if (exp.operand instanceof Expression.Unary) {
if (exp.operand.operator === "NOT") return exp.operand.operand;
}
} else if (exp.operator === "IS NULL") {
if (exp.operand instanceof Expression.Literal) {
return new Expression.Literal(exp.operand.value == null);
}
} else if (exp.operator === "IS NOT NULL") {
if (exp.operand instanceof Expression.Literal) {
return new Expression.Literal(exp.operand.value != null);
}
}
} else if (exp instanceof Expression.Binary) {
if (exp.operator === "AND") {
return Expression.and(exp.left, exp.right);
} else if (exp.operator === "OR") {
return Expression.or(exp.left, exp.right);
} else if (["=", ">", "<", "<>", ">=", "<="].includes(exp.operator)) {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const c = compare(exp.left.value, exp.right.value);
switch (exp.operator) {
case "=":
return new Expression.Literal(c === 0);
case ">":
return new Expression.Literal(c > 0);
case "<":
return new Expression.Literal(c < 0);
case "<>":
return new Expression.Literal(c !== 0);
case ">=":
return new Expression.Literal(c >= 0);
case "<=":
return new Expression.Literal(c <= 0);
}
}
} else if (["+", "-", "*", "/"].includes(exp.operator)) {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const a = toNumber(exp.left.value);
const b = toNumber(exp.right.value);
switch (exp.operator) {
case "+":
return new Expression.Literal(a + b);
case "-":
return new Expression.Literal(a - b);
case "*":
return new Expression.Literal(a * b);
case "/":
return new Expression.Literal(a / b);
}
}
} else if (exp.operator === "%") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const a = toNumber(exp.left.value);
const b = Math.trunc(toNumber(exp.right.value));
if (b === 0) return new Expression.Literal(null);
return new Expression.Literal(a % b);
}
} else if (exp.operator === "||") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const a = toString(exp.left.value);
const b = toString(exp.right.value);
return new Expression.Literal(a + b);
}
} else if (exp.operator === "LIKE") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const s = toString(exp.left.value);
let r = regExpCache.get(exp.right);
if (!r) {
r = likePatternToRegExp(toString(exp.right.value));
regExpCache.set(exp.right, r);
}
return new Expression.Literal(r.test(s));
}
} else if (exp.operator === "NOT LIKE") {
if (exp.left instanceof Expression.Literal && exp.left.value == null)
return exp.left;
if (exp.right instanceof Expression.Literal && exp.right.value == null)
return exp.right;
if (
exp.left instanceof Expression.Literal &&
exp.right instanceof Expression.Literal
) {
const s = toString(exp.left.value);
let r = regExpCache.get(exp.right);
if (!r) {
r = likePatternToRegExp(toString(exp.right.value));
regExpCache.set(exp.right, r);
}
return new Expression.Literal(!r.test(s));
}
}
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "COALESCE") {
const args = exp.args.filter(
(arg) => !(arg instanceof Expression.Literal && arg.value == null),
);
if (!args.length) return new Expression.Literal(null);
if (args.length === 1) return args[0];
if (args[0] instanceof Expression.Literal) return args[0];
if (args.length !== exp.args.length)
return new Expression.FunctionCall("COALESCE", args);
} else if (exp.name === "UPPER") {
if (exp.args[0] instanceof Expression.Literal) {
if (exp.args[0].value == null) return exp.args[0];
const a = toString(exp.args[0].value);
return new Expression.Literal(a.toUpperCase());
}
} else if (exp.name === "LOWER") {
if (exp.args[0] instanceof Expression.Literal) {
if (exp.args[0].value == null) return exp.args[0];
const a = toString(exp.args[0].value);
return new Expression.Literal(a.toLowerCase());
}
} else if (exp.name === "ROUND") {
let p = 0;
if (exp.args.length > 1) {
if (exp.args[1] instanceof Expression.Literal) {
if (exp.args[1].value == null) return exp.args[1];
p = Math.trunc(toNumber(exp.args[1].value));
}
}
if (exp.args[0] instanceof Expression.Literal) {
if (exp.args[0].value == null) return exp.args[0];
const n = toNumber(exp.args[0].value);
const d = 10 ** p;
const m = n * d * (1 + Number.EPSILON);
return new Expression.Literal(Math.round(m) / d);
}
}
} else if (exp instanceof Expression.Conditional) {
if (exp.condition instanceof Expression.Literal) {
if (exp.condition.value) return exp.then;
return exp.otherwise;
}
}
return exp;
}
+229 -231
View File
@@ -1,22 +1,15 @@
import * as BI from "./bigint.ts"; import Expression from "../expression.ts";
import { Expression } from "../../types.ts"; import { reduce } from "./evaluate.ts";
import { map } from "./parser.ts";
import { and, evaluateCallback } from "./util.ts";
const ZERO = BI.BigInt(0);
const ONE = BI.BigInt(1);
const TWO = BI.BigInt(2);
const NEGATIVE_ONE = BI.BigInt(-1);
class Indeterminates { class Indeterminates {
declare public map: Map<string, number>; declare public map: Map<Expression, number>;
declare public sortedKeys: string[]; declare public sortedKeys: Expression[];
public constructor(str?: string) { public constructor(exp?: Expression) {
this.map = new Map(); this.map = new Map();
if (str) { if (exp) {
this.map.set(str, 1); this.map.set(exp, 1);
this.sortedKeys = [str]; this.sortedKeys = [exp];
} else { } else {
this.sortedKeys = []; this.sortedKeys = [];
} }
@@ -37,27 +30,31 @@ class Indeterminates {
const res = new Indeterminates(); const res = new Indeterminates();
res.sortedKeys = indeterminates1.sortedKeys.slice(); res.sortedKeys = indeterminates1.sortedKeys.slice();
res.map = new Map(indeterminates1.map); res.map = new Map(indeterminates1.map);
const strMap: Map<string, Expression> = new Map();
for (const k of res.map.keys()) strMap.set(k.toString(), k);
for (const [key, val] of indeterminates2.map) { for (const [key, val] of indeterminates2.map) {
const v = res.map.get(key); const k = strMap.get(key.toString());
if (!v) { if (!k) {
res.map.set(key, val); res.map.set(key, val);
res.sortedKeys.push(key); res.sortedKeys.push(key);
} else { } else {
const v2 = val + v; const v2 = val + res.map.get(k);
if (!v2) { if (!v2) {
res.map.delete(key); res.map.delete(k);
res.sortedKeys = res.sortedKeys.filter((s) => s !== key); res.sortedKeys = res.sortedKeys.filter((s) => s !== k);
} else { } else {
res.map.set(key, v2); res.map.set(k, v2);
} }
} }
} }
res.sortedKeys.sort((a, b) => { res.sortedKeys.sort((a, b) => {
if (a.length !== b.length) return b.length - a.length; const str1 = a.toString();
else if (a > b) return 1; const str2 = b.toString();
else if (a < b) return -1; if (str1.length !== str2.length) return str2.length - str1.length;
else if (str1 > str2) return 1;
else if (str1 < str2) return -1;
return 0; return 0;
}); });
@@ -73,10 +70,10 @@ class Indeterminates {
const k2 = b.sortedKeys[i]; const k2 = b.sortedKeys[i];
const w2 = b.map.get(k2); const w2 = b.map.get(k2);
if (w1 !== w2) return w2 - w1; if (w1 !== w2) return w2 - w1;
if (k1.length > k2.length) return -1; if (k1.toString().length > k2.toString().length) return -1;
else if (k1.length < k2.length) return 1; else if (k1.toString().length < k2.toString().length) return 1;
else if (k1 > k2) return 1; else if (k1.toString() > k2.toString()) return 1;
else if (k1 < k2) return -1; else if (k1.toString() < k2.toString()) return -1;
} }
return 0; return 0;
} }
@@ -84,26 +81,35 @@ class Indeterminates {
interface Term { interface Term {
indeterminates: Indeterminates; indeterminates: Indeterminates;
coefficientNumerator: BI.bigint; coefficientNumerator: bigint;
coefficientDenominator: BI.bigint; coefficientDenominator: bigint;
} }
function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint { function findGcd(a: bigint, b: bigint): bigint {
while (BI.ne(b, ZERO)) { while (b !== 0n) {
const t = b; const t = b;
b = BI.rem(a, b); b = a % b;
a = t; a = t;
} }
return a; return a;
} }
class Polynomial { class Polynomial extends Expression {
declare public terms: Term[]; declare public terms: Term[];
public constructor(terms: Term[]) { public constructor(terms: Term[]) {
super();
this.terms = terms; this.terms = terms;
} }
map(): Polynomial {
return this;
}
async mapAsync(): Promise<Polynomial> {
return this;
}
public static simplifyTerms(terms: Term[]): Term[] { public static simplifyTerms(terms: Term[]): Term[] {
const ts = terms const ts = terms
.slice() .slice()
@@ -115,40 +121,37 @@ class Polynomial {
const t1 = ts[i - 1]; const t1 = ts[i - 1];
const t2 = ts[i]; const t2 = ts[i];
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) { if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
const numerator = BI.add( const numerator =
BI.mul(t1.coefficientNumerator, t2.coefficientDenominator), t1.coefficientNumerator * t2.coefficientDenominator +
BI.mul(t2.coefficientNumerator, t1.coefficientDenominator), t2.coefficientNumerator * t1.coefficientDenominator;
);
const denominator = BI.mul( const denominator =
t1.coefficientDenominator, t1.coefficientDenominator * t2.coefficientDenominator;
t2.coefficientDenominator,
);
const gcd = findGcd(numerator, denominator); const gcd = findGcd(numerator, denominator);
ts[i] = { ts[i] = {
indeterminates: t2.indeterminates, indeterminates: t2.indeterminates,
coefficientNumerator: BI.div(numerator, gcd), coefficientNumerator: numerator / gcd,
coefficientDenominator: BI.div(denominator, gcd), coefficientDenominator: denominator / gcd,
}; };
ts[i - 1] = { ts[i - 1] = {
indeterminates: t1.indeterminates, indeterminates: t1.indeterminates,
coefficientNumerator: ZERO, coefficientNumerator: 0n,
coefficientDenominator: t1.coefficientDenominator, coefficientDenominator: t1.coefficientDenominator,
}; };
} }
} }
return ts.filter((v) => BI.ne(v.coefficientNumerator, ZERO)); return ts.filter((v) => v.coefficientNumerator !== 0n);
} }
public static fromIndeterminate(indeterminate: Expression): Polynomial { public static fromIndeterminate(indeterminate: Expression): Polynomial {
const indeterminates = new Indeterminates(JSON.stringify(indeterminate)); const indeterminates = new Indeterminates(indeterminate);
const terms = [ const terms = [
{ {
indeterminates: indeterminates, indeterminates: indeterminates,
coefficientNumerator: ONE, coefficientNumerator: 1n,
coefficientDenominator: ONE, coefficientDenominator: 1n,
}, },
]; ];
return new Polynomial(terms); return new Polynomial(terms);
@@ -156,15 +159,12 @@ class Polynomial {
public static fromConstant(constant: number): Polynomial { public static fromConstant(constant: number): Polynomial {
const [int, frac] = Math.abs(constant).toString(2).split(".", 2); const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
let numerator = BI.BigInt("0b" + int); let numerator = BigInt("0b" + int);
if (constant < 0) numerator = BI.mul(numerator, NEGATIVE_ONE); if (constant < 0) numerator = numerator / -1n;
let denominator = ONE; let denominator = 1n;
if (frac) { if (frac) {
denominator = BI.exp(TWO, BI.BigInt(frac.length)); denominator = 2n ** BigInt(frac.length);
numerator = BI.add( numerator = numerator * denominator + BigInt("0b" + frac);
BI.mul(numerator, denominator),
BI.BigInt("0b" + frac),
);
} }
const terms = [ const terms = [
@@ -181,7 +181,7 @@ class Polynomial {
public negation(): Polynomial { public negation(): Polynomial {
const terms = this.terms.map((t) => ({ const terms = this.terms.map((t) => ({
indeterminates: t.indeterminates, indeterminates: t.indeterminates,
coefficientNumerator: BI.mul(t.coefficientNumerator, NEGATIVE_ONE), coefficientNumerator: t.coefficientNumerator * -1n,
coefficientDenominator: t.coefficientDenominator, coefficientDenominator: t.coefficientDenominator,
})); }));
@@ -217,14 +217,9 @@ class Polynomial {
for (const t1 of this.terms) { for (const t1 of this.terms) {
for (const t2 of rhs.terms) { for (const t2 of rhs.terms) {
const numerator = BI.mul( const numerator = t1.coefficientNumerator * t2.coefficientNumerator;
t1.coefficientNumerator, const denominator =
t2.coefficientNumerator, t1.coefficientDenominator * t2.coefficientDenominator;
);
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const gcd = findGcd(numerator, denominator); const gcd = findGcd(numerator, denominator);
terms.push({ terms.push({
@@ -232,8 +227,8 @@ class Polynomial {
t1.indeterminates, t1.indeterminates,
t2.indeterminates, t2.indeterminates,
), ),
coefficientNumerator: BI.div(numerator, gcd), coefficientNumerator: numerator / gcd,
coefficientDenominator: BI.div(denominator, gcd), coefficientDenominator: denominator / gcd,
}); });
} }
} }
@@ -245,41 +240,49 @@ class Polynomial {
return this.multiply(rhs.reciprocal()); return this.multiply(rhs.reciprocal());
} }
public toString(): string { public toExpression(): Expression {
const add: string[] = []; const add: Expression[] = [];
for (const t of this.terms) { for (const t of this.terms) {
const coefficient = const coefficient =
BI.toNumber(t.coefficientNumerator) / Number(t.coefficientNumerator) / Number(t.coefficientDenominator);
BI.toNumber(t.coefficientDenominator);
const mul: string[] = []; const mul: Expression[] = [];
if (t.indeterminates.sortedKeys.length) { if (t.indeterminates.sortedKeys.length) {
for (const k of t.indeterminates.sortedKeys) { for (const k of t.indeterminates.sortedKeys) {
const w = t.indeterminates.map.get(k); const w = t.indeterminates.map.get(k);
for (let i = Math.abs(w); i > 0; --i) { for (let i = Math.abs(w); i > 0; --i) {
if (w > 0) mul.push(k); if (w > 0) mul.push(k);
else mul.push(`["/",1,${k}]`); else
mul.push(
new Expression.Binary("/", new Expression.Literal(1), k),
);
} }
} }
if (coefficient !== 1) mul.push(coefficient.toString()); if (coefficient !== 1) mul.push(new Expression.Literal(coefficient));
if (mul.length > 1) add.push(`["*",${mul.join(",")}]`); while (mul.length > 1) {
else add.push(mul["0"]); const r = mul.pop();
const l = mul.pop();
mul.push(new Expression.Binary("*", l, r));
}
add.push(mul[0]);
} else { } else {
add.push(coefficient.toString()); add.push(new Expression.Literal(coefficient));
} }
} }
if (!add.length) return "0"; while (add.length > 1) {
else if (add.length === 1) return add[0]; const r = add.pop();
else return `["+",${add.join(",")}]`; const l = add.pop();
add.push(new Expression.Binary("+", l, r));
}
if (!add.length) return new Expression.Literal(0);
return add[0];
} }
} }
const ADDITIVE_IDENTITY = Polynomial.fromConstant(0);
const MULTIPLICATIVE_IDENTITY = Polynomial.fromConstant(1);
const SWAPPED_OPS = { const SWAPPED_OPS = {
"=": "=", "=": "=",
"<>": "<>", "<>": "<>",
@@ -289,156 +292,163 @@ const SWAPPED_OPS = {
"<=": ">=", "<=": ">=",
}; };
function cartesianProduct<T>(arrays: T[][]): T[][] {
return arrays.reduce<T[][]>(
(acc, cur) => acc.flatMap((a) => cur.map((item) => [...a, item])),
[[]],
);
}
function toPolynomial(e: Expression): Polynomial {
if (e instanceof Polynomial) return e;
if (e instanceof Expression.Literal) {
if (e.value == null) return null;
if (typeof e.value === "number") return Polynomial.fromConstant(e.value);
if (typeof e.value === "string")
return Polynomial.fromConstant(parseFloat(e.value) || 0);
if (typeof e.value === "boolean") return Polynomial.fromConstant(+e.value);
}
return Polynomial.fromIndeterminate(e);
}
function fromPolynomial(e: Expression): Expression {
if (e instanceof Polynomial) return e.toExpression();
if (e instanceof Expression.Conditional) return e.map(fromPolynomial);
return e;
}
function normalizeCallback(exp: Expression): Expression { function normalizeCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp; if (exp instanceof Expression.FunctionCall) {
const op = exp[0]; if (exp.name === "COALESCE") {
let e: Expression = new Expression.Literal(null);
if (op === "FUNC" && exp[1] === "COALESCE") { for (let i = exp.args.length - 1; i >= 0; --i) {
const res: Expression[] = ["CASE"]; e = new Expression.Conditional(
for (let i = 2; i < exp.length; ++i) normalizeCallback(new Expression.Unary("IS NOT NULL", exp.args[i])),
res.push(normalizeCallback(["IS NOT NULL", exp[i]]), exp[i]); exp.args[i],
return normalizeCallback(res); e,
}
if (op === "CASE") {
const res = [] as [Expression, Expression][];
for (let i = 1; i < exp.length; i += 2) {
let w = exp[i];
if (w instanceof Polynomial) w = JSON.parse(w.toString());
if (!Array.isArray(w) && !w) continue;
const t = exp[i + 1];
if (!Array.isArray(t) || t[0] !== "CASE") {
res.push([w, t]);
continue;
}
for (let j = 1; j < t.length; j += 2) res.push([and(w, t[j]), t[j + 1]]);
res.push([w, null]);
if (!Array.isArray(w) && w) break;
}
while (res[res.length - 1][1] == null) res.pop();
return ["CASE", ...res.flat()];
}
const permutations: Map<number, [Expression, Expression][]> = new Map();
for (const [i, e] of exp.entries()) {
if (!Array.isArray(e)) continue;
if (e[0] !== "CASE") continue;
const perms: [Expression, Expression][] = [];
for (let j = 1; j < e.length; j += 2) perms.push([e[j], e[j + 1]]);
perms.push([true, null]);
permutations.set(i, perms);
}
if (permutations.size) {
let res: [Expression, Expression][] = [[true, exp]];
for (const [i, perms] of permutations) {
const res2: [Expression, Expression][] = [];
for (const [w, t] of perms) {
res2.push(
...res.map((r) => {
const e = (r[1] as Expression[]).slice();
e[i] = t;
return [and(w, r[0]), e] as [Expression, Expression];
}),
); );
if (!Array.isArray(w) && w) break;
} }
res = res2; return normalizeCallback(e);
} }
for (const r of res) r[1] = normalizeCallback(r[1]);
if (res[0][0] === true) return res[0][1];
while (res[res.length - 1][1] == null) res.pop();
return ["CASE", ...res.flat()];
} }
function toPolynomial(e: Expression): Polynomial { if (exp instanceof Expression.Conditional) {
if (e == null) return null; let e = exp;
if (e instanceof Polynomial) return e;
if (typeof e === "number") return Polynomial.fromConstant(e); if (e.condition instanceof Polynomial)
if (typeof e === "string") e = new Expression.Conditional(
return Polynomial.fromConstant(parseFloat(e) || 0); e.condition.toExpression(),
if (typeof e === "boolean") return Polynomial.fromConstant(+e); e.then,
return Polynomial.fromIndeterminate(e); e.otherwise,
);
if (e.then instanceof Expression.Conditional) {
e = new Expression.Conditional(
Expression.and(e.condition, e.then.condition),
e.then.then,
new Expression.Conditional(e.condition, e.then.otherwise, e.otherwise),
);
}
return e;
} }
if (op === "+") { const combs: [Expression, Expression][][] = [];
const args: Polynomial[] = []; const callback: (e: Expression) => Expression = (e) => {
for (let i = 1; i < exp.length; ++i) { if (e instanceof Expression.Conditional) {
const p = toPolynomial(exp[i]); combs[combs.length - 1].push([e.condition, e.then]);
if (p == null) return null; callback(e.otherwise);
args.push(p); } else {
combs[combs.length - 1].push([new Expression.Literal(true), e]);
} }
return args.reduce( return e;
(previousValue, currentValue) => previousValue.add(currentValue), };
ADDITIVE_IDENTITY,
) as unknown as Expression; exp.map((e) => {
} else if (op === "*") { combs.push([]);
const args: Polynomial[] = []; callback(e);
for (let i = 1; i < exp.length; ++i) { return e;
const p = toPolynomial(exp[i]); });
if (p == null) return null;
args.push(p); if (combs.some((a) => a.length > 1)) {
let res: Expression = new Expression.Literal(null);
for (const p of cartesianProduct(combs).reverse()) {
let condition: Expression = new Expression.Literal(true);
const e = reduce(
normalizeCallback(
exp.map((_, i) => {
condition = Expression.and(condition, p[i][0]);
return p[i][1];
}),
),
);
if (!(condition instanceof Expression.Literal))
res = new Expression.Conditional(condition, e, res);
else if (condition.value) res = e;
} }
return args.reduce( return res;
(previousValue, currentValue) => previousValue.multiply(currentValue), }
MULTIPLICATIVE_IDENTITY,
) as unknown as Expression;
} else if (op === "-") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce((previousValue, currentValue) =>
previousValue.subtract(currentValue),
) as unknown as Expression;
} else if (op === "/") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce((previousValue, currentValue) =>
previousValue.divide(currentValue),
) as unknown as Expression;
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
if (exp[1] == null || exp[2] == null) return null;
let lhs: Polynomial, rhs: Polynomial;
if (exp[1] instanceof Polynomial) lhs = exp[1];
else if (typeof exp[1] === "number") lhs = Polynomial.fromConstant(exp[1]);
if (exp[2] instanceof Polynomial) rhs = exp[2]; if (exp instanceof Expression.Binary) {
else if (typeof exp[2] === "number") rhs = Polynomial.fromConstant(exp[2]); if (["+", "-", "*", "/"].includes(exp.operator)) {
const lhs = toPolynomial(exp.left);
const rhs = toPolynomial(exp.right);
if (lhs == null || rhs == null) return new Expression.Literal(null);
if (exp.operator === "+") return lhs.add(rhs);
if (exp.operator === "-") return lhs.subtract(rhs);
if (exp.operator === "*") return lhs.multiply(rhs);
if (exp.operator === "/") return lhs.divide(rhs);
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(exp.operator)) {
let lhs: Polynomial, rhs: Polynomial;
if (lhs || rhs) { if (exp.left instanceof Polynomial) lhs = exp.left;
if (!lhs) lhs = Polynomial.fromIndeterminate(exp[1]); else if (exp.left instanceof Expression.Literal) {
if (!rhs) rhs = Polynomial.fromIndeterminate(exp[2]); if (exp.left.value == null) return exp.left;
else if (typeof exp.left.value === "number")
lhs = Polynomial.fromConstant(exp.left.value);
}
lhs = lhs.subtract(rhs); if (exp.right instanceof Polynomial) rhs = exp.right;
rhs = lhs.constant().negation(); else if (exp.right instanceof Expression.Literal) {
lhs = lhs.add(rhs); if (exp.right.value == null) return exp.right;
else if (typeof exp.right.value === "number")
rhs = Polynomial.fromConstant(exp.right.value);
}
if (!lhs.terms.length) { if (lhs || rhs) {
exp = [op, JSON.parse(lhs.toString()), JSON.parse(rhs.toString())]; if (!lhs) lhs = Polynomial.fromIndeterminate(exp.left);
if (!rhs) rhs = Polynomial.fromIndeterminate(exp.right);
lhs = lhs.subtract(rhs);
rhs = lhs.constant().negation();
lhs = lhs.add(rhs);
if (!lhs.terms.length) {
const l = lhs.toExpression() as Expression.Literal;
const r = rhs.toExpression() as Expression.Literal;
if (exp.operator === "=")
return new Expression.Literal(l.value === r.value);
else if (exp.operator === "<>")
return new Expression.Literal(l.value !== r.value);
else if (exp.operator === ">")
return new Expression.Literal(l.value > r.value);
else if (exp.operator === ">=")
return new Expression.Literal(l.value >= r.value);
else if (exp.operator === "<")
return new Expression.Literal(l.value < r.value);
else if (exp.operator === "<=")
return new Expression.Literal(l.value <= r.value);
else throw new Error("Invalid operator");
}
if (op === "=") return exp[1] === exp[2];
else if (op === "<>") return exp[1] !== exp[2];
else if (op === ">") return exp[1] > exp[2];
else if (op === ">=") return exp[1] >= exp[2];
else if (op === "<") return exp[1] < exp[2];
else if (op === "<=") return exp[1] <= exp[2];
} else {
let flipOp = 1; let flipOp = 1;
const n = lhs.terms[0].coefficientNumerator; const n = lhs.terms[0].coefficientNumerator;
const d = lhs.terms[0].coefficientDenominator; const d = lhs.terms[0].coefficientDenominator;
if (BI.lt(n, ZERO) || BI.lt(d, ZERO)) flipOp *= -1; if (n < 0n || d < 0n) flipOp *= -1;
const reciprocal = new Polynomial([ const reciprocal = new Polynomial([
{ {
@@ -463,30 +473,18 @@ function normalizeCallback(exp: Expression): Expression {
rhs = rhs.reciprocal(); rhs = rhs.reciprocal();
} }
if (flipOp < 0) exp = [SWAPPED_OPS[op], lhs, rhs]; if (flipOp > 0) exp = new Expression.Binary(exp.operator, lhs, rhs);
else exp = [op, lhs, rhs]; else exp = new Expression.Binary(SWAPPED_OPS[exp.operator], lhs, rhs);
} }
} }
} }
// Restore polynomial expressions // Restore polynomial expressions
exp = exp.map((e) => exp = exp.map(fromPolynomial);
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
exp = evaluateCallback(exp);
return exp; return exp;
} }
export default function normalize(expr: Expression): Expression { export default function normalize(exp: Expression): Expression {
expr = map(expr, normalizeCallback); return fromPolynomial(exp.evaluate(normalizeCallback));
if (expr instanceof Polynomial) {
expr = JSON.parse(expr.toString());
} else if (Array.isArray(expr) && expr[0] === "CASE") {
expr = expr.map((e) =>
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
}
return expr;
} }
+42 -30
View File
@@ -1,8 +1,8 @@
import { complement } from "espresso-iisojs"; import { complement } from "espresso-iisojs";
import { Expression } from "../../types.ts"; import Expression from "../expression.ts";
import normalize from "./normalize.ts"; import normalize from "./normalize.ts";
import { Clause, SynthContext } from "./synth.ts"; import { Clause, SynthContext } from "./synth.ts";
import { and, or } from "./util.ts"; import Path from "../path.ts";
type Bookmark = Record<string, null | boolean | number | string>; type Bookmark = Record<string, null | boolean | number | string>;
@@ -27,25 +27,35 @@ export function bookmarkToExpression(
): Expression { ): Expression {
return Object.entries(sort) return Object.entries(sort)
.reverse() .reverse()
.reduce((cur, kv) => { .reduce(
const [param, asc] = kv; (cur, kv) => {
if (asc < 0) { const [param, asc] = kv;
if (bookmark[param] == null) { const p = new Expression.Parameter(Path.parse(param));
return or( const b = new Expression.Literal(bookmark[param]);
["IS NOT NULL", ["PARAM", param]], if (asc < 0) {
and(["IS NULL", ["PARAM", param]], cur), if (bookmark[param] == null) {
return Expression.or(
new Expression.Unary("IS NOT NULL", p),
Expression.and(new Expression.Unary("IS NULL", p), cur),
);
}
return new Expression.Binary(
"OR",
new Expression.Binary(">", p, b),
new Expression.Binary("AND", new Expression.Binary("=", p, b), cur),
);
} else {
let f: Expression = new Expression.Unary("IS NULL", p);
if (bookmark[param] == null) return Expression.and(f, cur);
f = Expression.or(f, new Expression.Binary("<", p, b));
return Expression.or(
f,
Expression.and(new Expression.Binary("=", p, b), cur),
); );
} }
let f = null; },
f = or(f, [">", ["PARAM", param], bookmark[param]]); new Expression.Literal(true) as Expression,
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur)); );
} else {
let f: Expression = ["IS NULL", ["PARAM", param]];
if (bookmark[param] == null) return and(f, cur);
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
}
}, true as Expression);
} }
function getCover( function getCover(
@@ -60,7 +70,7 @@ function getCover(
const nextCov: Minterm = []; const nextCov: Minterm = [];
if (sort > 0) { if (sort > 0) {
const lhs = new Clause.Exp(["PARAM", param]); const lhs = new Clause.Exp(new Expression.Parameter(Path.parse(param)));
const isNull = context.getVar(new Clause.IsNull(lhs)); const isNull = context.getVar(new Clause.IsNull(lhs));
cov.push((isNull << 2) ^ 2); cov.push((isNull << 2) ^ 2);
} }
@@ -68,15 +78,15 @@ function getCover(
for (const m of minterm) { for (const m of minterm) {
const clause = context.getClause(m >>> 2); const clause = context.getClause(m >>> 2);
if (clause instanceof Clause.IsNull) { if (clause instanceof Clause.IsNull) {
const lhs = clause.operand.expression(); if (!(clause.operand instanceof Clause.Exp)) continue;
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue; if (!(clause.operand.exp instanceof Expression.Parameter)) continue;
if (lhs[1] !== param) continue; if (clause.operand.exp.path.toString() !== param) continue;
nextCov.push(m); nextCov.push(m);
if (sort < 0 && m & 1) cov.push(m, m ^ 1); if (sort < 0 && m & 1) cov.push(m, m ^ 1);
} else if (clause instanceof Clause.Compare) { } else if (clause instanceof Clause.Compare) {
const lhs = clause.lhs.expression(); if (!(clause.lhs instanceof Clause.Exp)) continue;
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue; if (!(clause.lhs.exp instanceof Expression.Parameter)) continue;
if (lhs[1] !== param) continue; if (clause.lhs.exp.path.toString() !== param) continue;
if (!(m & 1) && sort > 0) continue; if (!(m & 1) && sort > 0) continue;
nextCov.push(m); nextCov.push(m);
@@ -117,18 +127,20 @@ export function paginate(
sort: Record<string, number>, sort: Record<string, number>,
): [Expression, Expression] { ): [Expression, Expression] {
fetched = normalize(fetched); fetched = normalize(fetched);
if (!fetched) return [false, toFetch]; if (fetched instanceof Expression.Literal && !fetched.value)
return [new Expression.Literal(false), toFetch];
toFetch = normalize(toFetch); toFetch = normalize(toFetch);
if (!toFetch) return [false, toFetch]; if (toFetch instanceof Expression.Literal && !toFetch.value)
return [new Expression.Literal(false), toFetch];
const synth1 = Clause.fromExpression(fetched); const synth1 = Clause.fromExpression(fetched);
const synth2 = Clause.fromExpression(toFetch); const synth2 = Clause.fromExpression(toFetch);
const context = new SynthContext(); const context = new SynthContext();
const expr1Minterms = synth1.true(context); const expr1Minterms = synth1.getMinterms(context, 0b100);
const expr2MintermsC = complement(synth2.true(context)); const expr2MintermsC = synth2.getMinterms(context, 0b011);
const gaps = context.sanitizeMinterms( const gaps = context.sanitizeMinterms(
complement([ complement([
+496 -398
View File
@@ -1,5 +1,166 @@
import parsimmon from "parsimmon"; import Path from "../path.ts";
import { Expression } from "../../types.ts"; import Expression from "../expression.ts";
export class Cursor {
input: string;
pos: number;
boundryStack: number[][];
boundry: number[];
charCode: number;
constructor(input: string) {
this.input = input;
this.pos = 0;
this.boundryStack = [];
this.boundry = [];
this.charCode = input.charCodeAt(0) || 0;
}
fork(): Cursor {
const cursor = new Cursor(this.input);
cursor.pos = this.pos;
cursor.boundryStack = this.boundryStack.slice();
cursor.boundry = this.boundry;
cursor.charCode = this.charCode;
return cursor;
}
sync(cursor: Cursor): void {
this.pos = cursor.pos;
this.boundryStack = cursor.boundryStack.slice();
this.boundry = cursor.boundry;
this.charCode = cursor.charCode;
}
read(cur: Cursor): string {
return this.input.slice(this.pos, cur.pos);
}
step(): Cursor {
if (this.charCode === 0) return this;
++this.pos;
this.charCode = this.input.charCodeAt(this.pos) || 0;
if (this.boundry.includes(this.charCode)) this.charCode = 0;
return this;
}
walk(callback: (charCode: number) => boolean): Cursor {
while (this.charCode && callback(this.charCode)) this.step();
return this;
}
descend(chars: number[], override = true): void {
this.boundryStack.push(this.boundry);
if (override) this.boundry = chars;
else this.boundry = [...this.boundry, ...chars];
this.charCode = this.input.charCodeAt(this.pos) || 0;
if (this.boundry.includes(this.charCode)) this.charCode = 0;
}
ascend(): void {
if (!this.boundryStack.length) throw new Error("Unmatched boundry");
this.boundry = this.boundryStack.pop();
this.charCode = this.input.charCodeAt(this.pos) || 0;
}
skipwhitespace(): Cursor {
return this.walk((c) => c <= 32);
}
}
const CHAR_SINGLE_QUOTE = 39;
const CHAR_DOUBLE_QUOTE = 34;
const CHAR_OPEN_PAREN = 40;
const CHAR_CLOSE_PAREN = 41;
const CHAR_COMMA = 44;
const CHAR_PERIOD = 46;
const CHAR_COLON = 58;
const CHAR_OPEN_BRACKET = 91;
const CHAR_BACKSLASH = 92;
const CHAR_CLOSE_BRACKET = 93;
const BINARY_OPERATORS = [
">=",
"<=",
"<>",
"=",
">",
"<",
"LIKE",
"NOT LIKE",
"AND",
"OR",
"*",
"/",
"%",
"||",
"-",
"+",
];
const PRECEDENCE = {
OR: 10,
AND: 11,
NOT: 12,
"=": 20,
"<>": 20,
">": 20,
">=": 20,
"<": 20,
"<=": 20,
LIKE: 20,
"NOT LIKE": 20,
"IS NULL": 20,
"IS NOT NULL": 20,
"||": 30,
"+": 31,
"-": 31,
"*": 32,
"/": 32,
"%": 32,
};
function* range(s: number, e: number): Generator<number> {
for (let i = s; i < e; i++) yield i;
}
const PATH_CHARS = new Set<number>([
...range(65, 91),
...range(97, 123),
...range(48, 58),
95,
45,
42,
123,
125,
]);
function findOperator(cursor: Cursor): string {
cursor.skipwhitespace();
let found = "";
let foundCursor = cursor;
let operators = [...BINARY_OPERATORS, "IS NULL", "IS NOT NULL"];
for (let i = 0; operators.length && cursor.charCode; ++i) {
let c = cursor.charCode;
cursor.step();
if (c >= 97 && c <= 122) c -= 32;
if (c <= 32) {
cursor.skipwhitespace();
c = 32;
}
operators = operators.filter((o) => {
if (o.charCodeAt(i) !== c) return false;
if (o.length === i + 1) {
found = o;
foundCursor = cursor.fork();
}
return true;
});
}
if (found) cursor.sync(foundCursor);
return found;
}
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n"). // Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
function interpretEscapes(str): string { function interpretEscapes(str): string {
@@ -21,423 +182,330 @@ function interpretEscapes(str): string {
}); });
} }
export function map<T>(exp: Expression, callback: (e: Expression) => T): T { export function parseExpression(cursor: Cursor, presedence = 0): Expression {
if (!Array.isArray(exp)) return callback(exp); cursor.skipwhitespace();
const char = cursor.charCode;
let lhs: Expression;
if (char === CHAR_OPEN_PAREN) {
cursor.step();
cursor.descend([CHAR_CLOSE_PAREN]);
lhs = parseExpression(cursor, 0);
cursor.ascend();
let clone; cursor.skipwhitespace();
for (let i = 1; i < exp.length; ++i) { if (cursor.charCode !== CHAR_CLOSE_PAREN)
const sub = map(exp[i], callback); throw new Error("Expected ')'" + cursor.pos + " " + cursor.charCode);
if (sub !== exp[i]) { cursor.step();
clone = clone || exp.slice(); } else if (char === CHAR_DOUBLE_QUOTE) {
clone[i] = sub; cursor.step();
const cursor2 = cursor.fork();
for (
cursor2.descend([]);
cursor2.charCode !== CHAR_DOUBLE_QUOTE;
cursor2.step()
) {
if (!cursor2.charCode) throw new Error("Unterminated string");
if (cursor2.charCode === CHAR_BACKSLASH) cursor2.step();
}
cursor2.ascend();
const str = cursor.read(cursor2);
cursor.sync(cursor2);
cursor.step();
return new Expression.Literal(interpretEscapes(str));
} else if (char === CHAR_SINGLE_QUOTE) {
cursor.step();
const cursor2 = cursor.fork();
for (
cursor2.descend([]);
cursor2.charCode !== CHAR_SINGLE_QUOTE;
cursor2.step()
) {
if (!cursor2.charCode) throw new Error("Unterminated string");
if (cursor2.charCode === CHAR_BACKSLASH) cursor2.step();
}
cursor2.ascend();
const str = cursor.read(cursor2);
cursor.sync(cursor2);
cursor.step();
return new Expression.Literal(str.replaceAll("''", "'"));
} else {
const cursor2 = cursor.fork();
cursor.walk(
(c) => c > 32 && c !== CHAR_OPEN_PAREN && c !== CHAR_OPEN_BRACKET,
);
const token = cursor2.read(cursor);
if (!token) throw new Error("Invalid expression");
if (/^true$/i.test(token)) lhs = new Expression.Literal(true);
else if (/^false$/i.test(token)) lhs = new Expression.Literal(false);
else if (/^null$/i.test(token)) lhs = new Expression.Literal(null);
else if (/^-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(token))
lhs = new Expression.Literal(Number(token));
else if (/^not$/i.test(token)) {
lhs = new Expression.Unary(
"NOT",
parseExpression(cursor, PRECEDENCE["NOT"]),
);
} else if (/^case$/i.test(token)) {
const pairs: [Expression, Expression][] = [];
for (;;) {
cursor.skipwhitespace();
const whenStr = cursor.fork().read(cursor.walk((c) => c > 32));
if (/^else$/i.test(whenStr)) {
lhs = parseExpression(cursor);
continue;
}
if (/^end$/i.test(whenStr)) break;
else if (lhs) throw new Error("Expected END");
if (!/^when$/i.test(whenStr)) throw new Error("Expected WHEN");
const condition = parseExpression(cursor);
cursor.skipwhitespace();
const thenStr = cursor.fork().read(cursor.walk((c) => c > 32));
if (!/^then$/i.test(thenStr)) throw new Error("Expected THEN");
const then = parseExpression(cursor);
pairs.push([condition, then]);
}
if (!lhs) lhs = new Expression.Literal(null);
while (pairs.length) {
const [condition, then] = pairs.pop();
lhs = new Expression.Conditional(condition, then, lhs);
}
} else if (cursor.charCode === CHAR_OPEN_PAREN) {
cursor.step();
cursor.descend([CHAR_CLOSE_PAREN]);
cursor.skipwhitespace();
const args = [] as Expression[];
while (cursor.charCode) {
cursor.descend([CHAR_COMMA], false);
const e = parseExpression(cursor);
args.push(e);
cursor.ascend();
cursor.skipwhitespace();
if ((cursor.charCode as number) !== CHAR_COMMA) break;
cursor.step();
}
cursor.ascend();
cursor.step();
lhs = new Expression.FunctionCall(token, args);
} else {
const p = parsePath(cursor2);
lhs = new Expression.Parameter(p);
cursor.sync(cursor2);
} }
} }
return callback(clone || exp); for (;;) {
} cursor.skipwhitespace();
const cursor2 = cursor.fork();
export async function mapAsync( const op = findOperator(cursor2);
exp: Expression, const p = PRECEDENCE[op];
callback: (e: Expression) => Promise<Expression>, if (p <= presedence) return lhs;
): Promise<Expression> { if (op === "IS NULL") {
if (!Array.isArray(exp)) return callback(exp); lhs = new Expression.Unary("IS NULL", lhs);
} else if (op === "IS NOT NULL") {
let clone; lhs = new Expression.Unary("IS NOT NULL", lhs);
for (let i = 1; i < exp.length; ++i) { } else if (BINARY_OPERATORS.includes(op)) {
const sub = await mapAsync(exp[i], callback); lhs = new Expression.Binary(op, lhs, parseExpression(cursor2, p));
if (sub !== exp[i]) { } else if (op) throw new Error("Unrecognized operator: " + op);
clone = clone || exp.slice(); else break;
clone[i] = sub; cursor.sync(cursor2);
}
} }
return lhs;
return callback(clone || exp);
} }
function binaryLeft( // Parse old-format alias value: unquoted, double-quoted, or single-quoted.
operatorsParser: parsimmon.Parser<string>, // Returns the parsed string value. Cursor is advanced past the value.
nextParser: parsimmon.Parser<Expression>, function parseOldAliasValue(cursor: Cursor): string {
): parsimmon.Parser<Expression> { cursor.walk((c) => c <= 32); // skip leading whitespace
return parsimmon.seqMap( const c = cursor.charCode;
nextParser, if (c === CHAR_DOUBLE_QUOTE) {
parsimmon.seq(operatorsParser, nextParser).many(), // Double-quoted: use JSON.parse semantics (handles \", \n, \uXXXX etc.)
(first, rest) => // Descend with no boundaries so commas/brackets inside quotes are not
rest.reduce((acc, ch) => { // treated as terminators.
const [op, another] = ch; const start = cursor.pos;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]); cursor.step();
return [op, acc, another]; cursor.descend([]);
}, first), while (cursor.charCode) {
); if (cursor.charCode === CHAR_BACKSLASH) {
cursor.step();
if (!cursor.charCode) break;
} else if (cursor.charCode === CHAR_DOUBLE_QUOTE) {
cursor.ascend();
cursor.step();
return JSON.parse(cursor.input.slice(start, cursor.pos)) as string;
}
cursor.step();
}
throw new Error("Unterminated string");
}
if (c === CHAR_SINGLE_QUOTE) {
// Single-quoted: strip quotes, no escape processing
cursor.step();
cursor.descend([]);
const start = cursor.pos;
while (cursor.charCode && cursor.charCode !== CHAR_SINGLE_QUOTE)
cursor.step();
if (!cursor.charCode) throw new Error("Unterminated string");
const value = cursor.input.slice(start, cursor.pos);
cursor.ascend();
cursor.step();
return value;
}
// Unquoted: read until , or ] (respecting boundary), trim result
const start = cursor.pos;
cursor.walk(() => true);
return cursor.input.slice(start, cursor.pos).trim();
} }
const lang = parsimmon.createLanguage({ // Parse old-format alias content: key:value,key:value,...
ComparisonOperator: function () { // Cursor should be positioned at the start of bracket content
return parsimmon // (after '[', with ']' set as boundary).
.alt( // Returns an Expression: Binary("AND", ...) chain of Binary("=", param, literal).
parsimmon.string(">="), // Empty content returns Literal(true).
parsimmon.string("<>"), function parseOldAlias(cursor: Cursor): Expression {
parsimmon.string("<="), cursor.skipwhitespace();
parsimmon.string("="), if (!cursor.charCode) return new Expression.Literal(true);
parsimmon.string(">"),
parsimmon.string("<"), let result: Expression | null = null;
) for (;;) {
.skip(parsimmon.optWhitespace); cursor.skipwhitespace();
}, // Parse key as a path (supports nested aliases via parsePath).
LikeOperator: function () { // Add colon and comma as boundaries so the path stops there.
return parsimmon cursor.descend([CHAR_COLON, CHAR_COMMA], false);
.alt( const keyPath = parsePath(cursor);
parsimmon.regexp(/like/i).result("LIKE").desc("LIKE"), cursor.ascend();
parsimmon
.regexp(/not\s+like/i) // After ascend, check for the colon separator
.result("NOT LIKE") cursor.skipwhitespace();
.desc("NOT LIKE"), if ((cursor.charCode as number) !== CHAR_COLON)
) throw new Error("Expected ':'");
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) cursor.step();
.skip(parsimmon.optWhitespace);
}, // Parse value (reads until boundary , or ])
IsNullOperator: function () { cursor.descend([CHAR_COMMA], false);
return parsimmon const value = parseOldAliasValue(cursor);
.alt( cursor.ascend();
parsimmon
.regexp(/is\s+null/i) const pair = new Expression.Binary(
.result("IS NULL") "=",
.desc("IS NULL"), new Expression.Parameter(keyPath),
parsimmon new Expression.Literal(value),
.regexp(/is\s+not\s+null/i)
.result("IS NOT NULL")
.desc("IS NOT NULL"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NotOperator: function () {
return parsimmon
.regexp(/not/i)
.result("NOT")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("NOT");
},
AndOperator: function () {
return parsimmon
.regexp(/and/i)
.result("AND")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("AND");
},
OrOperator: function () {
return parsimmon
.regexp(/or/i)
.result("OR")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("OR");
},
Parameter: function (r) {
return parsimmon
.alt(
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
r.Expression.wrap(
parsimmon.string("{").skip(parsimmon.optWhitespace),
parsimmon.string("}"),
),
)
.atLeast(1)
.map((x) => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
.skip(parsimmon.optWhitespace)
.desc("parameter");
},
StringValueSql: function () {
return parsimmon
.regexp(/'([^']*)'/, 1)
.atLeast(1)
.skip(parsimmon.optWhitespace)
.map((s) => s.join("'"))
.desc("string");
},
StringValueJs: function () {
return parsimmon
.regexp(/"((?:\\.|.)*?)"/, 1)
.skip(parsimmon.optWhitespace)
.map(interpretEscapes)
.desc("string");
},
NumberValue: function () {
return parsimmon
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.map(Number)
.desc("number");
},
BooleanValue: function () {
return parsimmon
.alt(
parsimmon.regexp(/true/i).result(true).desc("TRUE"),
parsimmon.regexp(/false/i).result(false).desc("FALSE"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NullValue: function () {
return parsimmon
.regexp(/null/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.result(null)
.desc("NULL");
},
FuncValue: function (r) {
return parsimmon.seqMap(
parsimmon
.regexp(/([a-zA-Z0-9_]+)/, 1)
.skip(parsimmon.optWhitespace)
.desc("function"),
r.ExpressionList.wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace),
),
(f, args) => ["FUNC", f.toUpperCase()].concat(args),
); );
}, result = result ? new Expression.Binary("AND", result, pair) : pair;
WhenPair: function (r) {
return parsimmon.seq( cursor.skipwhitespace();
parsimmon if ((cursor.charCode as number) !== CHAR_COMMA) break;
.regexp(/when/i) cursor.step();
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) }
.skip(parsimmon.optWhitespace) return result;
.desc("WHEN") }
.then(r.Expression),
parsimmon export function parsePath(cur: Cursor): Path {
.regexp(/then/i) const segments: (string | Expression)[] = [];
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) let colon = 0;
.skip(parsimmon.optWhitespace) const cur2 = cur.fork();
.desc("THEN") for (;;) {
.then(r.Expression), let char = cur2.charCode;
); let exp: Expression;
},
CaseStatement: function (r) { if (char === CHAR_OPEN_BRACKET) {
return parsimmon.seqMap( if (cur2.pos !== cur.pos) throw new Error("Invalid path");
parsimmon cur2.step();
.regexp(/case/i) cur2.descend([CHAR_CLOSE_BRACKET]);
.result("CASE") // Try parsing as expression first
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) const cur3 = cur2.fork();
.skip(parsimmon.optWhitespace) let err: unknown;
.desc("CASE"), try {
r.WhenPair.many(), exp = parseExpression(cur3);
parsimmon } catch (e) {
.regexp(/else/i) err = e;
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) }
.skip(parsimmon.optWhitespace) // Fall back to old alias format if not a valid expression or produced
.desc("ELSE") // a bare Parameter (old format like [a:1] mis-parsed as a path).
.then(r.Expression) if (err || exp instanceof Expression.Parameter) {
.map((e) => [[true, e]]) exp = parseOldAlias(cur2);
.fallback(null) } else {
.skip( cur2.sync(cur3);
parsimmon }
.regex(/end/i) cur2.ascend();
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)), char = cur2.charCode;
) if (char !== CHAR_CLOSE_BRACKET) throw new Error("Expected ']'");
.skip(parsimmon.optWhitespace), char = cur2.step().charCode;
(...arr) => arr.flat(2),
);
},
Value: function (r) {
return parsimmon.alt(
r.NullValue,
r.BooleanValue,
r.NumberValue,
r.StringValueSql,
r.StringValueJs,
r.FuncValue,
r.CaseStatement,
);
},
ValueExpression: function (r) {
return binaryLeft(
parsimmon.string("||").skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(parsimmon.string("+"), parsimmon.string("-"))
.skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(
parsimmon.string("*"),
parsimmon.string("/"),
parsimmon.string("%"),
)
.skip(parsimmon.optWhitespace),
parsimmon.alt(
r.Value,
r.Parameter,
r.Expression.wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace),
),
),
),
),
);
},
Comparison: function (r) {
return parsimmon.alt(
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
parsimmon.seqMap(
r.ValueExpression,
r.ComparisonOperator,
r.ValueExpression,
(p, o, v) => [o, p, v],
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression.skip(
parsimmon
.regexp(/escape/i)
.result("ESCAPE")
.skip(parsimmon.whitespace)
.desc("ESCAPE"),
),
r.ValueExpression,
(a, b, c, d) => [b, a, c, d],
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression,
(a, b, c) => [b, a, c],
),
);
},
ExpressionList: function (r) {
return r.Expression.sepBy(
parsimmon.string(",").skip(parsimmon.optWhitespace),
);
},
Expression: function (r) {
function unary(
operatorsParser: parsimmon.Parser<string>,
nextParser: parsimmon.Parser<Expression>,
): parsimmon.Parser<Expression> {
return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
} }
return binaryLeft( if (!PATH_CHARS.has(char)) {
r.OrOperator, if (cur2.pos <= cur.pos) throw new Error("Invalid path");
binaryLeft(
r.AndOperator,
unary(r.NotOperator, r.Comparison.or(r.ValueExpression)),
),
).trim(parsimmon.optWhitespace);
},
});
export function parse(str: string): Expression { if (char === CHAR_COLON) {
if (!str) return null; if (colon) throw new Error("Multiple colons");
return lang.Expression.tryParse(str); colon = segments.length + 1;
}
if (exp) segments.push(exp);
else segments.push(cur.read(cur2));
if (char !== CHAR_PERIOD && char !== CHAR_COLON) break;
cur2.step();
cur.sync(cur2);
continue;
}
if (exp) throw new Error("Invalid path");
cur2.step();
}
cur.sync(cur2);
if (colon) colon = segments.length - colon;
return new Path(segments, colon);
} }
export function parseList(str: string): Expression[] { export function stringifyExpression(exp: Expression, level = 0): string {
if (!str) return []; function wrap(e: string, op: string): string {
return lang.ExpressionList.tryParse(str); if (PRECEDENCE[op] <= level) return `(${e})`;
}
export function stringify(exp: Expression, level = 0): string {
if (!Array.isArray(exp)) return JSON.stringify(exp);
const opLevels = {
OR: 10,
AND: 11,
NOT: 12,
"=": 20,
"<>": 20,
">": 20,
">=": 20,
"<": 20,
"<=": 20,
LIKE: 20,
"NOT LIKE": 20,
"IS NULL": 20,
"IS NOT NULL": 20,
"||": 30,
"+": 31,
"-": 31,
"*": 32,
"/": 32,
"%": 32,
};
const op = exp[0].toUpperCase();
function wrap(e): string {
if (opLevels[op] <= level) return `(${e})`;
else return e; else return e;
} }
if (op === "FUNC") { if (exp instanceof Expression.Literal) {
if (exp.value == null) return "NULL";
if (exp.value === true) return "TRUE";
if (exp.value === false) return "FALSE";
return JSON.stringify(exp.value);
} else if (exp instanceof Expression.Unary) {
if (exp.operator === "NOT") {
return wrap(
`NOT ${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])}`,
"NOT",
);
} else if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") {
return wrap(
`${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])} ${exp.operator}`,
exp.operator,
);
}
} else if (exp instanceof Expression.Binary) {
const op = exp.operator;
if (!(op in PRECEDENCE)) throw new Error("Invalid operator");
return wrap( return wrap(
`${exp[1]}(${exp `${stringifyExpression(exp.left, PRECEDENCE[op] - 1)} ${exp.operator} ${stringifyExpression(exp.right, PRECEDENCE[op])}`,
.slice(2) op,
.map((e) => stringify(e))
.join(", ")})`,
); );
} else if (op === "PARAM") { } else if (exp instanceof Expression.Parameter) {
if (typeof exp[1] === "string") { return exp.path.toString();
return wrap(exp[1]); } else if (exp instanceof Expression.FunctionCall) {
} else if (Array.isArray(exp[1]) && exp[1][0] === "||") { return `${exp.name}(${exp.args.map((a) => stringifyExpression(a)).join(", ")})`;
return wrap( } else if (exp instanceof Expression.Conditional) {
exp[1] let str = `CASE WHEN ${stringifyExpression(exp.condition)} THEN ${stringifyExpression(exp.then)}`;
.slice(1) if (
.map((p) => { exp.otherwise instanceof Expression.Literal &&
if (typeof p === "string") return p; exp.otherwise.value == null
else return `{${stringify(p)}}`; )
}) str += " END";
.join(""), else if (exp.otherwise instanceof Expression.Conditional)
); str += stringifyExpression(exp.otherwise).slice(4);
} else { else str += ` ELSE ${stringifyExpression(exp.otherwise)} END`;
return wrap(`{${stringify(exp[1])}}`); return str;
}
} else if (op === "IS NULL" || op === "IS NOT NULL") {
return wrap(`${stringify(exp[1], opLevels[op])} ${op}`);
} else if (op === "LIKE" || op === "NOT LIKE") {
if (exp[3]) {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op],
)} ESCAPE ${stringify(exp[3], opLevels[op])}`,
);
} else {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op],
)}`,
);
}
} else if (op === "CASE") {
const parts: string[] = ["CASE"];
for (let i = 1; i < exp.length - 1; i += 2) {
if (!Array.isArray(exp[i]) && exp[i]) {
if (exp[i + 1] != null) parts.push("ELSE", stringify(exp[i + 1]));
break;
}
parts.push("WHEN", stringify(exp[i]), "THEN", stringify(exp[i + 1]));
}
parts.push("END");
return parts.join(" ");
} else if (op in opLevels) {
const parts = exp.slice(1).map((e, i) => {
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
});
if (op === "NOT") return wrap(`${op} ${parts[0]}`);
else return wrap(parts.join(` ${op} `));
} else {
throw new Error(`Unrecognized operator ${exp[0]}`);
} }
throw new Error("Invalid expression");
} }
export function parseLikePattern(pat: string, esc: string): string[] { export function parseLikePattern(pat: string, esc: string): string[] {
@@ -457,3 +525,33 @@ export function parseLikePattern(pat: string, esc: string): string[] {
} }
return chars.filter((c) => c); return chars.filter((c) => c);
} }
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": ".",
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map((c) => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
+295 -374
View File
@@ -1,7 +1,7 @@
import { espresso, complement, tautology } from "espresso-iisojs"; import { espresso, complement, tautology } from "espresso-iisojs";
import { Expression } from "../../types.ts"; import Expression from "../expression.ts";
import { parseLikePattern } from "./parser.ts";
import normalize from "./normalize.ts"; import normalize from "./normalize.ts";
import { map, parseLikePattern } from "./parser.ts";
type Minterm = number[]; type Minterm = number[];
@@ -27,7 +27,7 @@ export abstract class SynthContextBase<
return this.clauses.get(v); return this.clauses.get(v);
} }
abstract getMinterms(exp: T, res: true | false | null): Minterm[]; abstract getMinterms(exp: T, res: number): Minterm[];
abstract getDcSet(minterms: Minterm[]): Minterm[]; abstract getDcSet(minterms: Minterm[]): Minterm[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
canRaise(idx: number, set: Set<number>): boolean { canRaise(idx: number, set: Set<number>): boolean {
@@ -67,36 +67,34 @@ export abstract class SynthContextBase<
} }
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> { function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
if (!Array.isArray(exp)) return; if (exp instanceof Expression.Literal) return;
const op = exp[0]; else if (exp instanceof Expression.Unary) {
if (op === "IS NULL" || op === "IS NOT NULL") return; if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") return;
yield* findIsNullDeps(exp.operand);
if (op === "FUNC") { } else if (exp instanceof Expression.Binary) {
if (exp[1] === "NOW") { yield* findIsNullDeps(exp.left);
return; yield* findIsNullDeps(exp.right);
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") { } else if (exp instanceof Expression.FunctionCall) {
yield* findIsNullDeps(exp[2]); if (exp.name === "NOW") return;
} else if (exp[1] === "ROUND") { else if (exp.name === "LOWER" || exp.name === "UPPER")
for (const e of exp.slice(2, 4)) yield* findIsNullDeps(e); yield* findIsNullDeps(exp.args[0]);
return; else if (exp.name === "ROUND") {
for (const e of exp.args.slice(0, 2)) yield* findIsNullDeps(e);
} }
} else if (op !== "PARAM") { } else yield exp;
for (const e of exp.slice(1)) yield* findIsNullDeps(e);
return;
}
yield exp;
} }
export abstract class Clause { export abstract class Clause {
private _isNullable: Set<string>; private _isNullable: Set<string>;
protected _expression: Expression; protected _expression: Expression;
abstract true(context: SynthContextBase<Clause>): Minterm[]; abstract getMinterms(
abstract false(context: SynthContextBase<Clause>): Minterm[]; context: SynthContextBase<Clause>,
abstract null(context: SynthContextBase<Clause>): Minterm[]; res: number,
): Minterm[];
expression(): Expression { expression(): Expression {
if (this._expression !== undefined) return this._expression; if (this._expression !== undefined) return this._expression;
const context = createSynthContext(); const context = createSynthContext();
const minterms = this.true(context); const minterms = this.getMinterms(context, 0b100);
const minimized = context.minimize(minterms); const minimized = context.minimize(minterms);
this._expression = context.toExpression(minimized) as Expression; this._expression = context.toExpression(minimized) as Expression;
return this._expression; return this._expression;
@@ -120,7 +118,7 @@ export abstract class Clause {
} }
} }
toString(): string { toString(): string {
return JSON.stringify(this.expression()); return this.expression().toString();
} }
} }
@@ -130,68 +128,11 @@ export namespace Clause {
constructor(public operand: Clause) { constructor(public operand: Clause) {
super(); super();
} }
true(context: SynthContextBase<Clause>): Minterm[] { getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
return this.operand.false(context); let r = res & 0b001;
} if (res & 0b010) r |= 0b100;
false(context: SynthContextBase<Clause>): Minterm[] { if (res & 0b100) r |= 0b010;
return this.operand.true(context); return this.operand.getMinterms(context, r);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.null(context);
}
}
export class Or extends Clause {
constructor(public operands: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
for (const s of this.operands) {
const m = s.true(context);
if (m.length === 1 && !m[0].length) return [[]];
minterms.push(...m);
}
return minterms;
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[][] = [];
const operands = [...this.operands];
for (const e of operands) {
if (e instanceof Or) {
operands.push(...e.operands);
continue;
}
const m = e.false(context);
if (!m.length) return [];
minterms.push(m);
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
null(context: SynthContextBase<Clause>): Minterm[] {
const trueMinterms: Minterm[] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof Or) {
operands.push(...s.operands);
continue;
}
const t = s.true(context);
if (t.length === 1 && !t[0].length) return [];
trueMinterms.push(...t);
}
const nullMinterms: Minterm[] = [];
for (const s of operands) {
const n = s.null(context);
if (n.length === 1 && !n[0].length && !trueMinterms.length) return [[]];
nullMinterms.push(...n);
}
if (!trueMinterms.length) return nullMinterms;
return complement([...complement(nullMinterms), ...trueMinterms]);
} }
} }
@@ -199,175 +140,43 @@ export namespace Clause {
constructor(public operands: Clause[]) { constructor(public operands: Clause[]) {
super(); super();
} }
true(context: SynthContextBase<Clause>): Minterm[] { getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
const minterms: Minterm[][] = []; res = res & 0b111;
const operands = [...this.operands]; if (!res) return [];
for (const s of operands) { if (res === 0b111) return [[]];
if (s instanceof And) {
operands.push(...s.operands); if (res === 0b110)
continue; return [
} ...this.getMinterms(context, 0b010),
const m = s.true(context); ...this.getMinterms(context, 0b100),
if (!m.length) return []; ];
if (m.length === 1 && !m[0].length) continue;
minterms.push(m); if (!(res & 0b010)) return complement(this.getMinterms(context, ~res));
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = []; const minterms: Minterm[] = [];
for (const s of this.operands) { for (const o of this.operands) {
const m = s.false(context); const m = o.getMinterms(context, res);
if (m.length === 1 && !m[0].length) return [[]]; if (m.length === 1 && !m[0].length) return [[]];
minterms.push(...m); minterms.push(...m);
} }
return minterms;
}
null(context: SynthContextBase<Clause>): Minterm[] {
const falseMinterms: Minterm[] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof And) {
operands.push(...s.operands);
continue;
}
const f = s.false(context);
if (f.length === 1 && !f[0].length) return [];
falseMinterms.push(...f);
}
const nullMinterms: Minterm[] = [];
for (const s of operands) {
const n = s.null(context);
if (n.length === 1 && !n[0].length && !falseMinterms.length)
return [[]];
nullMinterms.push(...n);
}
if (!falseMinterms.length) return nullMinterms;
return complement([...complement(nullMinterms), ...falseMinterms]);
}
}
export class Case extends Clause {
constructor(public clauses: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].true(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
if (i < this.clauses.length - 2) {
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
}
return minterms; return minterms;
} }
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].false(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
if (i < this.clauses.length - 2) {
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
}
return minterms;
}
null(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].null(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
minterms.push(...complement([...cumulative]));
return minterms;
}
expression(): Expression {
if (this._expression != null) return this._expression;
if (this.isBoolean()) {
this._expression = new Clause.Not(new Clause.Not(this)).expression();
return this._expression;
}
const context = createSynthContext();
const cases: { when: Minterm[]; then: Expression }[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
let minterms = this.clauses[i].true(context);
const then = this.clauses[i + 1].expression();
if (
cases.length &&
JSON.stringify(then) === JSON.stringify(cases[cases.length - 1].then)
)
minterms.push(...cases.pop().when);
minterms = context.minimize(
minterms,
cases.flatMap((c) => c.when),
);
if (!minterms.length) continue;
cases.push({ when: minterms, then });
if (minterms.length === 1 && !minterms[0].length) break;
}
while (cases[cases.length - 1].then == null) cases.pop();
if (!cases.length) return null;
this._expression = [
"CASE",
...cases.flatMap((c) => [context.toExpression(c.when), c.then]),
];
return this._expression;
}
isBoolean(): boolean {
for (let i = 1; i < this.clauses.length; i += 2)
if (!this.clauses[i].isBoolean()) return false;
return true;
}
} }
export class IsNull extends Clause { export class IsNull extends Clause {
private _boolean: Clause;
constructor(public operand: Clause) { constructor(public operand: Clause) {
super(); super();
this._boolean = new Not(new Not(this));
} }
true(context: SynthContextBase<Clause>): Minterm[] { getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
return this.operand.null(context); res = res & 0b111;
} const minterms: Minterm[] = [];
false(context: SynthContextBase<Clause>): Minterm[] { if ((res & 0b110) === 0b110) return [[]];
return complement(this.true(context)); if (res & 0b100)
} minterms.push(...this.operand.getMinterms(context, 0b001));
null(): Minterm[] { if (res & 0b010)
return []; minterms.push(...this.operand.getMinterms(context, 0b110));
return minterms;
} }
isBoolean(): boolean { isBoolean(): boolean {
return true; return true;
@@ -378,7 +187,7 @@ export namespace Clause {
expression(): Expression { expression(): Expression {
const nullables = [...this.operand.getNullables()]; const nullables = [...this.operand.getNullables()];
if (nullables.length === 1 && nullables[0].operand === this.operand) if (nullables.length === 1 && nullables[0].operand === this.operand)
return ["IS NULL", this.operand.expression()]; return new Expression.Unary("IS NULL", this.operand.expression());
return super.expression(); return super.expression();
} }
} }
@@ -387,26 +196,26 @@ export namespace Clause {
constructor(public exp: Expression) { constructor(public exp: Expression) {
super(); super();
} }
true(context: SynthContextBase<Clause>): Minterm[] { getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, true); if (!(this.exp instanceof Expression.Literal))
if (this.exp) return [[]]; return context.getMinterms(this, res);
return []; if (this.exp.value == null) return res & 0b001 ? [[]] : [];
} if (this.exp.value && res & 0b100) return [[]];
false(context: SynthContextBase<Clause>): Minterm[] { if (!this.exp.value && res & 0b010) return [[]];
if (Array.isArray(this.exp)) return context.getMinterms(this, false);
if (!this.exp && this.exp != null) return [[]];
return [];
}
null(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, null);
if (this.exp == null) return [[]];
return []; return [];
} }
expression(): Expression { expression(): Expression {
return this.exp; return this.exp;
} }
isBoolean(): boolean { isBoolean(): boolean {
return this.exp === true || this.exp === false || this.exp == null; if (this.exp instanceof Expression.Literal) {
return (
this.exp.value === true ||
this.exp.value === false ||
this.exp.value == null
);
}
return false;
} }
} }
@@ -418,14 +227,13 @@ export namespace Clause {
) { ) {
super(); super();
} }
true(context: SynthContextBase<Clause>): Minterm[] { getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
return context.getMinterms(this, true); res = res & 0b111;
} if (!res) return [];
false(context: SynthContextBase<Clause>): Minterm[] { if (res === 0b111) return [[]];
return context.getMinterms(this, false); if (res === 0b001 || res === 0b110)
} return this.lhs.getMinterms(context, res);
null(context: SynthContextBase<Clause>): Minterm[] { return context.getMinterms(this, res);
return this.lhs.null(context);
} }
*getNullables(): IterableIterator<IsNull> { *getNullables(): IterableIterator<IsNull> {
yield* this.lhs.getNullables(); yield* this.lhs.getNullables();
@@ -434,7 +242,11 @@ export namespace Clause {
return true; return true;
} }
expression(): Expression { expression(): Expression {
return [this.op, this.lhs.expression(), this.rhs]; return new Expression.Binary(
this.op,
this.lhs.expression(),
new Expression.Literal(this.rhs),
);
} }
} }
@@ -453,15 +265,13 @@ export namespace Clause {
const exp = lhs.expression(); const exp = lhs.expression();
let caseSensitive = true; let caseSensitive = true;
let contradiction = false; let contradiction = false;
if (Array.isArray(exp)) { if (exp instanceof Expression.FunctionCall) {
if (exp[0] === "FUNC") { if (exp.name === "UPPER" || exp.name === "LOWER") {
const func = exp[1]; const p =
if (func === "UPPER" || func === "LOWER") { exp.name === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
const p = func === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase(); if (p === rhs) caseSensitive = false;
if (p === rhs) caseSensitive = false; else contradiction = true;
else contradiction = true; lhs = new Exp(exp.args[0]);
lhs = new Exp(exp[2]);
}
} }
} }
this.lhs = lhs; this.lhs = lhs;
@@ -469,14 +279,13 @@ export namespace Clause {
this.caseSensitive = caseSensitive; this.caseSensitive = caseSensitive;
this.contradiction = contradiction; this.contradiction = contradiction;
} }
true(context: SynthContextBase<Clause>): Minterm[] { getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
return context.getMinterms(this, true); res = res & 0b111;
} if (!res) return [];
false(context: SynthContextBase<Clause>): Minterm[] { if (res === 0b111) return [[]];
return context.getMinterms(this, false); if (res === 0b001 || res === 0b110)
} return this.lhs.getMinterms(context, res);
null(context: SynthContextBase<Clause>): Minterm[] { return context.getMinterms(this, res);
return this.lhs.null(context);
} }
isBoolean(): boolean { isBoolean(): boolean {
return true; return true;
@@ -491,67 +300,157 @@ export namespace Clause {
let lhs = this.lhs.expression(); let lhs = this.lhs.expression();
if (this.contradiction) { if (this.contradiction) {
if (this.rhs === this.rhs.toLocaleUpperCase()) if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "LOWER", lhs]; lhs = new Expression.FunctionCall("LOWER", [lhs]);
else lhs = ["FUNC", "UPPER", lhs]; else lhs = new Expression.FunctionCall("UPPER", [lhs]);
} else if (!this.caseSensitive) { } else if (!this.caseSensitive) {
if (this.rhs === this.rhs.toLocaleUpperCase()) if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "UPPER", lhs]; lhs = new Expression.FunctionCall("UPPER", [lhs]);
else lhs = ["FUNC", "LOWER", lhs]; else lhs = new Expression.FunctionCall("LOWER", [lhs]);
} }
return ["LIKE", lhs, this.rhs, ...(this.esc ? [this.esc] : [])]; return new Expression.Binary(
"LIKE",
lhs,
new Expression.Literal(this.rhs),
);
}
}
export class Conditional extends Clause {
constructor(
public condition: Clause,
public then: Clause,
public otherwise: Clause,
) {
super();
}
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
const condition = this.condition.getMinterms(context, 0b011);
if (!condition.length) return this.then.getMinterms(context, res);
return [
...complement([...condition, ...this.then.getMinterms(context, ~res)]),
...complement([
...complement(condition),
...this.otherwise.getMinterms(context, ~res),
]),
];
}
expression(): Expression {
if (this._expression != null) return this._expression;
if (this.isBoolean()) {
this._expression = new Clause.Not(new Clause.Not(this)).expression();
return this._expression;
}
const context = createSynthContext();
const cases: { when: Minterm[]; then: Expression }[] = [];
let clause = this as Conditional;
for (;;) {
let minterms = clause.condition.getMinterms(context, 0b100);
const then = clause.then.expression();
if (
cases.length &&
then.toString() === cases[cases.length - 1].then.toString()
)
minterms.push(...cases.pop().when);
minterms = context.minimize(
minterms,
cases.flatMap((c) => c.when),
);
if (!minterms.length) continue;
cases.push({ when: minterms, then });
if (minterms.length === 1 && !minterms[0].length) break;
if (clause.otherwise instanceof Conditional) clause = clause.otherwise;
else
clause = new Conditional(
new Exp(new Expression.Literal(true)),
clause.otherwise,
new Exp(new Expression.Literal(null)),
);
}
while (
cases[cases.length - 1].then instanceof Expression.Literal &&
(cases[cases.length - 1].then as Expression.Literal).value == null
)
cases.pop();
let res: Expression = new Expression.Literal(null);
while (cases.length) {
const c = cases.pop();
if (c.when.length === 1 && !c.when[0].length) {
res = c.then;
} else {
res = new Expression.Conditional(
context.toExpression(c.when),
c.then,
res,
);
}
}
this._expression = res;
return this._expression;
}
isBoolean(): boolean {
return this.then.isBoolean() && this.otherwise.isBoolean();
} }
} }
export function fromExpression(exp: Expression): Clause { export function fromExpression(exp: Expression): Clause {
const res = map(exp, (e) => { let res: Clause;
if (!Array.isArray(e)) return new Clause.Exp(e); let negate = false;
let op = e[0]; if (exp instanceof Expression.Unary) {
let negate = true; let op = exp.operator;
negate = true;
if (op === "IS NOT NULL") op = "IS NULL";
else negate = false;
if (op === "NOT") res = new Clause.Not(fromExpression(exp.operand));
else if (op === "IS NULL") res = new IsNull(fromExpression(exp.operand));
} else if (exp instanceof Expression.Binary) {
let op = exp.operator;
negate = true;
if (op === "NOT LIKE") op = "LIKE"; if (op === "NOT LIKE") op = "LIKE";
else if (op === "IS NOT NULL") op = "IS NULL";
else if (op === "<>") op = "="; else if (op === "<>") op = "=";
else if (op === ">=") op = "<"; else if (op === ">=") op = "<";
else if (op === "<=") op = ">"; else if (op === "<=") op = ">";
else negate = false; else negate = false;
let clause: Clause; if (op === "AND") {
if (op === "IS NULL") { res = new Clause.And([
clause = new Clause.IsNull(e[1]); fromExpression(exp.left),
} else if (op === "NOT") { fromExpression(exp.right),
clause = new Clause.Not(e[1]); ]);
} else if (op === "OR") { } else if (op === "OR") {
clause = new Clause.Or(e.slice(1)); negate = true;
} else if (op === "AND") { res = new Clause.And([
clause = new Clause.And(e.slice(1)); new Clause.Not(fromExpression(exp.left)),
} else if (op === "CASE") { new Clause.Not(fromExpression(exp.right)),
clause = new Clause.Case(e.slice(1)); ]);
} else if (op === "LIKE") { } else if (exp.right instanceof Expression.Literal) {
const rhs = e[2] instanceof Exp ? e[2].expression() : null; if (["=", ">", "<"].includes(op)) {
const esc = e[3] instanceof Exp ? e[3].expression() : null; if (
if (typeof rhs === "string" && typeof esc === "string") ["boolean", "number", "string"].includes(typeof exp.right.value)
clause = new Clause.Like(e[1], rhs, esc); ) {
else if (typeof rhs === "string" && esc == null) res = new Compare(
clause = new Clause.Like(e[1], rhs); fromExpression(exp.left),
} else if (op === ">" || op === "<" || op === "=") { op as ">" | "<" | "=",
const rhs = e[2] instanceof Exp ? e[2].expression() : null; exp.right.value,
if (["boolean", "number", "string"].includes(typeof rhs)) { );
clause = new Clause.Compare( }
e[1], } else if (op === "LIKE") {
op, if (typeof exp.right.value === "string")
rhs as boolean | number | string, res = new Like(fromExpression(exp.left), exp.right.value);
);
} }
} }
} else if (exp instanceof Expression.Conditional) {
res = new Conditional(
fromExpression(exp.condition),
fromExpression(exp.then),
fromExpression(exp.otherwise),
);
}
if (!clause) { if (!res) res = new Exp(exp);
const args = e.slice(1).map((a) => (a as Clause).expression()); if (negate) res = new Not(res);
clause = new Clause.Exp([op, ...args]);
}
if (negate) clause = new Clause.Not(clause);
return clause;
});
return res; return res;
} }
} }
@@ -576,11 +475,24 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
super(); super();
} }
getMinterms(clause: Clause, res: true | false | null): number[][] { getMinterms(clause: Clause, res: number): number[][] {
const v = this.getVar(clause); const v = this.getVar(clause);
if (res === true) return [[(v << 2) ^ 3]]; switch (res & 0b111) {
else if (res === false) return [[(v << 2) ^ 1]]; case 0b100:
else return [[v << 2, (v << 2) ^ 2]]; return [[(v << 2) ^ 3]];
case 0b010:
return [[(v << 2) ^ 1]];
case 0b001:
return [[v << 2, (v << 2) ^ 2]];
case 0b110:
return [[(v << 2) ^ 3], [(v << 2) ^ 1]];
case 0b101:
return [[(v << 2) ^ 3], [v << 2, (v << 2) ^ 2]];
case 0b011:
return [[(v << 2) ^ 1], [v << 2, (v << 2) ^ 2]];
default:
throw new Error("Invalid minterms");
}
} }
getDcSet(minterms: Minterm[]): number[][] { getDcSet(minterms: Minterm[]): number[][] {
@@ -594,9 +506,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
) as Clause.Compare[]; ) as Clause.Compare[];
// Comparisons // Comparisons
for (const [, clauses] of groupBy(comparisons, (c) => for (const [, clauses] of groupBy(comparisons, (c) => c.lhs.toString())) {
JSON.stringify(c.lhs),
)) {
const lhs = clauses[0].lhs; const lhs = clauses[0].lhs;
const values = new Set(clauses.map((c) => c.rhs)); const values = new Set(clauses.map((c) => c.rhs));
const valuesSorted = [...values].sort((a, b) => { const valuesSorted = [...values].sort((a, b) => {
@@ -657,7 +567,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
(c) => c instanceof Clause.Like, (c) => c instanceof Clause.Like,
) as Clause.Like[]; ) as Clause.Like[];
for (const [, clauses] of groupBy(likes, (c) => JSON.stringify(c.lhs))) { for (const [, clauses] of groupBy(likes, (c) => c.lhs.toString())) {
for (let i1 = 0; i1 < clauses.length; ++i1) { for (let i1 = 0; i1 < clauses.length; ++i1) {
const l1 = clauses[i1]; const l1 = clauses[i1];
if (l1.contradiction) { if (l1.contradiction) {
@@ -724,9 +634,13 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
} }
if (clause instanceof Clause.IsNull) { if (clause instanceof Clause.IsNull) {
const str = clause.operand.toString(); if (clause.operand instanceof Clause.Exp) {
if (str === '["PARAM","DeviceID.ID"]' || str === '["PARAM","_id"]') if (clause.operand.exp instanceof Expression.Parameter) {
dcSet.push([(v << 2) ^ 3]); const str = clause.operand.exp.path.toString();
if (str === "DeviceID.ID" || str === "_id")
dcSet.push([(v << 2) ^ 3]);
}
}
} }
} }
return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2))); return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2)));
@@ -808,11 +722,9 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
} }
toExpression(sop: number[][]): Expression { toExpression(sop: number[][]): Expression {
if (!sop.length) return false; let res: Expression = new Expression.Literal(false);
const res: Expression[] = [];
for (const s of sop) { for (const s of sop) {
if (!s.length) return true; let conjs: Expression = new Expression.Literal(true);
const conjs: Expression[] = [];
for (const i of s) { for (const i of s) {
const clause = this.getClause(i >>> 2); const clause = this.getClause(i >>> 2);
if (!clause) throw new Error("Invalid literal"); if (!clause) throw new Error("Invalid literal");
@@ -823,7 +735,8 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
const isNullVars = [...clause.getNullables()].map((c) => const isNullVars = [...clause.getNullables()].map((c) =>
this.getVar(c), this.getVar(c),
); );
conjs.push( conjs = Expression.and(
conjs,
this.toExpression([ this.toExpression([
[i ^ 3], [i ^ 3],
...isNullVars.map((n) => [(n << 2) ^ 3]), ...isNullVars.map((n) => [(n << 2) ^ 3]),
@@ -832,31 +745,38 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
continue; continue;
} }
let expr = clause.expression() as Expression; let expr = clause.expression();
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr]; if (!(i & 1) !== !(i & 2)) expr = new Expression.Unary("NOT", expr);
if ( if (expr instanceof Expression.Unary && expr.operator === "NOT") {
Array.isArray(expr) && const e = expr.operand;
expr[0] === "NOT" && if (e instanceof Expression.Unary) {
Array.isArray(expr[1]) if (e.operator === "IS NULL")
) { expr = new Expression.Unary("IS NOT NULL", e.operand);
const e: Expression[] = expr[1]; else if (e.operator === "IS NOT NULL")
if (e[0] === "IS NULL") expr = ["IS NOT NULL", ...e.slice(1)]; expr = new Expression.Unary("IS NULL", e.operand);
else if (e[0] === "LIKE") expr = ["NOT LIKE", ...e.slice(1)]; else if (e.operator === "NOT") expr = e.operand;
else if (e[0] === "=") expr = ["<>", ...e.slice(1)]; } else if (e instanceof Expression.Binary) {
else if (e[0] === "<>") expr = ["=", ...e.slice(1)]; if (e.operator === "LIKE")
else if (e[0] === ">") expr = ["<=", ...e.slice(1)]; expr = new Expression.Binary("NOT LIKE", e.left, e.right);
else if (e[0] === ">=") expr = ["<", ...e.slice(1)]; else if (e.operator === "=")
else if (e[0] === "<") expr = [">=", ...e.slice(1)]; expr = new Expression.Binary("<>", e.left, e.right);
else if (e[0] === "<=") expr = [">", ...e.slice(1)]; else if (e.operator === "<>")
else if (e[0] === "NOT") expr = e[1]; expr = new Expression.Binary("=", e.left, e.right);
else if (e.operator === ">")
expr = new Expression.Binary("<=", e.left, e.right);
else if (e.operator === ">=")
expr = new Expression.Binary("<", e.left, e.right);
else if (e.operator === "<")
expr = new Expression.Binary(">=", e.left, e.right);
else if (e.operator === "<=")
expr = new Expression.Binary(">", e.left, e.right);
}
} }
conjs.push(expr); conjs = Expression.and(conjs, expr);
} }
if (conjs.length > 1) res.push(["AND", ...conjs]); res = Expression.or(res, conjs);
else res.push(conjs[0]);
} }
if (res.length > 1) return ["OR", ...res]; return res;
return res[0];
} }
} }
@@ -925,11 +845,12 @@ export function unionDiff(
): [Expression, Expression] { ): [Expression, Expression] {
expr2 = normalize(expr2); expr2 = normalize(expr2);
if (!expr2) return [expr1, false]; if (expr2 instanceof Expression.Literal && !expr2.value)
return [expr1, new Expression.Literal(false)];
const synth2 = Clause.fromExpression(expr2); const synth2 = Clause.fromExpression(expr2);
if (!expr1) { if (expr1 instanceof Expression.Literal && !expr1.value) {
const e = synth2.expression(); const e = synth2.expression();
return [e, e]; return [e, e];
} }
@@ -939,8 +860,8 @@ export function unionDiff(
const context = new SynthContext(); const context = new SynthContext();
const expr2Minterms = synth2.true(context); const expr2Minterms = synth2.getMinterms(context, 0b100);
const expr1Minterms = synth1.true(context); const expr1Minterms = synth1.getMinterms(context, 0b100);
const union = context.minimize([...expr1Minterms, ...expr2Minterms]); const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
@@ -953,16 +874,16 @@ export function unionDiff(
export function covers(expr1: Expression, expr2: Expression): boolean { export function covers(expr1: Expression, expr2: Expression): boolean {
expr2 = normalize(expr2); expr2 = normalize(expr2);
if (!expr2) return true; if (expr2 instanceof Expression.Literal && !expr2.value) return true;
expr1 = normalize(expr1); expr1 = normalize(expr1);
if (!Array.isArray(expr1) && expr1) return true; if (expr1 instanceof Expression.Literal && expr1.value) return true;
const synt1 = Clause.fromExpression(expr1); const synt1 = Clause.fromExpression(expr1);
const synt2 = Clause.fromExpression(expr2); const synt2 = Clause.fromExpression(expr2);
const context = new SynthContext(); const context = new SynthContext();
const expr1Minterms = synt1.true(context); const expr1Minterms = synt1.getMinterms(context, 0b100);
const expr2Minterms = synt2.true(context); const expr2Minterms = synt2.getMinterms(context, 0b100);
return tautology([ return tautology([
...context.sanitizeMinterms(complement(expr2Minterms)), ...context.sanitizeMinterms(complement(expr2Minterms)),
-393
View File
@@ -1,393 +0,0 @@
import { map, mapAsync, parseLikePattern } from "./parser.ts";
import { Expression } from "../../types.ts";
const isArray = Array.isArray;
const regExpCache = new WeakMap<any, RegExp>();
const REDUCE_SKIP = {};
function reduce(exp, callback): Expression {
let loop = true;
while (loop) {
loop = false;
for (let i = 2; i < exp.length; ++i) {
const res = callback(exp[i - 1], exp[i], i - 2);
if (res !== REDUCE_SKIP) {
loop = true;
exp = exp.slice();
exp.splice(i - 1, 2, res);
}
}
}
if (exp.length === 2) return exp[1];
return exp;
}
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": ".",
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map((c) => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
function compare(
a: boolean | number | string,
b: boolean | number | string,
): number {
if (typeof a === "boolean") a = +a;
if (typeof b === "boolean") b = +b;
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
return a > b ? 1 : a < b ? -1 : 0;
}
function toNumber(a: boolean | number | string): number {
switch (typeof a) {
case "number":
return a;
case "boolean":
return +a;
case "string":
return parseFloat(a) || 0;
}
}
function toString(a: boolean | number | string): string {
switch (typeof a) {
case "string":
return a;
case "number":
return a.toString();
case "boolean":
return (+a).toString();
}
}
export function evaluateCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "CASE") {
for (let i = 1; i < exp.length; i += 2) {
if (Array.isArray(exp[i])) return exp;
if (exp[i]) return exp[i + 1];
}
return null;
} else if (exp[0] === "FUNC") {
if (exp[1] === "COALESCE") {
const args: Expression[] = [];
for (let i = 2; i < exp.length; ++i) {
const e = exp[i];
if (e == null) continue;
args.push(e);
if (!Array.isArray(e)) break;
}
if (!args.length) return null;
if (args.length === 1) return args[0];
return ["FUNC", "COALESCE", ...args];
} else if (exp[1] === "UPPER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toUpperCase();
} else if (exp[1] === "LOWER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
} else if (exp[1] === "ROUND") {
const n = exp[2];
const p = exp.length > 3 ? exp[3] : 0;
if (n == null || p == null) return null;
if (!isArray(n) && !isArray(p)) {
const d = 10 ** p;
const m = n * d * (1 + Number.EPSILON);
return Math.round(m) / d;
}
}
} else if (exp[0] === "PARAM") {
if (exp[1] == null) return null;
} else if (exp[0] === "AND") {
for (let i = 1; i < exp.length; ++i)
if (!Array.isArray(exp[i]) && exp[i] != null && !exp[i]) return false;
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (ee == null) return null;
if (Array.isArray(ee)) {
if (ee[0] === "AND") args.push(...ee.slice(1));
else args.push(ee);
}
}
if (!args.length) return true;
if (args.length === 1) args.push(true);
return ["AND", ...args];
} else if (exp[0] === "OR") {
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (Array.isArray(ee)) {
if (ee[0] === "OR") args.push(...ee.slice(1));
else args.push(ee);
} else if (ee) {
return true;
}
}
if (!args.length) return exp.some((ee) => ee == null) ? null : false;
if (args.length === 1) args.push(false);
return ["OR", ...args];
} else if (exp[0] === "NOT") {
if (exp[1] == null) return null;
if (!isArray(exp[1])) return !exp[1];
else if (exp[1][0] === "NOT") return exp[1][1];
} else if (exp[0] === "IS NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] == null) return true;
else return false;
} else if (exp[0] === "IS NOT NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] != null) return true;
else return false;
} else if (exp[0] === "LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if (
exp[1] == null ||
exp[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null)
)
return null;
let r = regExpCache.get(exp as any[]);
if (!r) {
r = likePatternToRegExp(exp[2], exp[3]);
regExpCache.set(exp as any[], r);
}
return r.test(exp[1]);
} else if (exp[0] === "NOT LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if (
exp[1] == null ||
exp[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null)
)
return null;
let r = regExpCache.get(exp as any[]);
if (!r) {
r = likePatternToRegExp(exp[2], exp[3]);
regExpCache.set(exp as any[], r);
}
return !r.test(exp[1]);
} else if (exp[0] === "=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) === 0;
} else if (exp[0] === "<>") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) !== 0;
} else if (exp[0] === ">") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) > 0;
} else if (exp[0] === ">=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) >= 0;
} else if (exp[0] === "<") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) < 0;
} else if (exp[0] === "<=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) <= 0;
} else if (exp[0] === "*") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) * toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "/") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = toNumber(b);
if (i !== 0) return n1 * n2;
if (n2 === 0) return null;
return n1 / n2;
});
} else if (exp[0] === "+") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) + toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "-") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b))
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "%") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = Math.trunc(toNumber(b));
if (n2 === 0) return null;
return n1 % n2;
});
} else if (exp[0] === "||") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toString(a) + toString(b);
return REDUCE_SKIP;
});
}
return exp;
}
export function evaluate(
exp: Expression,
obj: Record<string, unknown> | ((e: string) => Expression),
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression,
): Expression;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression,
): Expression {
return map(exp, (e) => {
if (cb) e = cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC" && e[1] === "NOW") {
if (now) return now;
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v;
if (typeof obj === "function") v = obj(e[1]);
else v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v.value ? v.value[0] : null;
return v;
}
}
return evaluateCallback(e);
});
}
export async function evaluateAsync(
exp: Expression,
obj: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<string | number | boolean | null>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<Expression>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<Expression> {
return mapAsync(exp, async (e) => {
if (cb) e = await cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC") {
if (e[1] === "NOW") if (now) return now;
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v["value"] ? v["value"][0] : null;
return v as Expression;
}
}
return evaluateCallback(e);
});
}
export function and(exp1: Expression, exp2: Expression): Expression {
if (exp1 != null && !exp1) return false;
if (exp2 != null && !exp2) return false;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return true;
}
if (!Array.isArray(exp2) && exp1[0] === "AND") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "AND") return exp2;
const res: Expression = ["AND"];
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1));
else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1));
else res.push(exp2);
return res;
}
export function or(exp1: Expression, exp2: Expression): Expression {
if (!Array.isArray(exp1) && exp1) return true;
if (!Array.isArray(exp2) && exp2) return true;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return false;
}
if (!Array.isArray(exp2) && exp1[0] === "OR") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "OR") return exp2;
const res: Expression = ["OR"];
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1));
else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1));
else res.push(exp2);
return res;
}
export function extractParams(exp: Expression): Expression[] {
const params = new Set<Expression>();
map(exp, (e) => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
return e;
});
return Array.from(params);
}
+69 -65
View File
@@ -1,40 +1,32 @@
import Path from "./path.ts"; import Path from "./path.ts";
export default class PathSet { export default class PathSet {
declare private lengthIndex: Set<Path>[]; private paramSegmentIndex: Map<string, Set<Path>>[] = [];
declare private fragmentIndex: Map<string, Set<Path>>[]; private attrSegmentIndex: Map<string, Set<Path>>[] = [];
declare private stringIndex: Map<string, Path>; private stringIndex: Map<string, Path> = new Map();
public constructor() { public constructor() {}
this.lengthIndex = [];
this.fragmentIndex = [];
this.stringIndex = new Map();
}
public get depth(): number { public add(pathStr: string): Path {
return this.lengthIndex.length; let path: Path = this.get(pathStr);
} if (path) return path;
path = Path.parse(pathStr);
public add(path: Path): Path {
if (path.alias) throw new Error("PathSet does not support aliased paths"); if (path.alias) throw new Error("PathSet does not support aliased paths");
const p = this.get(path);
if (p) return p;
this.stringIndex.set(path.toString(), path); this.stringIndex.set(path.toString(), path);
while (this.lengthIndex.length <= path.length) { while (this.paramSegmentIndex.length < path.paramLength)
this.lengthIndex.push(new Set()); this.paramSegmentIndex.push(new Map());
// fragmentIndex is one less than lengthIndex
if (this.lengthIndex.length > 1) this.fragmentIndex.push(new Map());
}
const lengthIndex = this.lengthIndex[path.length]; while (this.attrSegmentIndex.length < path.attrLength)
lengthIndex.add(path); this.attrSegmentIndex.push(new Map());
for (let i = 0; i < path.length; ++i) { for (let i = 0; i < path.length; ++i) {
const fragment = path.segments[i] as string; const fragment = path.segments[i] as string;
const fragmentIndex = this.fragmentIndex[i]; const fragmentIndex =
i < path.paramLength
? this.paramSegmentIndex[i]
: this.attrSegmentIndex[i - path.paramLength];
let fragmentIndexSet = fragmentIndex.get(fragment); let fragmentIndexSet = fragmentIndex.get(fragment);
if (!fragmentIndexSet) { if (!fragmentIndexSet) {
@@ -48,64 +40,76 @@ export default class PathSet {
return path; return path;
} }
public get(path: Path): Path { public get(path: string): Path {
return this.stringIndex.get(path.toString()) || null; return this.stringIndex.get(path);
} }
public find( public findCompat(
path: Path, path: Path,
superset = false, superset = false,
subset = false, subset = false,
depth: number = path.length, depth = path.length,
): Path[] { ): Path[] {
if (path.attrLength)
throw new Error("findCompat() does not support attribute paths");
depth = Math.min(31, depth);
const paramMask = (1 << path.paramLength) - 1;
let mask = ((0b10 << depth) - 1) & ~paramMask;
if (superset) mask |= ~path.wildcard & paramMask;
if (subset) mask |= path.wildcard;
return this.find(path, mask, 1);
}
public find(path: Path, paramMask: number, attrMask: number): Path[] {
if (path.alias) throw new Error("PathSet does not support aliased paths"); if (path.alias) throw new Error("PathSet does not support aliased paths");
if (path.paramLength > this.paramSegmentIndex.length) return [];
if (path.attrLength > this.attrSegmentIndex.length) return [];
const len = path.length; const emptySet: Set<Path> = new Set();
if (!superset && depth === len && (!subset || !path.wildcard)) { const indexes: [Set<Path>, Set<Path>][] = [];
const p = this.get(path);
return p ? [p] : []; const paramLengthMask = (1 << path.paramLength) - 1;
const attrLengthMask = (1 << path.attrLength) - 1;
const mask = (paramMask & paramLengthMask) | (attrMask << path.paramLength);
for (const [i, s] of path.segments.entries()) {
const b = 1 << i;
const m = mask & b;
const w = b & path.wildcard;
if (w && m) continue;
const idxSet =
i < path.paramLength
? this.paramSegmentIndex[i]
: this.attrSegmentIndex[i - path.paramLength];
const idx1 = idxSet.get(s as string) || emptySet;
let idx2 = emptySet;
if (m) idx2 = idxSet.get("*") || emptySet;
indexes.push([idx1, idx2]);
} }
const lengthIndex = this.lengthIndex.slice(len, depth + 1); indexes.sort((a, b) => a[0].size + a[1].size - (b[0].size + b[1].size));
if (!lengthIndex.length) return [];
let res; let res: Path[];
for (let i = len - 1; i >= 0; --i) {
let fragmentIndexSet2;
const fragmentIndex = this.fragmentIndex[i];
if ((path.wildcard >> i) & 1) { if (!indexes.length) res = [...this.stringIndex.values()];
if (subset) continue; else res = [...indexes[0][0], ...indexes[0][1]];
} else if (superset) {
fragmentIndexSet2 = fragmentIndex.get("*");
}
const fragment = path.segments[i] as string; const paramCover = ~paramLengthMask & paramMask;
const fragmentIndexSet1 = fragmentIndex.get(fragment); const attrCover = ~attrLengthMask & attrMask;
if (!fragmentIndexSet1) { res = res.filter(
if (!fragmentIndexSet2) return []; (p) =>
if (!res) res = [...fragmentIndexSet2]; (1 << p.paramLength) & paramCover && (1 << p.attrLength) & attrCover,
else res = res.filter((r) => fragmentIndexSet2.has(r)); );
} else if (!fragmentIndexSet2) {
if (!res) res = [...fragmentIndexSet1]; for (let i = 1; i < indexes.length; ++i) {
else res = res.filter((r) => fragmentIndexSet1.has(r)); const [idx1, idx2] = indexes[i];
} else { res = res.filter((p) => idx1.has(p) || idx2.has(p));
if (!res) {
res = [...fragmentIndexSet1, ...fragmentIndexSet2];
} else {
res = res.filter(
(r) => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r),
);
}
}
if (!res.length) return res;
} }
if (!res) res = [].concat(...lengthIndex.map((a) => [...a]));
else res = res.filter((r) => lengthIndex.some((a) => a.has(r)));
return res; return res;
} }
} }
+75 -170
View File
@@ -1,162 +1,31 @@
type Segments = (string | Alias)[]; import Expression from "./expression.ts";
type Alias = [Path, string][]; import { Cursor, parsePath } from "./expression/parser.ts";
type Segments = (string | Expression)[];
let cache1 = new Map<string, Path>(); let cache1 = new Map<string, Path>();
let cache2 = new Map<string, Path>(); let cache2 = new Map<string, Path>();
const CHAR_SPACE = 32;
const CHAR_BACKSLASH = 92;
const CHAR_COLON = 58;
const CHAR_QUOTE = 34;
const CHAR_OPEN_BRACKET = 91;
const CHAR_CLOSE_BRACKET = 93;
const CHAR_ASTERISK = 42;
const CHAR_COMMA = 44;
const CHAR_DOT = 46;
function charCodeAt(str: string, index: number): number {
if (index >= str.length) return 0;
return str.charCodeAt(index);
}
function legalChar(c: number): boolean {
return (
(c >= 97 && c <= 122) ||
(c >= 65 && c <= 90) ||
(c >= 48 && c <= 57) ||
c === 95 ||
c === 45
);
}
export default class Path { export default class Path {
declare public readonly segments: Segments; declare public readonly segments: Segments;
declare public readonly colon: number;
declare public readonly wildcard: number; declare public readonly wildcard: number;
declare public readonly alias: number; declare public readonly alias: number;
declare protected _string: string; declare protected _string: string;
declare protected _stringIndex: number[]; declare protected _stringIndex: number[];
protected static parseAliasValue( constructor(segments: Segments, colon: number) {
pattern: string, if (!(colon <= segments.length)) throw new Error("Invalid path");
index: number, if (segments.length > 32) throw new Error("Path too long");
): { index: number; value: string } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
for (let j = i + 1; j < pattern.length; ++j) {
if (
pattern.charCodeAt(j) === CHAR_QUOTE &&
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
) {
try {
++j;
const v = JSON.parse(pattern.slice(i, j));
return { index: j, value: v };
} catch {
return { index, value: null };
}
}
}
return { index, value: null };
}
for (; i < pattern.length; ++i) {
const c = pattern.charCodeAt(i);
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
}
return { index: i, value: pattern.slice(index, i).trim() };
}
protected static parseAliasPath(
pattern: string,
index: number,
): { index: number; path: Path } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
const { index: idx, segments } = Path.parsePath(pattern, i);
if (idx === i) return { index, path: null };
i = idx;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
return { index: i + 1, path: new Path(segments) };
}
protected static parseAlias(
pattern: string,
index: number,
): { index: number; alias: Alias } {
const alias: Alias = [];
let i = index;
for (;;) {
let path: Path;
let value: string;
({ index: i, path } = Path.parseAliasPath(pattern, i));
if (!path) break;
({ index: i, value } = Path.parseAliasValue(pattern, i));
if (value == null) break;
alias.push([path, value]);
index = i;
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
++i;
}
// Ensure identical expressions have idential string representation
alias.sort((a, b) => {
if (a[0].toString() > b[0].toString()) return 1;
else if (a[0].toString() < b[0].toString()) return -1;
else if (a[1] > b[1]) return 1;
else if (a[1] < b[1]) return -1;
else return 0;
});
Object.freeze(alias);
return { index, alias };
}
protected static parsePath(
pattern: string,
index: number,
): { index: number; segments: Segments } {
const segments = [];
let segStart = index;
let wildcard = -1;
for (let i = index; i <= pattern.length; ++i) {
const c = charCodeAt(pattern, i);
if (c === CHAR_OPEN_BRACKET && i === segStart) {
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
segments.push(alias);
index = i = idx + 1;
segStart = i + 1;
} else if (c === CHAR_ASTERISK && i === segStart) {
wildcard = i + 1;
} else if (!legalChar(c) || i === wildcard) {
if (i === segStart) break;
const s = pattern.slice(segStart, i);
segments.push(s);
index = i;
if (c !== CHAR_DOT) break;
segStart = i + 1;
}
}
Object.freeze(segments); Object.freeze(segments);
return { index, segments };
}
protected constructor(segments: Segments) {
let alias = 0; let alias = 0;
let wildcard = 0; let wildcard = 0;
const arr = segments.map((s, i) => { const arr = segments.map((s, i) => {
if (Array.isArray(s)) { if (s instanceof Expression) {
alias |= 1 << i; alias |= 1 << i;
const parts = s.map( return `[${s.toString()}]`;
(al) => `${al[0].toString()}:${JSON.stringify(al[1])}`,
);
return `[${parts.join(",")}]`;
} else if (s === "*") { } else if (s === "*") {
wildcard |= 1 << i; wildcard |= 1 << i;
} }
@@ -167,23 +36,27 @@ export default class Path {
const stringIndex = arr.map((s, i) => (offset += s.length) + i); const stringIndex = arr.map((s, i) => (offset += s.length) + i);
this.segments = segments; this.segments = segments;
this.colon = colon;
this.wildcard = wildcard; this.wildcard = wildcard;
this.alias = alias; this.alias = alias;
this._string = arr.join("."); if (!colon) this._string = arr.join(".");
else
this._string =
arr.slice(0, -colon).join(".") + ":" + arr.slice(-colon).join(".");
this._stringIndex = stringIndex; this._stringIndex = stringIndex;
} }
public static parse(str: string): Path { public static parse(input: string): Path {
let path = cache1.get(str); let path = cache1.get(input);
if (!path) { if (!path) {
path = cache2.get(str); path = cache2.get(input);
if (!path) { if (!path) {
const { index, segments } = Path.parsePath(str, 0); const cursor = new Cursor(input);
if (index < str.length) throw new Error("Invalid parameter path"); path = parsePath(cursor);
path = new Path(segments); if (cursor.charCode) throw new Error("Unexpected character");
if (path.toString() !== str) cache1.set(path.toString(), path); if (path.toString() !== input) cache1.set(path.toString(), path);
} }
cache1.set(str, path); cache1.set(input, path);
} }
return path; return path;
} }
@@ -192,6 +65,14 @@ export default class Path {
return this.segments.length; return this.segments.length;
} }
public get paramLength(): number {
return this.segments.length - this.colon;
}
public get attrLength(): number {
return this.colon;
}
public toString(): string { public toString(): string {
return this._string; return this._string;
} }
@@ -200,25 +81,27 @@ export default class Path {
if (start < 0) start = Math.max(0, this.segments.length + start); if (start < 0) start = Math.max(0, this.segments.length + start);
if (end < 0) end = Math.max(0, this.segments.length + end); if (end < 0) end = Math.max(0, this.segments.length + end);
let str; if (start >= end) return Path.root;
if (start >= end) {
str = ""; let i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
} else { // Include the ":" when slicing exactly at the colon boundary
const i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0; if (this.colon && start === this.segments.length - this.colon) --i1;
const i2 = const i2 =
end <= this.segments.length end <= this.segments.length
? this._stringIndex[end - 1] ? this._stringIndex[end - 1]
: this._string.length; : this._string.length;
str = this._string.slice(i1, i2); const str = this._string.slice(i1, i2);
}
let path = cache1.get(str); let path = cache1.get(str);
if (!path) { if (!path) {
path = cache2.get(str); path = cache2.get(str);
if (!path) { if (!path) {
const segments = this.segments.slice(start, end); const segments = this.segments.slice(start, end);
Object.freeze(segments); const colon =
path = new Path(segments); start <= this.segments.length - this.colon
? Math.max(0, this.colon - this.segments.length + end)
: 0;
path = new Path(segments, colon);
} }
cache1.set(str, path); cache1.set(str, path);
} }
@@ -230,15 +113,28 @@ export default class Path {
if (!path2._string) return this; if (!path2._string) return this;
else if (!this._string) return path2; else if (!this._string) return path2;
const str = `${this._string}.${path2._string}`; if (this.colon && path2.colon && path2.colon < path2.segments.length)
throw new Error("Invalid path");
const colon = this.colon ? this.colon + path2.segments.length : path2.colon;
let str;
if (this.colon && path2.colon === path2.segments.length) {
// Right is all-colon; strip its ":" prefix and join with "."
str = `${this._string}.${path2._string.slice(1)}`;
} else if (path2.colon === path2.segments.length) {
// Left has no colon; right is all-colon; concatenate directly
str = `${this._string}${path2._string}`;
} else {
str = `${this._string}.${path2._string}`;
}
let path = cache1.get(str); let path = cache1.get(str);
if (!path) { if (!path) {
path = cache2.get(str); path = cache2.get(str);
if (!path) { if (!path) {
const segments = this.segments.concat(path2.segments); const segments = this.segments.concat(path2.segments);
Object.freeze(segments); path = new Path(segments, colon);
path = new Path(segments);
} }
cache1.set(str, path); cache1.set(str, path);
} }
@@ -248,21 +144,30 @@ export default class Path {
public stripAlias(): Path { public stripAlias(): Path {
if (!this.alias) return this; if (!this.alias) return this;
const segments = this.segments.map((s) => (Array.isArray(s) ? "*" : s)); const segments = this.segments.map((s) =>
const str = segments.join("."); s instanceof Expression ? "*" : s,
);
let str: string;
if (!this.colon) str = segments.join(".");
else
str =
segments.slice(0, -this.colon).join(".") +
":" +
segments.slice(-this.colon).join(".");
let path = cache1.get(str); let path = cache1.get(str);
if (!path) { if (!path) {
path = cache2.get(str); path = cache2.get(str);
if (!path) { if (!path) {
Object.freeze(segments); path = new Path(segments, this.colon);
path = new Path(segments);
} }
cache1.set(str, path); cache1.set(str, path);
} }
return path; return path;
} }
static root = new Path([], 0);
} }
const interval = setInterval(() => { const interval = setInterval(() => {
+24 -21
View File
@@ -1,8 +1,7 @@
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import * as dgram from "node:dgram"; import * as dgram from "node:dgram";
import * as http from "node:http"; import * as http from "node:http";
import { evaluateAsync } from "./common/expression/util.ts"; import Expression, { Value } from "./common/expression.ts";
import { Expression } from "./types.ts";
import * as auth from "./auth.ts"; import * as auth from "./auth.ts";
import * as extensions from "./extensions.ts"; import * as extensions from "./extensions.ts";
import * as debug from "./debug.ts"; import * as debug from "./debug.ts";
@@ -13,29 +12,33 @@ import * as logger from "../lib/logger.ts";
async function extractAuth( async function extractAuth(
exp: Expression, exp: Expression,
dflt: any, dflt: Value,
): Promise<[string, string, Expression]> { ): Promise<[string, string, Expression]> {
let username, password; let username: string, password: string;
const _exp = await evaluateAsync( const _exp = await exp.evaluateAsync(
exp,
{},
0,
async (e: Expression): Promise<Expression> => { async (e: Expression): Promise<Expression> => {
if (!username && Array.isArray(e) && e[0] === "FUNC") { if (e instanceof Expression.Parameter)
if (e[1] === "EXT") { return new Expression.Literal(null);
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null; if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(0);
if (!username) {
if (e.name === "EXT") {
if (!e.args.every((a) => a instanceof Expression.Literal))
return new Expression.Literal(null);
const args = e.args.map((a) => a.value.toString());
if (typeof args[0] !== "string" || typeof args[1] !== "string")
return new Expression.Literal(null);
for (let i = 4; i < e.length; i++) const { fault, value } = await extensions.run(args);
if (Array.isArray(e[i])) return null; if (fault) return new Expression.Literal(null);
return new Expression.Literal(value);
const { fault, value } = await extensions.run(e.slice(2)); } else if (e.name === "AUTH") {
return fault ? null : value; if (e.args.every((a) => a instanceof Expression.Literal)) {
} else if (e[1] === "AUTH") { username = `${e.args[0].value ?? ""}`;
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) { password = `${e.args[1].value ?? ""}`;
username = e[2] || ""; }
password = e[3] || ""; return new Expression.Literal(dflt);
} }
return dflt;
} }
} }
return e; return e;
+105 -99
View File
@@ -10,11 +10,7 @@ import * as config from "./config.ts";
import { generateDeviceId, once, setTimeoutPromise } from "./util.ts"; import { generateDeviceId, once, setTimeoutPromise } from "./util.ts";
import * as soap from "./soap.ts"; import * as soap from "./soap.ts";
import * as session from "./session.ts"; import * as session from "./session.ts";
import { import Expression, { Value } from "./common/expression.ts";
evaluateAsync,
evaluate,
extractParams,
} from "./common/expression/util.ts";
import * as cache from "./cache.ts"; import * as cache from "./cache.ts";
import * as lock from "./lock.ts"; import * as lock from "./lock.ts";
import * as localCache from "./cwmp/local-cache.ts"; import * as localCache from "./cwmp/local-cache.ts";
@@ -39,7 +35,6 @@ import {
AcsRequest, AcsRequest,
SessionFault, SessionFault,
Fault, Fault,
Expression,
SoapMessage, SoapMessage,
InformRequest, InformRequest,
Preset, Preset,
@@ -80,7 +75,7 @@ async function authenticate(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.auth", "cwmp.auth",
); );
if (authExpression == null) return true; if (!authExpression) return true;
let authentication; let authentication;
@@ -107,56 +102,65 @@ async function authenticate(
authentication["body"] = body; authentication["body"] = body;
} }
const res = await evaluateAsync( const res = await authExpression.evaluateAsync(
authExpression,
{},
sessionContext.timestamp,
async (e: Expression): Promise<Expression> => { async (e: Expression): Promise<Expression> => {
e = session.configContextCallback(sessionContext, e); e = session.configContextCallback(sessionContext, e);
if (Array.isArray(e) && e[0] === "FUNC") { if (e instanceof Expression.Parameter)
if (e[1] === "EXT") { return new Expression.Literal(null);
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null;
for (let i = 4; i < e.length; i++) if (e instanceof Expression.FunctionCall) {
if (Array.isArray(e[i])) return null; if (e.name === "NOW")
return new Expression.Literal(sessionContext.timestamp);
if (e.name === "EXT") {
if (!e.args.every((a) => a instanceof Expression.Literal))
return new Expression.Literal(null);
const { fault, value } = await extensions.run(e.slice(2)); const args = e.args.map((a) => a.value.toString());
return fault ? null : value; if (typeof args[0] !== "string" || typeof args[1] !== "string")
} else if (e[1] === "AUTH") { return new Expression.Literal(null);
const username = e[2];
const password = e[3];
if (username != null && password != null && authentication) {
if (authentication["method"] === "Basic") {
return (
authentication["username"] === e[2] &&
authentication["password"] === e[3]
);
}
if (authentication["method"] === "Digest") { const { fault, value } = await extensions.run(args);
const expected = auth.digest( if (fault) return new Expression.Literal(null);
username, return new Expression.Literal(value);
REALM, } else if (e.name === "AUTH") {
password, if (e.args.every((a) => a instanceof Expression.Literal)) {
authentication["nonce"], const username = e.args[0].value;
"POST", const password = e.args[1].value;
authentication["uri"], if (username != null && password != null && authentication) {
authentication["qop"], if (authentication["method"] === "Basic") {
authentication["body"], return new Expression.Literal(
authentication["cnonce"], authentication["username"] === username.toString() &&
authentication["nc"], authentication["password"] === password.toString(),
); );
return expected === authentication["response"]; }
if (authentication["method"] === "Digest") {
const expected = auth.digest(
username.toString(),
REALM,
password.toString(),
authentication["nonce"],
"POST",
authentication["uri"],
authentication["qop"],
body,
authentication["cnonce"],
authentication["nc"],
);
return new Expression.Literal(
expected === authentication["response"],
);
}
} }
} }
return false; return new Expression.Literal(false);
} }
} }
return e; return e;
}, },
); );
if (res && !Array.isArray(res)) return true; if (res instanceof Expression.Literal) return !!res.value;
return false; return false;
} }
@@ -315,8 +319,7 @@ async function inform(
const cookiesPath = localCache.getConfig( const cookiesPath = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.cookiesPath", "cwmp.cookiesPath",
{}, "",
sessionContext.timestamp,
(e) => session.configContextCallback(sessionContext, e), (e) => session.configContextCallback(sessionContext, e),
); );
@@ -406,8 +409,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
const RETRY_DELAY = +localCache.getConfig( const RETRY_DELAY = +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.retryDelay", "cwmp.retryDelay",
{}, 300,
sessionContext.timestamp,
(e) => session.configContextCallback(sessionContext, e), (e) => session.configContextCallback(sessionContext, e),
); );
@@ -434,14 +436,14 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
deviceData.attributes.revision = 1; deviceData.attributes.revision = 1;
const deviceEvents = {}; const deviceEvents = {};
for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) { for (const p of deviceData.paths.find(Path.parse("Events"), 0b001, 0b1)) {
const attrs = deviceData.attributes.get(p); const attrs = deviceData.attributes.get(p);
const t = attrs?.value[1][0] as number; const t = attrs?.value[1][0] as number;
if (t >= sessionContext.timestamp) if (t >= sessionContext.timestamp)
deviceEvents[p.segments[1] as string] = true; deviceEvents[p.segments[1] as string] = true;
} }
const parameters: { [name: string]: Path } = {}; const parameters = new Set<string>();
const filteredPresets: Preset[] = []; const filteredPresets: Preset[] = [];
for (const preset of presets) { for (const preset of presets) {
@@ -470,29 +472,31 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
continue; continue;
} }
filteredPresets.push(preset); const pre = { ...preset };
for (const k of extractParams( const evalCallback = (e: Expression): Expression => {
evaluate(preset.precondition, null, sessionContext.timestamp), if (e instanceof Expression.FunctionCall && e.name === "NOW")
)) { return new Expression.Literal(sessionContext.timestamp);
// Mark channel in case of fault during fetching precondition if (e instanceof Expression.Parameter) {
sessionContext.channels[preset.channel] = 0; // Mark channel in case of fault during fetching precondition
if (typeof k === "string") parameters[k] = Path.parse(k); sessionContext.channels[preset.channel] = 0;
} parameters.add(e.path.toString());
for (const prov of preset.provisions) {
for (const arg of prov.slice(1)) {
for (const k of extractParams(
evaluate(arg, null, sessionContext.timestamp),
)) {
// Mark channel in case of fault during fetching precondition
sessionContext.channels[preset.channel] = 0;
if (typeof k === "string") parameters[k] = Path.parse(k);
}
} }
} return e;
};
pre.precondition = preset.precondition.evaluate(evalCallback);
pre.provisions = pre.provisions.map((prov) => {
let args = prov.slice(1) as Expression[];
args = args.map((arg) => arg.evaluate(evalCallback));
return [prov[0], ...args];
});
filteredPresets.push(pre);
} }
const declarations = Object.values(parameters).map((v) => ({ const declarations = [...parameters].map((v) => ({
path: v, path: Path.parse(v),
pathGet: 1, pathGet: 1,
pathSet: null, pathSet: null,
attrGet: { value: 1 }, attrGet: { value: 1 },
@@ -520,22 +524,21 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
session.addProvisions(sessionContext, whiteList, whiteListProvisions); session.addProvisions(sessionContext, whiteList, whiteListProvisions);
const appendProvisionsToFaults = {}; const appendProvisionsToFaults = {};
const evalCallback2 = (e: Expression): Expression.Literal => {
e = session.configContextCallback(sessionContext, e);
if (!(e instanceof Expression.Literal)) return new Expression.Literal(null);
return e;
};
for (const p of filteredPresets) { for (const p of filteredPresets) {
if ( if (p.precondition.evaluate(evalCallback2).value) {
evaluate(p.precondition, {}, sessionContext.timestamp, (e) =>
session.configContextCallback(sessionContext, e),
)
) {
const provs = p.provisions.map((pp) => [ const provs = p.provisions.map((pp) => [
pp[0], pp[0],
...pp ...pp
.slice(1) .slice(1)
.map((arg) => .map((arg) => (arg as Expression).evaluate(evalCallback2).value),
evaluate(arg, {}, sessionContext.timestamp, (e) => ]) as [string, ...Value[]][];
session.configContextCallback(sessionContext, e),
),
),
]) as [string, ...Expression[]][];
if (blackList[p.channel] === 2) { if (blackList[p.channel] === 2) {
appendProvisionsToFaults[p.channel] = ( appendProvisionsToFaults[p.channel] = (
appendProvisionsToFaults[p.channel] || [] appendProvisionsToFaults[p.channel] || []
@@ -925,16 +928,18 @@ export async function onConnection(socket: Socket): Promise<void> {
export async function onClientError(err: Error, socket: Socket): Promise<void> { export async function onClientError(err: Error, socket: Socket): Promise<void> {
const remoteAddress = getSocketEndpoints(socket).remoteAddress; const remoteAddress = getSocketEndpoints(socket).remoteAddress;
const cacheSnapshot = await localCache.getRevision(); const cacheSnapshot = await localCache.getRevision();
const debugEnabled = !!localCache.getConfig( const debugEnabled = localCache.getConfig(
cacheSnapshot, cacheSnapshot,
"cwmp.debug", "cwmp.debug",
{ false,
remoteAddress: remoteAddress,
},
Date.now(),
(e) => { (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS") if (e instanceof Expression.FunctionCall) {
return remoteAddress; if (e.name === "REMOTE_ADDRESS")
return new Expression.Literal(remoteAddress);
if (e.name === "NOW") return new Expression.Literal(Date.now());
}
if (!(e instanceof Expression.Literal))
return new Expression.Literal(null);
return e; return e;
}, },
); );
@@ -1030,8 +1035,7 @@ async function processRequest(
sessionContext.debug = !!localCache.getConfig( sessionContext.debug = !!localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.debug", "cwmp.debug",
{}, false,
sessionContext.timestamp,
(e) => session.configContextCallback(sessionContext, e), (e) => session.configContextCallback(sessionContext, e),
); );
@@ -1039,8 +1043,7 @@ async function processRequest(
sessionContext.timeout = +localCache.getConfig( sessionContext.timeout = +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.sessionTimeout", "cwmp.sessionTimeout",
{}, 30,
sessionContext.timestamp,
(e) => session.configContextCallback(sessionContext, e), (e) => session.configContextCallback(sessionContext, e),
); );
} }
@@ -1282,16 +1285,19 @@ async function clientError(
deviceId = sessionContext.deviceId; deviceId = sessionContext.deviceId;
} else { } else {
const cacheSnapshot = await localCache.getRevision(); const cacheSnapshot = await localCache.getRevision();
debugEnabled = !!localCache.getConfig( const remoteAddress = getRequestOrigin(httpRequest).remoteAddress;
debugEnabled = localCache.getConfig(
cacheSnapshot, cacheSnapshot,
"cwmp.debug", "cwmp.debug",
{ false,
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
},
Date.now(),
(e) => { (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS") if (e instanceof Expression.FunctionCall) {
return getRequestOrigin(httpRequest).remoteAddress; if (e.name === "REMOTE_ADDRESS")
return new Expression.Literal(remoteAddress);
if (e.name === "NOW") return new Expression.Literal(Date.now());
}
if (!(e instanceof Expression.Literal))
return new Expression.Literal(null);
return e; return e;
}, },
); );
+21 -26
View File
@@ -7,7 +7,6 @@ import {
Task, Task,
Operation, Operation,
} from "../types.ts"; } from "../types.ts";
import Path from "../common/path.ts";
import { collections } from "../db/db.ts"; import { collections } from "../db/db.ts";
import { optimizeProjection } from "../db/util.ts"; import { optimizeProjection } from "../db/util.ts";
import * as MongoTypes from "../db/types.ts"; import * as MongoTypes from "../db/types.ts";
@@ -23,21 +22,17 @@ function compareAccessLists(list1: string[], list2: string[]): boolean {
export async function fetchDevice( export async function fetchDevice(
id: string, id: string,
timestamp: number, timestamp: number,
): Promise<[Path, number, Attributes?][]> { ): Promise<[string, number, Attributes?][]> {
const res: [Path, number, Attributes?][] = [ const res: [string, number, Attributes?][] = [
["Events", timestamp, { object: [timestamp, 1], writable: [timestamp, 0] }],
[ [
Path.parse("Events"), "DeviceID",
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
],
[
Path.parse("DeviceID"),
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }, { 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; if (!device) return null;
function storeParams( function storeParams(
@@ -77,7 +72,7 @@ export async function fetchDevice(
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]]; attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
try { try {
res.push([Path.parse(path), t, attrs]); res.push([path, t, attrs]);
} catch { } catch {
// The path parser is now more strict so we might be in a situation where // 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 // the database contains invalid paths from before this change So here we
@@ -86,7 +81,7 @@ export async function fetchDevice(
splits[splits.length - 1] = splits[splits.length - 1] =
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX; encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
path = splits.join("."); path = splits.join(".");
res.push([Path.parse(path), t, attrs]); res.push([path, t, attrs]);
return; return;
} }
@@ -98,17 +93,17 @@ export async function fetchDevice(
} }
if (obj["_object"] && obj["_timestamp"]) if (obj["_object"] && obj["_timestamp"])
res.push([Path.parse(path + ".*"), obj["_timestamp"]]); res.push([path + ".*", obj["_timestamp"]]);
} }
const ts: number = +device["_timestamp"] || 0; 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)) { for (const [k, v] of Object.entries(device)) {
switch (k) { switch (k) {
case "_lastInform": case "_lastInform":
res.push([ res.push([
Path.parse("Events.Inform"), "Events.Inform",
+v, +v,
{ {
object: [+v, 0], object: [+v, 0],
@@ -119,7 +114,7 @@ export async function fetchDevice(
break; break;
case "_lastBoot": case "_lastBoot":
res.push([ res.push([
Path.parse("Events.1_BOOT"), "Events.1_BOOT",
+v, +v,
{ {
object: [+v, 0], object: [+v, 0],
@@ -130,7 +125,7 @@ export async function fetchDevice(
break; break;
case "_lastBootstrap": case "_lastBootstrap":
res.push([ res.push([
Path.parse("Events.0_BOOTSTRAP"), "Events.0_BOOTSTRAP",
+v, +v,
{ {
object: [+v, 0], object: [+v, 0],
@@ -142,7 +137,7 @@ export async function fetchDevice(
case "_registered": case "_registered":
// Use current timestamp for registered event attribute timestamps // Use current timestamp for registered event attribute timestamps
res.push([ res.push([
Path.parse("Events.Registered"), "Events.Registered",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -153,7 +148,7 @@ export async function fetchDevice(
break; break;
case "_id": case "_id":
res.push([ res.push([
Path.parse("DeviceID.ID"), "DeviceID.ID",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -165,7 +160,7 @@ export async function fetchDevice(
case "_tags": case "_tags":
if ((v as string[]).length) { if ((v as string[]).length) {
res.push([ res.push([
Path.parse("Tags"), "Tags",
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }, { object: [timestamp, 1], writable: [timestamp, 0] },
]); ]);
@@ -173,7 +168,7 @@ export async function fetchDevice(
for (const t of v as string[]) { for (const t of v as string[]) {
res.push([ res.push([
Path.parse("Tags." + encodeTag(t)), "Tags." + encodeTag(t),
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -186,7 +181,7 @@ export async function fetchDevice(
case "_deviceId": case "_deviceId":
if (v["_Manufacturer"] != null) { if (v["_Manufacturer"] != null) {
res.push([ res.push([
Path.parse("DeviceID.Manufacturer"), "DeviceID.Manufacturer",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -198,7 +193,7 @@ export async function fetchDevice(
if (v["_OUI"] != null) { if (v["_OUI"] != null) {
res.push([ res.push([
Path.parse("DeviceID.OUI"), "DeviceID.OUI",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -210,7 +205,7 @@ export async function fetchDevice(
if (v["_ProductClass"] != null) { if (v["_ProductClass"] != null) {
res.push([ res.push([
Path.parse("DeviceID.ProductClass"), "DeviceID.ProductClass",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -222,7 +217,7 @@ export async function fetchDevice(
if (v["_SerialNumber"] != null) { if (v["_SerialNumber"] != null) {
res.push([ res.push([
Path.parse("DeviceID.SerialNumber"), "DeviceID.SerialNumber",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -257,7 +252,7 @@ export async function saveDevice(
) )
continue; 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 // Param timestamps may be greater than session timestamp to track revisions
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp; if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
+72 -56
View File
@@ -1,15 +1,12 @@
import * as vm from "node:vm"; import * as vm from "node:vm";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts"; import { collections } from "../db/db.ts";
import { convertOldPrecondition } from "../db/util.ts"; import { convertOldPrecondition } from "../db/util.ts";
import * as logger from "../logger.ts"; import * as logger from "../logger.ts";
import * as scheduling from "../scheduling.ts"; import * as scheduling from "../scheduling.ts";
import * as expression from "../common/expression/util.ts"; import Expression, { Value } from "../common/expression.ts";
import { parse } from "../common/expression/parser.ts";
import { import {
Preset, Preset,
Expression,
Provisions, Provisions,
VirtualParameters, VirtualParameters,
Files, Files,
@@ -93,10 +90,10 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
} }
const events = preset["events"] || {}; const events = preset["events"] || {};
let precondition = true as Expression; let precondition: Expression = new Expression.Literal(true);
if (preset["precondition"]) { if (preset["precondition"]) {
try { try {
precondition = parse(preset["precondition"]); precondition = Expression.parse(preset["precondition"]);
} catch { } catch {
precondition = convertOldPrecondition( precondition = convertOldPrecondition(
JSON.parse(preset["precondition"]), JSON.parse(preset["precondition"]),
@@ -104,7 +101,7 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
} }
// Simplify expression // Simplify expression
precondition = expression.evaluate(precondition); precondition = precondition.evaluate((e) => e);
} }
const _provisions: Preset["provisions"] = []; const _provisions: Preset["provisions"] = [];
@@ -113,23 +110,42 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
for (const c of preset.configurations) { for (const c of preset.configurations) {
switch (c.type) { switch (c.type) {
case "age": case "age":
_provisions.push(["refresh", c.name, +c.age]); _provisions.push([
"refresh",
new Expression.Literal(c.name),
new Expression.Literal(+c.age),
]);
break; break;
case "value": case "value":
_provisions.push(["value", c.name, c.value]); _provisions.push([
"value",
new Expression.Literal(c.name),
new Expression.Literal(c.value),
]);
break; break;
case "add_tag": case "add_tag":
_provisions.push(["tag", c.tag, true]); _provisions.push([
"tag",
new Expression.Literal(c.tag),
new Expression.Literal(true),
]);
break; break;
case "delete_tag": case "delete_tag":
_provisions.push(["tag", c.tag, false]); _provisions.push([
"tag",
new Expression.Literal(c.tag),
new Expression.Literal(false),
]);
break; break;
case "provision": case "provision":
_provisions.push([c.name, ...(c.args || [])]); _provisions.push([
c.name,
...(c.args || []).map((a) => Expression.parse(a)),
]);
break; break;
case "add_object": case "add_object":
@@ -139,11 +155,19 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
.map((k) => `${k}:${JSON.stringify(obj[k])}`) .map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(","); .join(",");
const p = `${c.name}.[${alias}]`; 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) { for (const k in obj) {
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1)) 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])}`) .map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(","); .join(",");
const p = `${c.name}.[${alias}]`; 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) { for (const c of conf) {
// Evaluate expressions to simplify them // 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]; return [h, _config];
@@ -303,49 +331,37 @@ export function getFiles(revision: string): Files {
} }
export function getConfig( export function getConfig(
snapshotKey: string, revision: string,
key: string, key: string,
context: Record<string, unknown>, dflt: string,
now: number, fn: (e: Expression) => Expression.Literal,
cb?: (e: Expression) => Expression, ): string;
): string | number | boolean | null { export function getConfig(
const snapshot = localCache.get(snapshotKey); 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"); if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = { const e = snapshot.config[key];
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT", if (!e) return dflt;
"cwmp.debug": "DEBUG", const v = e.evaluate(fn).value;
"cwmp.retryDelay": "RETRY_DELAY", if (typeof v !== typeof dflt) return dflt;
"cwmp.sessionTimeout": "SESSION_TIMEOUT", return v;
"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;
} }
export function getConfigExpression( export function getConfigExpression(
+56 -50
View File
@@ -1,7 +1,8 @@
import { Filter } from "mongodb"; import { Filter } from "mongodb";
import { EJSON } from "bson"; import { EJSON } from "bson";
import { complement } from "espresso-iisojs";
import { parseLikePattern } from "../common/expression/parser.ts"; import { parseLikePattern } from "../common/expression/parser.ts";
import { Expression } from "../types.ts"; import Expression from "../common/expression.ts";
import { decodeTag } from "../util.ts"; import { decodeTag } from "../util.ts";
import { import {
SynthContextBase, SynthContextBase,
@@ -10,21 +11,14 @@ import {
Clause, Clause,
} from "../common/expression/synth.ts"; } from "../common/expression/synth.ts";
import normalize from "../common/expression/normalize.ts"; import normalize from "../common/expression/normalize.ts";
import * as BI from "../common/expression/bigint.ts";
type Minterm = number[]; type Minterm = number[];
function getParam(exp: Expression, collection: string): string { function getParam(exp: Expression, collection: string): string {
if (!Array.isArray(exp)) if (!(exp instanceof Expression.Parameter))
throw new Error("Left-hand operand must be a parameter"); throw new Error("Left-hand operand must be a parameter");
if (exp[0] !== "PARAM") const p = exp.path.toString();
throw new Error("Left-hand operand must be a parameter");
if (typeof exp[1] !== "string")
throw new Error("Left-hand operand must be a parameter");
const p = exp[1];
if (collection === "devices") { if (collection === "devices") {
if (p === "DeviceID.ID") return "_id"; if (p === "DeviceID.ID") return "_id";
else if (p === "DeviceID") return "_deviceId"; else if (p === "DeviceID") return "_deviceId";
@@ -117,12 +111,12 @@ function getTypes(parameter: string, collection: string): string[] {
function roundOid(oid: string, roundUp: boolean): string { function roundOid(oid: string, roundUp: boolean): string {
const match = (oid.match(/^[0-9a-f]*/)?.[0] ?? "").slice(0, 24); const match = (oid.match(/^[0-9a-f]*/)?.[0] ?? "").slice(0, 24);
let num = BI.BigInt("0x0" + match); let num = BigInt("0x0" + match);
let lastChar = 0; let lastChar = 0;
if (oid.length > match.length) lastChar += oid.charCodeAt(match.length); if (oid.length > match.length) lastChar += oid.charCodeAt(match.length);
if (match.length < 24) lastChar -= 48; if (match.length < 24) lastChar -= 48;
if (lastChar > 0 && roundUp) num++; if (lastChar > 0 && roundUp) num++;
num <<= BI.BigInt(4 * (24 - match.length)); num <<= BigInt(4 * (24 - match.length));
if (lastChar < 0 && !roundUp) --num; if (lastChar < 0 && !roundUp) --num;
const str = num.toString(16); const str = num.toString(16);
if (str.length > 24 || str.startsWith("-")) return null; if (str.length > 24 || str.startsWith("-")) return null;
@@ -259,11 +253,12 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
super(); super();
} }
getMinterms(clause: Clause, res: true | false | null): number[][] { getMinterms(clause: Clause, res: number): number[][] {
const exp = clause.expression(); res = res & 0b111;
if (!Array.isArray(exp)) throw new Error("Invalid query expression"); if (res & (res - 1)) return complement(this.getMinterms(clause, ~res));
if (res === null) { const exp = clause.expression();
if (res === 0b001) {
const minterms: number[][] = []; const minterms: number[][] = [];
for (const dep of clause.getNullables()) { for (const dep of clause.getNullables()) {
const e = dep.operand.expression(); const e = dep.operand.expression();
@@ -280,36 +275,42 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
return minterms; return minterms;
} }
if ([">", "<", "="].includes(exp[0])) { if (!(exp instanceof Expression.Binary))
const param = getParam(exp[1], this.collection); throw new Error("Invalid query expression");
let rhs = exp[2]; const truthy = res === 0b100;
if (typeof rhs === "boolean") rhs = +rhs;
else if (typeof rhs !== "string" && typeof rhs !== "number") if ([">", "<", "="].includes(exp.operator)) {
const param = getParam(exp.left, this.collection);
if (!(exp.right instanceof Expression.Literal))
throw new Error(`Right-hand operand must be a literal value`); throw new Error(`Right-hand operand must be a literal value`);
let rhs = exp.right.value;
if (typeof rhs === "boolean") rhs = +rhs;
if ( if (
param.startsWith("Tags.") && param.startsWith("Tags.") &&
this.collection === "devices" && this.collection === "devices" &&
exp[0] === "=" exp.operator === "="
) { ) {
const t = decodeTag(param.slice(5)); const t = decodeTag(param.slice(5));
const c = new MongoClauseArray("_tags", t); const c = new MongoClauseArray("_tags", t);
if (typeof rhs === "string") rhs = 2; if (typeof rhs === "string") rhs = 2;
if (exp[0] === "=") { if (exp.operator === "=") {
if ((rhs === 1) !== res) return []; if ((rhs === 1) !== truthy) return [];
} else if (exp[0] === ">") { } else if (exp.operator === ">") {
if ((res && rhs >= 1) || (!res && rhs < 1)) return []; if ((truthy && rhs >= 1) || (!truthy && rhs < 1)) return [];
} else if (exp[0] === "<") { } else if (exp.operator === "<") {
if ((res && rhs <= 1) || (!res && rhs > 1)) return []; if ((truthy && rhs <= 1) || (!truthy && rhs > 1)) return [];
} }
return [[(this.getVar(c) << 1) ^ 1]]; return [[(this.getVar(c) << 1) ^ 1]];
} }
let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte"; let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte";
if (exp[0] === "=") op = "$eq"; if (exp.operator === "=") op = "$eq";
else if (exp[0] === ">") op = res ? "$gt" : "$lte"; else if (exp.operator === ">") op = truthy ? "$gt" : "$lte";
else if (exp[0] === "<") op = res ? "$lt" : "$gte"; else if (exp.operator === "<") op = truthy ? "$lt" : "$gte";
const possibleTypes = new Set(getTypes(param, this.collection)); const possibleTypes = new Set(getTypes(param, this.collection));
const clauses: MongoClause[] = []; const clauses: MongoClause[] = [];
@@ -333,7 +334,7 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
} }
} }
if (op === "$eq" && !res) { if (op === "$eq" && !truthy) {
clauses.push(new MongoClauseCompare(param, "$eq", null, "")); clauses.push(new MongoClauseCompare(param, "$eq", null, ""));
return [clauses.map((c) => this.getVar(c) << 1)]; return [clauses.map((c) => this.getVar(c) << 1)];
} }
@@ -382,30 +383,35 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
} }
return clauses.map((c) => [(this.getVar(c) << 1) ^ 1]); return clauses.map((c) => [(this.getVar(c) << 1) ^ 1]);
} else if (exp[0] === "LIKE") { } else if (exp.operator === "LIKE") {
const pat = exp[2]; if (
if (typeof pat !== "string") !(
exp.right instanceof Expression.Literal &&
typeof exp.right.value === "string"
)
)
throw new Error("Right-hand operand of 'LIKE' must be a string"); throw new Error("Right-hand operand of 'LIKE' must be a string");
let p = exp[1]; const pat = exp.right.value;
let p = exp.left;
let caseSensitive = true; let caseSensitive = true;
if ( if (
Array.isArray(p) && p instanceof Expression.FunctionCall &&
p[0] === "FUNC" && ["UPPER", "LOWER"].includes(p.name)
["UPPER", "LOWER"].includes(p[1])
) { ) {
if (p[1] === "UPPER" && pat !== pat.toUpperCase()) if (p.name === "UPPER" && pat !== pat.toUpperCase())
return res ? [] : [[]]; return truthy ? [] : [[]];
if (p[1] === "LOWER" && pat !== pat.toLowerCase()) if (p.name === "LOWER" && pat !== pat.toLowerCase())
return res ? [] : [[]]; return truthy ? [] : [[]];
caseSensitive = false; caseSensitive = false;
p = p[2]; p = p.args[0];
} }
const param = getParam(p, this.collection); const param = getParam(p, this.collection);
const c = new MongoClauseLike(param, pat, exp[3], caseSensitive); const c = new MongoClauseLike(param, pat, null, caseSensitive);
if (res) return [[(this.getVar(c) << 1) ^ 1]]; if (truthy) return [[(this.getVar(c) << 1) ^ 1]];
const typeClase = new MongoClauseType(param, "string"); const typeClause = new MongoClauseCompare(param, "$gte", "", "string");
const r = [[this.getVar(c) << 1, (this.getVar(typeClase) << 1) ^ 1]]; const r = [[this.getVar(c) << 1, (this.getVar(typeClause) << 1) ^ 1]];
return r; return r;
} else { } else {
throw new Error("Invalid query expression"); throw new Error("Invalid query expression");
@@ -641,7 +647,7 @@ export function toMongoQuery(
exp = normalize(exp); exp = normalize(exp);
const clause = Clause.fromExpression(normalize(exp)); const clause = Clause.fromExpression(normalize(exp));
const context = new MongoSynthContext(resource); const context = new MongoSynthContext(resource);
const minterms = clause.true(context); const minterms = clause.getMinterms(context, 0b100);
const minimized = context.minimize(minterms); const minimized = context.minimize(minterms);
if (!minimized.length) return false; if (!minimized.length) return false;
return EJSON.deserialize(context.toQuery(minimized)); return EJSON.deserialize(context.toQuery(minimized));
@@ -650,5 +656,5 @@ export function toMongoQuery(
export function validQuery(exp: Expression, resource: string): void { export function validQuery(exp: Expression, resource: string): void {
const clause = Clause.fromExpression(normalize(exp)); const clause = Clause.fromExpression(normalize(exp));
const context = new MongoSynthContext(resource); const context = new MongoSynthContext(resource);
clause.true(context); clause.getMinterms(context, 0b100);
} }
+4 -3
View File
@@ -1,5 +1,6 @@
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
import { Expression, FaultStruct } from "../types.ts"; import { FaultStruct } from "../types.ts";
import { Value } from "../common/expression.ts";
export interface Fault { export interface Fault {
_id: string; _id: string;
@@ -71,7 +72,7 @@ interface TaskDeleteObject extends TaskBase {
interface TaskProvisions extends TaskBase { interface TaskProvisions extends TaskBase {
name: "provisions"; name: "provisions";
provisions?: [string, ...Expression[]][]; provisions?: [string, ...Value[]][];
} }
export type Task = export type Task =
@@ -122,7 +123,7 @@ type Configuration =
| { type: "delete_tag"; tag: string } | { type: "delete_tag"; tag: string }
| { type: "add_object"; name: string; object: string } | { type: "add_object"; name: string; object: string }
| { type: "delete_object"; name: string; object: string } | { type: "delete_object"; name: string; object: string }
| { type: "provision"; name: string; args?: Expression[] }; | { type: "provision"; name: string; args?: string[] };
export interface Preset { export interface Preset {
_id: string; _id: string;
+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"; import { encodeTag } from "../util.ts";
// Optimize projection by removing overlaps // Optimize projection by removing overlaps
@@ -26,44 +27,62 @@ export function optimizeProjection(obj: { [path: string]: 1 }): {
export function convertOldPrecondition(q: Record<string, unknown>): Expression { export function convertOldPrecondition(q: Record<string, unknown>): Expression {
function recursive(_query): Expression { function recursive(_query): Expression {
const expressions: Expression[] = []; let res: Expression = new Expression.Literal(true);
for (const [k, v] of Object.entries(_query)) { for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") { if (k[0] === "$") {
if (k === "$and") { if (k === "$and") {
const and: Expression = ["AND"]; for (const vv of Object.values(v))
for (const vv of Object.values(v)) and.push(recursive(vv)); res = Expression.and(res, recursive(vv));
expressions.push(and);
} else if (k === "$or") { } else if (k === "$or") {
const or: Expression = ["OR"]; let or: Expression = new Expression.Literal(false);
for (const vv of Object.values(v)) or.push(recursive(vv)); for (const vv of Object.values(v))
expressions.push(or); or = Expression.or(or, recursive(vv));
res = Expression.and(res, or);
} else { } else {
throw new Error(`Operator ${k} not supported`); throw new Error(`Operator ${k} not supported`);
} }
} else if (k === "_tags") { } else if (k === "_tags") {
if (typeof v === "object") { if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`); if (Array.isArray(v)) throw new Error(`Invalid type`);
const conjs: Expression[] = [];
for (const [op, val] of Object.entries(v)) { for (const [op, val] of Object.entries(v)) {
if (op === "$ne") { if (op === "$ne") {
if (typeof v["$ne"] !== "string") if (typeof v["$ne"] !== "string")
throw new Error("Only string values are allowed for _tags"); 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") { } else if (op === "$eq") {
if (typeof v["$eq"] !== "string") if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags"); 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 { } else {
throw new Error(`Invalid tag query`); throw new Error(`Invalid tag query`);
} }
} }
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else { } else {
expressions.push([ res = Expression.and(
"IS NOT NULL", res,
["PARAM", `Tags.${encodeTag(v as string)}`], new Expression.Unary(
]); "IS NOT NULL",
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(v as string)}`),
),
),
);
} }
} else if (k.startsWith("Tags.")) { } else if (k.startsWith("Tags.")) {
let exists: boolean; let exists: boolean;
@@ -73,51 +92,92 @@ export function convertOldPrecondition(q: Record<string, unknown>): Expression {
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"]; else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`); 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") { } else if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`); if (Array.isArray(v)) throw new Error(`Invalid type`);
const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) { for (const [kk, vv] of Object.entries(v)) {
if (kk === "$eq") { 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") { } else if (kk === "$ne") {
exps.push([ const p = new Expression.Parameter(Path.parse(k));
"OR", res = Expression.and(
["<>", ["PARAM", k], vv], res,
["IS NULL", ["PARAM", k]], Expression.or(
]); new Expression.Binary("<>", p, new Expression.Literal(vv)),
new Expression.Unary("IS NULL", p),
),
);
} else if (kk === "$lt") { } 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") { } 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") { } 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") { } 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 { } else {
throw new Error(`Operator ${kk} not supported`); throw new Error(`Operator ${kk} not supported`);
} }
if (!["string", "number", "boolean"].includes(typeof vv)) if (!["string", "number", "boolean"].includes(typeof vv))
throw new Error(`Invalid value for ${kk} operator`); 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 { } 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]; return res;
if (expressions.length === 0) return true;
return ["AND", ...expressions];
} }
// empty filter // empty filter
if (!Object.keys(q).length) return true; if (!Object.keys(q).length) return new Expression.Literal(true);
return recursive(q); return recursive(q);
} }
+42 -13
View File
@@ -1,3 +1,4 @@
import Expression, { Value } from "./common/expression.ts";
import Path from "./common/path.ts"; import Path from "./common/path.ts";
import { import {
DeviceData, DeviceData,
@@ -84,9 +85,9 @@ export function getAliasDeclarations(
if (path.alias) { if (path.alias) {
for (const [i, alias] of path.segments.entries()) { for (const [i, alias] of path.segments.entries()) {
if (Array.isArray(alias)) { if (alias instanceof Expression) {
const parent = stripped.slice(0, i + 1); const parent = stripped.slice(0, i + 1);
for (const [p] of alias as [Path, string][]) { for (const [p] of expressionToAlias(alias)) {
decs = decs.concat( decs = decs.concat(
getAliasDeclarations(parent.concat(p), timestamp, { getAliasDeclarations(parent.concat(p), timestamp, {
value: timestamp, value: timestamp,
@@ -100,6 +101,22 @@ export function getAliasDeclarations(
return decs; return decs;
} }
export function expressionToAlias(exp: Expression): [Path, Value][] {
if (exp instanceof Expression.Literal && exp.value === true) return [];
if (exp instanceof Expression.Binary) {
if (exp.operator === "AND")
return [...expressionToAlias(exp.left), ...expressionToAlias(exp.right)];
else if (exp.operator === "=") {
if (
exp.left instanceof Expression.Parameter &&
exp.right instanceof Expression.Literal
)
return [[exp.left.path, exp.right.value]];
}
}
throw new Error("Invalid alias expression");
}
export function unpack( export function unpack(
deviceData: DeviceData, deviceData: DeviceData,
path: Path, path: Path,
@@ -107,17 +124,19 @@ export function unpack(
): Path[] { ): Path[] {
let allMatches = [] as Path[]; let allMatches = [] as Path[];
if (!path.alias) { if (!path.alias) {
for (const p of deviceData.paths.find(path, false, true)) for (const p of deviceData.paths.findCompat(path, false, true))
if (deviceData.attributes.has(p, revision)) allMatches.push(p); if (deviceData.attributes.has(p, revision)) allMatches.push(p);
} else { } else {
const wildcardPath = path.stripAlias(); const wildcardPath = path.stripAlias();
for (const p of deviceData.paths.find(wildcardPath, false, true)) for (const p of deviceData.paths.findCompat(wildcardPath, false, true))
if (deviceData.attributes.has(p, revision)) allMatches.push(p); if (deviceData.attributes.has(p, revision)) allMatches.push(p);
for (let i = path.length - 1; i >= 0; --i) { for (let i = path.length - 1; i >= 0; --i) {
if (path.alias & (1 << i)) { if (path.alias & (1 << i)) {
for (const [param, val] of path.segments[i] as [Path, string][]) { for (const [param, val] of expressionToAlias(
path.segments[i] as Expression,
)) {
const p = wildcardPath.slice(0, i + 1).concat(param); const p = wildcardPath.slice(0, i + 1).concat(param);
const unpacked = unpack(deviceData, p, revision); const unpacked = unpack(deviceData, p, revision);
const filtered: Path[] = []; const filtered: Path[] = [];
@@ -189,7 +208,7 @@ export function clear(
attributes.value = attributes.object; attributes.value = attributes.object;
} }
for (const p of deviceData.paths.find( for (const p of deviceData.paths.findCompat(
path, path,
true, true,
true, true,
@@ -252,12 +271,12 @@ function compareEquality(a, b): boolean {
export function set( export function set(
deviceData: DeviceData, deviceData: DeviceData,
path: Path, pathStr: string,
timestamp: number, timestamp: number,
attributes: Attributes, attributes: Attributes,
toClear?: Clear[], toClear?: Clear[],
): Clear[] { ): Clear[] {
path = deviceData.paths.add(path); const path = deviceData.paths.add(pathStr);
const currentTimestamp = deviceData.timestamps.get(path); const currentTimestamp = deviceData.timestamps.get(path);
@@ -312,7 +331,7 @@ export function set(
if (path.length > 1) { if (path.length > 1) {
toClear = set( toClear = set(
deviceData, deviceData,
path.slice(0, path.length - 1), path.slice(0, path.length - 1).toString(),
timestamp, timestamp,
{ object: [timestamp, 1] }, { object: [timestamp, 1] },
toClear, toClear,
@@ -326,7 +345,12 @@ export function set(
deviceData.attributes.delete(path); deviceData.attributes.delete(path);
changeFlags |= 1; changeFlags |= 1;
} else if (path.wildcard) { } else if (path.wildcard) {
for (const p of deviceData.paths.find(path, false, true, path.length)) { for (const p of deviceData.paths.findCompat(
path,
false,
true,
path.length,
)) {
if (timestamp > deviceData.timestamps.get(p)) { if (timestamp > deviceData.timestamps.get(p)) {
toClear = toClear || []; toClear = toClear || [];
toClear.push([p, timestamp]); toClear.push([p, timestamp]);
@@ -343,7 +367,12 @@ export function set(
toClear = toClear || []; toClear = toClear || [];
toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]); toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]);
} else { } else {
for (const p of deviceData.paths.find(path, true, false, path.length)) { for (const p of deviceData.paths.findCompat(
path,
true,
false,
path.length,
)) {
const tracker = deviceData.trackers.get(p); const tracker = deviceData.trackers.get(p);
for (const k in tracker) for (const k in tracker)
if (tracker[k] & changeFlags) deviceData.changes.add(k); if (tracker[k] & changeFlags) deviceData.changes.add(k);
@@ -356,11 +385,11 @@ export function set(
export function track( export function track(
deviceData: DeviceData, deviceData: DeviceData,
path: Path, pathStr: string,
marker: string, marker: string,
attributes?: string[], attributes?: string[],
): void { ): void {
path = deviceData.paths.add(path); const path = deviceData.paths.add(pathStr);
let f = 1; let f = 1;
if (attributes) if (attributes)
+14 -5
View File
@@ -79,14 +79,23 @@ export async function getStatus(): Promise<Status> {
const users = getUsers(configSnapshot); const users = getUsers(configSnapshot);
const ui = getUiConfig(configSnapshot); const ui = getUiConfig(configSnapshot);
return { const status = {
users: !Object.keys(users).length, users: !Object.keys(users).length,
presets: !presetCount, presets: !presetCount,
filters: !Object.keys(ui["filters"]).length, filters: true,
device: !Object.keys(ui["device"]).length, device: true,
index: !Object.keys(ui["index"]).length, index: true,
overview: !Object.keys(ui["overview"]).length, overview: true,
}; };
for (const k of Object.keys(ui)) {
if (k.startsWith("filters.")) status.filters = false;
if (k.startsWith("device.")) status.device = false;
if (k.startsWith("index.")) status.index = false;
if (k.startsWith("overview.")) status.overview = false;
}
return status;
} }
export async function seed(options: Record<string, boolean>): Promise<void> { export async function seed(options: Record<string, boolean>): Promise<void> {
+22 -20
View File
@@ -14,6 +14,7 @@ import { flattenDevice } from "./ui/db.ts";
import { getRequestOrigin } from "./forwarded.ts"; import { getRequestOrigin } from "./forwarded.ts";
import { acquireLock, releaseLock } from "./lock.ts"; import { acquireLock, releaseLock } from "./lock.ts";
import { ResourceLockedError } from "./common/errors.ts"; import { ResourceLockedError } from "./common/errors.ts";
import Expression from "./common/expression.ts";
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/; const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/; const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
@@ -349,6 +350,24 @@ async function handler(
const lastInform = (dev["_lastInform"] as Date).getTime(); const lastInform = (dev["_lastInform"] as Date).getTime();
const device = flattenDevice(dev); const device = flattenDevice(dev);
const configCallback = (e: Expression): Expression.Literal => {
if (e instanceof Expression.Literal) return e;
else if (e instanceof Expression.Parameter) {
const p = device[e.path.toString()];
if (p != null) return new Expression.Literal(p);
} else if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(Date.now());
if (e.name === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p != null)
return new Expression.Literal(new URL(p as string).hostname);
}
}
}
return new Expression.Literal(null);
};
let onlineThreshold: number; let onlineThreshold: number;
if (url.searchParams.has("timeout")) { if (url.searchParams.has("timeout")) {
onlineThreshold = parseInt(url.searchParams.get("timeout")); onlineThreshold = parseInt(url.searchParams.get("timeout"));
@@ -357,26 +376,9 @@ async function handler(
onlineThreshold = getConfig( onlineThreshold = getConfig(
revision, revision,
"cwmp.deviceOnlineThreshold", "cwmp.deviceOnlineThreshold",
{}, 4000,
Date.now(), configCallback,
(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") {
for (const root of ["InternetGatewayDevice", "Device"]) {
const p =
device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p?.value) return new URL(p.value[0] as string).hostname;
}
return null;
}
}
return exp;
},
) as number;
} }
let status = await apiFunctions.connectionRequest(deviceId, device); let status = await apiFunctions.connectionRequest(deviceId, device);
+147 -184
View File
@@ -25,7 +25,6 @@ import {
TransferCompleteRequest, TransferCompleteRequest,
Operation, Operation,
ScriptResult, ScriptResult,
Expression,
GetParameterValues, GetParameterValues,
GetParameterAttributes, GetParameterAttributes,
GetParameterNames, GetParameterNames,
@@ -42,6 +41,7 @@ import {
import { getRequestOrigin } from "./forwarded.ts"; import { getRequestOrigin } from "./forwarded.ts";
import * as logger from "./logger.ts"; import * as logger from "./logger.ts";
import { encodeTag } from "./util.ts"; import { encodeTag } from "./util.ts";
import Expression, { Value } from "./common/expression.ts";
const VALID_PARAM_TYPES = new Set([ const VALID_PARAM_TYPES = new Set([
"xsd:int", "xsd:int",
@@ -102,29 +102,25 @@ function generateRpcId(sessionContext: SessionContext): string {
export function configContextCallback( export function configContextCallback(
sessionContext: SessionContext, sessionContext: SessionContext,
exp: Expression, exp: Expression,
): Expression { ): Expression.Literal {
if (!Array.isArray(exp)) return exp; if (exp instanceof Expression.Literal) return exp;
if (exp[0] === "PARAM" && typeof exp[1] === "string") { else if (exp instanceof Expression.FunctionCall) {
let name = exp[1]; if (exp.name === "NOW")
if (name === "id") name = "DeviceID.ID"; return new Expression.Literal(sessionContext.timestamp);
else if (name === "serialNumber") name = "DeviceID.SerialNumber"; if (exp.name === "REMOTE_ADDRESS")
else if (name === "productClass") name = "DeviceID.ProductClass"; return new Expression.Literal(
else if (name === "oui") name = "DeviceID.OUI"; getRequestOrigin(sessionContext.httpRequest).remoteAddress,
else if (name === "remoteAddress") );
return getRequestOrigin(sessionContext.httpRequest).remoteAddress; } else if (exp instanceof Expression.Parameter) {
const deviceData = sessionContext.deviceData; const deviceData = sessionContext.deviceData;
const paths = deviceData.paths; const paths = deviceData.paths;
const path = paths.get(Path.parse(name)); const path = paths.get(exp.path.toString());
if (path) { if (path) {
const attrs = deviceData.attributes.get(path, 1); const attrs = deviceData.attributes.get(path, 1);
if (attrs?.value?.[1]) return attrs.value[1][0]; if (attrs?.value?.[1]) return new Expression.Literal(attrs.value[1][0]);
} }
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS")
return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
} }
return exp; return new Expression.Literal(null);
} }
export async function inform( export async function inform(
@@ -132,9 +128,9 @@ export async function inform(
rpcReq: InformRequest, rpcReq: InformRequest,
): Promise<AcsResponse> { ): Promise<AcsResponse> {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
const params: [Path, number, Attributes][] = [ const params: [string, number, Attributes][] = [
[ [
Path.parse("DeviceID.Manufacturer"), "DeviceID.Manufacturer",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -144,7 +140,7 @@ export async function inform(
], ],
[ [
Path.parse("DeviceID.OUI"), "DeviceID.OUI",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -154,7 +150,7 @@ export async function inform(
], ],
[ [
Path.parse("DeviceID.ProductClass"), "DeviceID.ProductClass",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -164,7 +160,7 @@ export async function inform(
], ],
[ [
Path.parse("DeviceID.SerialNumber"), "DeviceID.SerialNumber",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -177,7 +173,7 @@ export async function inform(
for (const p of rpcReq.parameterList) { for (const p of rpcReq.parameterList) {
const path = p[0]; const path = p[0];
params.push([ params.push([
path, path.toString(),
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -187,7 +183,7 @@ export async function inform(
} }
params.push([ params.push([
Path.parse("Events.Inform"), "Events.Inform",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -198,7 +194,7 @@ export async function inform(
for (const e of rpcReq.event) { for (const e of rpcReq.event) {
params.push([ params.push([
Path.parse(`Events.${encodeTag(e.replace(/\s+/g, "_"))}`), `Events.${encodeTag(e.replace(/\s+/g, "_"))}`,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -210,7 +206,7 @@ export async function inform(
if (sessionContext.new) { if (sessionContext.new) {
params.push([ params.push([
Path.parse("DeviceID.ID"), "DeviceID.ID",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -219,7 +215,7 @@ export async function inform(
}, },
]); ]);
params.push([ params.push([
Path.parse("Events.Registered"), "Events.Registered",
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -235,7 +231,7 @@ export async function inform(
let toClear = null; let toClear = null;
for (const p of params) { for (const p of params) {
// Don't need to clear wildcards for Events // Don't need to clear wildcards for Events
if (p[0].segments[0] === "Events") { if (p[0].startsWith("Events.")) {
device.set(sessionContext.deviceData, p[0], p[1], p[2]); device.set(sessionContext.deviceData, p[0], p[1], p[2]);
} else { } else {
toClear = device.set( toClear = device.set(
@@ -301,69 +297,49 @@ export async function transferComplete(
let toClear = null; let toClear = null;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastDownload`),
);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, `Downloads.${instance}.LastDownload`,
timestamp, timestamp,
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] }, { value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
toClear, toClear,
); );
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastFileType`),
);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, `Downloads.${instance}.LastFileType`,
timestamp, timestamp,
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] }, { value: [timestamp, [operation.args.fileType, "xsd:string"]] },
toClear, toClear,
); );
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastFileName`),
);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, `Downloads.${instance}.LastFileName`,
timestamp, timestamp,
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] }, { value: [timestamp, [operation.args.fileName, "xsd:string"]] },
toClear, toClear,
); );
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastTargetFileName`),
);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, `Downloads.${instance}.LastTargetFileName`,
timestamp, timestamp,
{ value: [timestamp, [operation.args.targetFileName, "xsd:string"]] }, { value: [timestamp, [operation.args.targetFileName, "xsd:string"]] },
toClear, toClear,
); );
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.StartTime`),
);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, `Downloads.${instance}.StartTime`,
timestamp, timestamp,
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] }, { value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
toClear, toClear,
); );
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.CompleteTime`),
);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, `Downloads.${instance}.CompleteTime`,
timestamp, timestamp,
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] }, { value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
toClear, toClear,
@@ -387,22 +363,22 @@ function revertDownloadParameters(
): void { ): void {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p; const lastDownloadPath = sessionContext.deviceData.paths.add(
`Downloads.${instance}.LastDownload`,
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastDownload`),
); );
const lastDownload = sessionContext.deviceData.attributes.get(p); const lastDownload =
sessionContext.deviceData.attributes.get(lastDownloadPath);
p = sessionContext.deviceData.paths.add( const toClear = device.set(
Path.parse(`Downloads.${instance}.Download`), sessionContext.deviceData,
`Downloads.${instance}.Download`,
timestamp,
{
value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
},
); );
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
});
if (toClear) { if (toClear) {
for (const c of toClear) for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]); device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
@@ -426,11 +402,10 @@ export async function timeoutOperations(
throw new Error(`Unknown operation name ${operation.name}`); throw new Error(`Unknown operation name ${operation.name}`);
const DOWNLOAD_TIMEOUT = const DOWNLOAD_TIMEOUT =
+localCache.getConfig( localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.downloadTimeout", "cwmp.downloadTimeout",
{}, 3600,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) * 1000; ) * 1000;
@@ -443,11 +418,10 @@ export async function timeoutOperations(
commandKey: commandKey, commandKey: commandKey,
}); });
const SUCCESS_ON_TIMEOUT = +localCache.getConfig( const SUCCESS_ON_TIMEOUT = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.downloadSuccessOnTimeout", "cwmp.downloadSuccessOnTimeout",
{}, false,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
@@ -486,16 +460,15 @@ export async function timeoutOperations(
export function addProvisions( export function addProvisions(
sessionContext: SessionContext, sessionContext: SessionContext,
channel: string, channel: string,
provisions: [string, ...Expression[]][], provisions: [string, ...Value[]][],
): void { ): void {
// Multiply by two because every iteration is two // Multiply by two because every iteration is two
// phases: read and update // phases: read and update
const MAX_ITERATIONS = const MAX_ITERATIONS =
+localCache.getConfig( localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations", "cwmp.maxCommitIterations",
{}, 32,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) * 2; ) * 2;
@@ -549,8 +522,7 @@ export function clearProvisions(sessionContext: SessionContext): void {
+localCache.getConfig( +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations", "cwmp.maxCommitIterations",
{}, 32,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) * 2; ) * 2;
@@ -848,22 +820,20 @@ function runDeclarations(
(path.alias | path.wildcard) & 1 || (path.alias | path.wildcard) & 1 ||
path.segments[0] === "VirtualParameters" path.segments[0] === "VirtualParameters"
) { ) {
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters")); sessionContext.deviceData.paths.add("VirtualParameters");
if ((path.alias | path.wildcard) & 2) { if ((path.alias | path.wildcard) & 2) {
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters.*")); sessionContext.deviceData.paths.add("VirtualParameters.*");
for (const k of Object.keys(allVirtualParameters)) { for (const k of Object.keys(allVirtualParameters)) {
sessionContext.deviceData.paths.add( sessionContext.deviceData.paths.add(`VirtualParameters.${k}`);
Path.parse(`VirtualParameters.${k}`),
);
} }
} }
} }
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot") if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot")
sessionContext.deviceData.paths.add(Path.parse("Reboot")); sessionContext.deviceData.paths.add("Reboot");
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset") if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset")
sessionContext.deviceData.paths.add(Path.parse("FactoryReset")); sessionContext.deviceData.paths.add("FactoryReset");
if (path.alias) { if (path.alias) {
const aliasDecs = device.getAliasDeclarations( const aliasDecs = device.getAliasDeclarations(
@@ -871,12 +841,12 @@ function runDeclarations(
declaration.pathGet || 1, declaration.pathGet || 1,
); );
for (const ad of aliasDecs) { for (const ad of aliasDecs) {
const p = sessionContext.deviceData.paths.add(ad.path); const p = sessionContext.deviceData.paths.add(ad.path.toString());
allDeclareTimestamps.set( allDeclareTimestamps.set(
p, p,
Math.max(ad.pathGet || 1, allDeclareTimestamps.get(p) || 0), Math.max(ad.pathGet || 1, allDeclareTimestamps.get(p) || 0),
); );
let attrTrackers; let attrTrackers: string[];
if (ad.attrGet) { if (ad.attrGet) {
attrTrackers = Object.keys(ad.attrGet); attrTrackers = Object.keys(ad.attrGet);
mergeAttributeTimestamps(p, ad.attrGet); mergeAttributeTimestamps(p, ad.attrGet);
@@ -884,7 +854,7 @@ function runDeclarations(
device.track( device.track(
sessionContext.deviceData, sessionContext.deviceData,
p, p.toString(),
"prerequisite", "prerequisite",
attrTrackers, attrTrackers,
); );
@@ -900,14 +870,14 @@ function runDeclarations(
mergeAttributeTimestamps(u, declaration.attrGet); mergeAttributeTimestamps(u, declaration.attrGet);
} }
} else { } else {
path = sessionContext.deviceData.paths.add(path); path = sessionContext.deviceData.paths.add(path.toString());
allDeclareTimestamps.set( allDeclareTimestamps.set(
path, path,
Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(path) || 0), Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(path) || 0),
); );
if (declaration.attrGet) if (declaration.attrGet)
mergeAttributeTimestamps(path, declaration.attrGet); mergeAttributeTimestamps(path, declaration.attrGet);
device.track(sessionContext.deviceData, path, "prerequisite"); device.track(sessionContext.deviceData, path.toString(), "prerequisite");
} }
if (declaration.attrSet) { if (declaration.attrSet) {
@@ -928,7 +898,7 @@ function runDeclarations(
} }
if (declaration.pathSet != null) { if (declaration.pathSet != null) {
let minInstances, maxInstances; let minInstances: number, maxInstances: number;
if (Array.isArray(declaration.pathSet)) { if (Array.isArray(declaration.pathSet)) {
minInstances = declaration.pathSet[0]; minInstances = declaration.pathSet[0];
maxInstances = declaration.pathSet[1]; maxInstances = declaration.pathSet[1];
@@ -938,17 +908,19 @@ function runDeclarations(
let parent = path.slice(0, -1); let parent = path.slice(0, -1);
let keys; let keys: Record<string, string>;
if (Array.isArray(path.segments[path.length - 1])) { if (path.segments[path.length - 1] instanceof Expression) {
keys = {}; keys = {};
for (const [p, v] of path.segments[path.length - 1]) for (const [p, v] of device.expressionToAlias(
keys[p.toString()] = v; path.segments[path.length - 1] as Expression,
))
keys[p.toString()] = v as string;
} else if (path.segments[path.length - 1] === "*") { } else if (path.segments[path.length - 1] === "*") {
keys = {}; keys = {};
} }
if (!parent.wildcard && !parent.alias) { if (!parent.wildcard && !parent.alias) {
parent = sessionContext.deviceData.paths.add(parent); parent = sessionContext.deviceData.paths.add(parent.toString());
if (!unpacked) if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path); unpacked = device.unpack(sessionContext.deviceData, path);
@@ -1143,8 +1115,7 @@ export async function rpcRequest(
+localCache.getConfig( +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations", "cwmp.maxCommitIterations",
{}, 32,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) * 2; ) * 2;
@@ -1184,7 +1155,7 @@ export async function rpcRequest(
); );
const timestamp = sessionContext.timestamp + sessionContext.iteration; const timestamp = sessionContext.timestamp + sessionContext.iteration;
let toClear; let toClear: Clear[];
const allVirtualParameters = localCache.getVirtualParameters( const allVirtualParameters = localCache.getVirtualParameters(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
@@ -1198,7 +1169,7 @@ export async function rpcRequest(
if (!sessionContext.deviceData.attributes.has(declaration[0])) { if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
declaration[0], declaration[0].toString(),
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }, { object: [timestamp, 1], writable: [timestamp, 0] },
toClear, toClear,
@@ -1211,7 +1182,7 @@ export async function rpcRequest(
for (const k of Object.keys(allVirtualParameters)) { for (const k of Object.keys(allVirtualParameters)) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`VirtualParameters.${k}`), `VirtualParameters.${k}`,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -1221,7 +1192,7 @@ export async function rpcRequest(
} }
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
declaration[0], declaration[0].toString(),
timestamp, timestamp,
null, null,
toClear, toClear,
@@ -1235,7 +1206,7 @@ export async function rpcRequest(
if (!sessionContext.deviceData.attributes.has(declaration[0])) { if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
declaration[0], declaration[0].toString(),
timestamp, timestamp,
{ object: [timestamp, 0] }, { object: [timestamp, 0] },
toClear, toClear,
@@ -1247,7 +1218,7 @@ export async function rpcRequest(
} }
} }
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.findCompat(
declaration[0], declaration[0],
false, false,
true, true,
@@ -1290,7 +1261,7 @@ export async function rpcRequest(
return rpcRequest(sessionContext, null); return rpcRequest(sessionContext, null);
} }
let toClear; let toClear: Clear[];
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
// Update tags // Update tags
@@ -1299,7 +1270,7 @@ export async function rpcRequest(
if (v && !c) { if (v && !c) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p.toString(),
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -1311,7 +1282,7 @@ export async function rpcRequest(
} else if (c && !v) { } else if (c && !v) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p.toString(),
timestamp, timestamp,
null, null,
toClear, toClear,
@@ -1320,11 +1291,11 @@ export async function rpcRequest(
} }
// Downloads // Downloads
let index; let index: number;
for (const instance of sessionContext.syncState.downloadsToCreate) { for (const instance of sessionContext.syncState.downloadsToCreate) {
if (index == null) { if (index == null) {
index = 0; index = 0;
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.findCompat(
Path.parse("Downloads.*"), Path.parse("Downloads.*"),
false, false,
true, true,
@@ -1341,7 +1312,7 @@ export async function rpcRequest(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse("Downloads"), "Downloads",
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] }, { object: [timestamp, 1], writable: [timestamp, 1] },
toClear, toClear,
@@ -1349,7 +1320,7 @@ export async function rpcRequest(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${index}`), `Downloads.${index}`,
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] }, { object: [timestamp, 1], writable: [timestamp, 1] },
toClear, toClear,
@@ -1383,7 +1354,7 @@ export async function rpcRequest(
for (const [k, v] of Object.entries(params)) { for (const [k, v] of Object.entries(params)) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${index}.${k}`), `Downloads.${index}.${k}`,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -1399,7 +1370,7 @@ export async function rpcRequest(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${index}.*`), `Downloads.${index}.*`,
timestamp, timestamp,
null, null,
toClear, toClear,
@@ -1411,7 +1382,7 @@ export async function rpcRequest(
for (const instance of sessionContext.syncState.downloadsToDelete) { for (const instance of sessionContext.syncState.downloadsToDelete) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
instance, instance.toString(),
timestamp, timestamp,
null, null,
toClear, toClear,
@@ -1432,7 +1403,7 @@ export async function rpcRequest(
if (val[0] !== attrs.value[1][0]) { if (val[0] !== attrs.value[1][0]) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p.toString(),
timestamp, timestamp,
{ value: [timestamp, val] }, { value: [timestamp, val] },
toClear, toClear,
@@ -1504,7 +1475,7 @@ export async function rpcRequest(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`VirtualParameters.${vparams[i][0]}`), `VirtualParameters.${vparams[i][0]}`,
timestamp, timestamp,
vpu, vpu,
toClear, toClear,
@@ -1527,7 +1498,7 @@ function generateGetRpcRequest(
for (const path of syncState.refreshAttributes.exist) { for (const path of syncState.refreshAttributes.exist) {
let found = false; let found = false;
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.findCompat(
path, path,
false, false,
true, true,
@@ -1547,7 +1518,9 @@ function generateGetRpcRequest(
} }
if (!found) { if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); const p = sessionContext.deviceData.paths.add(
path.slice(0, -1).toString(),
);
syncState.gpn.add(p); syncState.gpn.add(p);
const f = 1 << p.length; const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1557,7 +1530,7 @@ function generateGetRpcRequest(
for (const path of syncState.refreshAttributes.object) { for (const path of syncState.refreshAttributes.object) {
let found = false; let found = false;
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.findCompat(
path, path,
false, false,
true, true,
@@ -1577,7 +1550,9 @@ function generateGetRpcRequest(
} }
if (!found) { if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); const p = sessionContext.deviceData.paths.add(
path.slice(0, -1).toString(),
);
syncState.gpn.add(p); syncState.gpn.add(p);
const f = 1 << p.length; const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1586,7 +1561,7 @@ function generateGetRpcRequest(
syncState.refreshAttributes.object.clear(); syncState.refreshAttributes.object.clear();
for (const path of syncState.refreshAttributes.writable) { for (const path of syncState.refreshAttributes.writable) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); const p = sessionContext.deviceData.paths.add(path.slice(0, -1).toString());
syncState.gpn.add(p); syncState.gpn.add(p);
const f = 1 << p.length; const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1597,8 +1572,7 @@ function generateGetRpcRequest(
const GPN_NEXT_LEVEL = localCache.getConfig( const GPN_NEXT_LEVEL = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.gpnNextLevel", "cwmp.gpnNextLevel",
{}, 0,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) as number; ) as number;
@@ -1609,11 +1583,10 @@ function generateGetRpcRequest(
// Skip root GPN workaround // Skip root GPN workaround
if (path && !path.length) { if (path && !path.length) {
const SKIP_ROOT_GPN = !!localCache.getConfig( const SKIP_ROOT_GPN = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.skipRootGpn", "cwmp.skipRootGpn",
{}, false,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
@@ -1634,7 +1607,7 @@ function generateGetRpcRequest(
let est = 0; let est = 0;
if (path.length >= GPN_NEXT_LEVEL) { if (path.length >= GPN_NEXT_LEVEL) {
const patterns: [Path, number][] = [[path, 0]]; const patterns: [Path, number][] = [[path, 0]];
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.findCompat(
path, path,
true, true,
false, false,
@@ -1651,7 +1624,7 @@ function generateGetRpcRequest(
syncState.gpn.delete(path); syncState.gpn.delete(path);
} else { } else {
nextLevel = false; nextLevel = false;
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.findCompat(
path, path,
false, false,
true, true,
@@ -1672,8 +1645,7 @@ function generateGetRpcRequest(
const GPV_BATCH_SIZE = localCache.getConfig( const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize", "cwmp.gpvBatchSize",
{}, 32,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) as number; ) as number;
@@ -1703,8 +1675,7 @@ function generateGetRpcRequest(
const GPV_BATCH_SIZE = localCache.getConfig( const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize", "cwmp.gpvBatchSize",
{}, 32,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) as number; ) as number;
@@ -1764,11 +1735,10 @@ function generateSetRpcRequest(
const deviceData = sessionContext.deviceData; const deviceData = sessionContext.deviceData;
const SKIP_WRITABLE_CHECK = !!localCache.getConfig( const SKIP_WRITABLE_CHECK = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.skipWritableCheck", "cwmp.skipWritableCheck",
{}, false,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
@@ -1810,24 +1780,21 @@ function generateSetRpcRequest(
const GPV_BATCH_SIZE = localCache.getConfig( const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize", "cwmp.gpvBatchSize",
{}, 32,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
) as number; ) as number;
const DATETIME_MILLISECONDS = !!localCache.getConfig( const DATETIME_MILLISECONDS = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds", "cwmp.datetimeMilliseconds",
{}, true,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
const BOOLEAN_LITERAL = !!localCache.getConfig( const BOOLEAN_LITERAL = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.booleanLiteral", "cwmp.booleanLiteral",
{}, true,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
@@ -1904,15 +1871,13 @@ function generateSetRpcRequest(
const t2 = attrs?.value?.[1]?.[0] as number; const t2 = attrs?.value?.[1]?.[0] as number;
if (!(t <= t2)) { if (!(t <= t2)) {
const fileTypeAttrs = deviceData.attributes.get( const fileTypeAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType"))), deviceData.paths.get(p.slice(0, -1).toString() + ".FileType"),
); );
const fileNameAttrs = deviceData.attributes.get( const fileNameAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileName"))), deviceData.paths.get(p.slice(0, -1).toString() + ".FileName"),
); );
const targetFileNameAttrs = deviceData.attributes.get( const targetFileNameAttrs = deviceData.attributes.get(
deviceData.paths.get( deviceData.paths.get(p.slice(0, -1).toString() + ".TargetFileName"),
p.slice(0, -1).concat(Path.parse("TargetFileName")),
),
); );
return { return {
@@ -1928,7 +1893,7 @@ function generateSetRpcRequest(
// Reboot // Reboot
if (syncState.reboot > 0 && syncState.reboot <= sessionContext.timestamp) { if (syncState.reboot > 0 && syncState.reboot <= sessionContext.timestamp) {
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot")); const p = sessionContext.deviceData.paths.get("Reboot");
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
const t = attrs?.value?.[1][0] as number; const t = attrs?.value?.[1][0] as number;
if (!(t >= syncState.reboot)) { if (!(t >= syncState.reboot)) {
@@ -1942,7 +1907,7 @@ function generateSetRpcRequest(
syncState.factoryReset > 0 && syncState.factoryReset > 0 &&
syncState.factoryReset <= sessionContext.timestamp syncState.factoryReset <= sessionContext.timestamp
) { ) {
const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset")); const p = sessionContext.deviceData.paths.get("FactoryReset");
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null; const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
const t = attrs?.value?.[1][0] as number; const t = attrs?.value?.[1][0] as number;
if (!(t >= syncState.factoryReset)) { if (!(t >= syncState.factoryReset)) {
@@ -2064,8 +2029,7 @@ function processDeclarations(
const deviceData = sessionContext.deviceData; const deviceData = sessionContext.deviceData;
const syncState = sessionContext.syncState; const syncState = sessionContext.syncState;
const root = sessionContext.deviceData.paths.add(Path.parse("")); const paths = deviceData.paths.findCompat(Path.root, false, true, 99);
const paths = deviceData.paths.find(root, false, true, 99);
paths.sort((a, b): number => paths.sort((a, b): number =>
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard, a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard,
); );
@@ -2358,7 +2322,7 @@ function processDeclarations(
allDeclareAttributeTimestamps.size || allDeclareAttributeTimestamps.size ||
allDeclareAttributeValues.size allDeclareAttributeValues.size
) )
func(root, 1, 0, paths); func(Path.root, 1, 0, [Path.root, ...paths]);
return virtualParameterDeclarations; return virtualParameterDeclarations;
} }
@@ -2372,7 +2336,7 @@ function processInstances(
maxInstances: number, maxInstances: number,
defer: boolean, defer: boolean,
): void { ): void {
parent = sessionContext.deviceData.paths.add(parent); parent = sessionContext.deviceData.paths.add(parent.toString());
let instancesToCreate: InstanceSet, instancesToDelete: Set<Path>; let instancesToCreate: InstanceSet, instancesToDelete: Set<Path>;
if (parent.segments[0] === "Downloads") { if (parent.segments[0] === "Downloads") {
if (parent.length !== 1) return; if (parent.length !== 1) return;
@@ -2491,19 +2455,17 @@ export async function rpcResponse(
if (!parameterList.length) { if (!parameterList.length) {
sessionContext.rpcRequest = null; sessionContext.rpcRequest = null;
} else { } else {
const DATETIME_MILLISECONDS = !!localCache.getConfig( const DATETIME_MILLISECONDS = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds", "cwmp.datetimeMilliseconds",
{}, true,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
const BOOLEAN_LITERAL = !!localCache.getConfig( const BOOLEAN_LITERAL = localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.booleanLiteral", "cwmp.booleanLiteral",
{}, true,
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e), (e) => configContextCallback(sessionContext, e),
); );
@@ -2543,7 +2505,7 @@ export async function rpcResponse(
} }
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
path, path.toString(),
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -2562,7 +2524,7 @@ export async function rpcResponse(
}); });
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(p), p,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -2589,7 +2551,7 @@ export async function rpcResponse(
} }
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
path, path.toString(),
timestamp, timestamp,
{ {
notification: [timestamp, notification], notification: [timestamp, notification],
@@ -2608,7 +2570,7 @@ export async function rpcResponse(
}); });
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(p), p,
timestamp, timestamp,
{ {
notification: [timestamp, 0], notification: [timestamp, 0],
@@ -2623,7 +2585,8 @@ export async function rpcResponse(
return invalidResponse("Response name does not match request name"); return invalidResponse("Response name does not match request name");
let root: Path; let root: Path;
if (rpcReq.parameterPath.endsWith(".")) if (!rpcReq.parameterPath) root = Path.root;
else if (rpcReq.parameterPath.endsWith("."))
root = Path.parse(rpcReq.parameterPath.slice(0, -1)); root = Path.parse(rpcReq.parameterPath.slice(0, -1));
else root = Path.parse(rpcReq.parameterPath); else root = Path.parse(rpcReq.parameterPath);
@@ -2667,7 +2630,7 @@ export async function rpcResponse(
"VirtualParameters", "VirtualParameters",
"Downloads", "Downloads",
]) { ]) {
const p = sessionContext.deviceData.paths.get(Path.parse(n)); const p = sessionContext.deviceData.paths.get(n);
if (p && sessionContext.deviceData.attributes.has(p)) if (p && sessionContext.deviceData.attributes.has(p))
sessionContext.deviceData.timestamps.set(p, timestamp); sessionContext.deviceData.timestamps.set(p, timestamp);
} }
@@ -2693,7 +2656,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
path, path.toString(),
timestamp, timestamp,
{ {
object: [timestamp, object ? 1 : 0], object: [timestamp, object ? 1 : 0],
@@ -2706,7 +2669,7 @@ export async function rpcResponse(
for (const path of wildcardParams) { for (const path of wildcardParams) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
path, path.toString(),
timestamp, timestamp,
null, null,
toClear, toClear,
@@ -2719,7 +2682,7 @@ export async function rpcResponse(
for (const p of rpcReq.parameterList) { for (const p of rpcReq.parameterList) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(p[0]), p[0],
timestamp + 1, timestamp + 1,
{ {
object: [timestamp + 1, 0], object: [timestamp + 1, 0],
@@ -2755,7 +2718,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(p[0]), p[0],
timestamp + 1, timestamp + 1,
attrs, attrs,
toClear, toClear,
@@ -2767,7 +2730,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(rpcReq.objectName + rpcRes.instanceNumber), rpcReq.objectName + rpcRes.instanceNumber,
timestamp + 1, timestamp + 1,
{ object: [timestamp + 1, 1] }, { object: [timestamp + 1, 1] },
toClear, toClear,
@@ -2778,7 +2741,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(rpcReq.objectName.slice(0, -1)), rpcReq.objectName.slice(0, -1),
timestamp + 1, timestamp + 1,
null, null,
toClear, toClear,
@@ -2789,7 +2752,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse("Reboot"), "Reboot",
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear, toClear,
@@ -2800,7 +2763,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse("FactoryReset"), "FactoryReset",
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear, toClear,
@@ -2811,7 +2774,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.Download`), `Downloads.${rpcReq.instance}.Download`,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear, toClear,
@@ -2820,7 +2783,7 @@ export async function rpcResponse(
if (rpcRes.status === 0) { if (rpcRes.status === 0) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastDownload`), `Downloads.${rpcReq.instance}.LastDownload`,
timestamp + 1, timestamp + 1,
{ {
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]], value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]],
@@ -2830,7 +2793,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastFileType`), `Downloads.${rpcReq.instance}.LastFileType`,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear, toClear,
@@ -2838,7 +2801,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastFileName`), `Downloads.${rpcReq.instance}.LastFileName`,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear, toClear,
@@ -2846,7 +2809,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastTargetFileName`), `Downloads.${rpcReq.instance}.LastTargetFileName`,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear, toClear,
@@ -2854,7 +2817,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.StartTime`), `Downloads.${rpcReq.instance}.StartTime`,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] }, { value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
toClear, toClear,
@@ -2862,7 +2825,7 @@ export async function rpcResponse(
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.CompleteTime`), `Downloads.${rpcReq.instance}.CompleteTime`,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] }, { value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
toClear, toClear,
@@ -2977,7 +2940,7 @@ export async function deserialize(
const deviceData = initDeviceData(); const deviceData = initDeviceData();
for (const r of sessionContext.deviceData as unknown as any[]) { for (const r of sessionContext.deviceData as unknown as any[]) {
const path = deviceData.paths.add(Path.parse(r[0])); const path = deviceData.paths.add(r[0]);
if (r[1]) deviceData.trackers.set(path, r[1]); if (r[1]) deviceData.trackers.set(path, r[1]);
@@ -2999,8 +2962,8 @@ export async function serialize(
): Promise<string> { ): Promise<string> {
const deviceData = []; const deviceData = [];
for (const path of sessionContext.deviceData.paths.find( for (const path of sessionContext.deviceData.paths.findCompat(
Path.parse(""), Path.root,
false, false,
false, false,
99, 99,
+5 -15
View File
@@ -4,8 +4,7 @@ import Path from "./common/path.ts";
import PathSet from "./common/path-set.ts"; import PathSet from "./common/path-set.ts";
import VersionedMap from "./versioned-map.ts"; import VersionedMap from "./versioned-map.ts";
import InstanceSet from "./instance-set.ts"; import InstanceSet from "./instance-set.ts";
import Expression, { Value } from "./common/expression.ts";
export type Expression = string | number | boolean | null | any[];
export interface Fault { export interface Fault {
code: string; code: string;
@@ -160,7 +159,7 @@ export interface Task {
fileName?: string; fileName?: string;
targetFileName?: string; targetFileName?: string;
expiry?: number; expiry?: number;
provisions?: [string, ...Expression[]][]; provisions?: [string, ...Value[]][];
} }
export interface Operation { export interface Operation {
@@ -470,7 +469,7 @@ export interface Permissions {
[resource: string]: { [resource: string]: {
access: number; access: number;
filter: Expression; filter: Expression;
validate?: Expression; validate: Expression;
}; };
}; };
}; };
@@ -479,7 +478,7 @@ export interface Permissions {
export type PermissionSet = { export type PermissionSet = {
[resource: string]: { [resource: string]: {
access: number; access: number;
validate?: Expression; validate: Expression;
filter: Expression; filter: Expression;
}; };
}[]; }[];
@@ -488,16 +487,7 @@ export interface Config {
[name: string]: Expression; [name: string]: Expression;
} }
export interface UiConfig { export type UiConfig = Record<string, string>;
filters: Record<string, unknown>;
device: Record<string, unknown>;
index: Record<string, unknown>;
overview: {
charts?: Record<string, unknown>;
groups?: Record<string, unknown>;
};
pageSize?: Expression;
}
export interface SoapMessage { export interface SoapMessage {
id: string; id: string;
+12 -5
View File
@@ -214,8 +214,17 @@ router.get("/", async (ctx) => {
// be problematic when using relatives asset paths in HTML // be problematic when using relatives asset paths in HTML
if (ctx.path.endsWith("//")) return; if (ctx.path.endsWith("//")) return;
const permissionSets: PermissionSet[] = const ps: PermissionSet[] = ctx.state.authorizer.getPermissionSets();
ctx.state.authorizer.getPermissionSets(); const permissionSets = ps.map((p) =>
p.map((s) =>
Object.fromEntries(
Object.entries(s).map(([resource, { access, validate, filter }]) => [
resource,
{ access, validate: validate.toString(), filter: filter.toString() },
]),
),
),
);
let wizard = ""; let wizard = "";
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
@@ -231,9 +240,7 @@ router.get("/", async (ctx) => {
<body class="h-full bg-stone-100"> <body class="h-full bg-stone-100">
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript> <noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
<script> <script>
window.clientConfig = ${JSON.stringify({ window.clientConfig = ${JSON.stringify(localCache.getUiConfig(ctx.state.configSnapshot))};
ui: localCache.getUiConfig(ctx.state.configSnapshot),
})};
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)}; window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
window.genieacsVersion = ${JSON.stringify(VERSION)}; window.genieacsVersion = ${JSON.stringify(VERSION)};
window.username = ${JSON.stringify( window.username = ${JSON.stringify(
+213 -141
View File
@@ -3,11 +3,11 @@ import Router from "@koa/router";
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
import * as db from "./db.ts"; import * as db from "./db.ts";
import * as apiFunctions from "../api-functions.ts"; import * as apiFunctions from "../api-functions.ts";
import { evaluate, and, extractParams } from "../common/expression/util.ts"; import Expression, { extractPaths } from "../common/expression.ts";
import { parse } from "../common/expression/parser.ts"; import Path from "../common/path.ts";
import * as logger from "../logger.ts"; import * as logger from "../logger.ts";
import { getConfig } from "../ui/local-cache.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 { generateSalt, hashPassword } from "../auth.ts";
import { del } from "../cache.ts"; import { del } from "../cache.ts";
import Authorizer from "../common/authorizer.ts"; import Authorizer from "../common/authorizer.ts";
@@ -67,11 +67,14 @@ router.get(`/devices/:id.csv`, async (ctx) => {
id: ctx.params.id, id: ctx.params.id,
}; };
const filter = and(authorizer.getFilter("devices", 2), [ const filter = Expression.and(
"=", authorizer.getFilter("devices", 2),
["PARAM", RESOURCE_IDS.devices], new Expression.Binary(
ctx.params.id, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS.devices)),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess("devices", 2)) { if (!authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
@@ -89,36 +92,51 @@ router.get(`/devices/:id.csv`, async (ctx) => {
); );
const lines: string[] = [ 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 keys = Object.keys(device).sort();
const p = device[k]; let prevParam = "";
let value = ""; let attrs: Record<string, string | number | boolean | null> = {};
let type = "";
if (p.value) { function flushRow(): void {
value = p.value[0]; if (!prevParam) return;
type = p.value[1]; let value: string | number | boolean | null = attrs["value"] ?? "";
if (type === "xsd:dateTime" && typeof value === "number") if (attrs["type"] === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toJSON(); value = new Date(value).toJSON();
}
const row = [ const row = [
k, prevParam,
p.object, attrs["object"] ?? "",
new Date(p.objectTimestamp).toJSON(), attrs["writable"] ?? "",
p.writable,
new Date(p.writableTimestamp).toJSON(),
`"${String(value).replace(/"/g, '""')}"`, `"${String(value).replace(/"/g, '""')}"`,
type, attrs["type"] ?? "",
new Date(p.valueTimestamp).toJSON(), attrs["timestamp"] != null ? new Date(+attrs["timestamp"]).toJSON() : "",
p.notification, attrs["valueTimestamp"] != null
new Date(p.notificationTimestamp).toJSON(), ? new Date(+attrs["valueTimestamp"]).toJSON()
p.accessList ? p.accessList.join(", ") : "", : "",
new Date(p.accessListTimestamp).toJSON(), attrs["notification"] ?? "",
attrs["accessList"] ?? "",
attrs["attributesTimestamp"] != null
? new Date(+attrs["attributesTimestamp"]).toJSON()
: "",
]; ];
lines.push(row.map((r) => (r != null ? r : "")).join(",")); 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"); ctx.body = lines.join("\n");
logger.accessInfo(log); logger.accessInfo(log);
}); });
@@ -128,7 +146,10 @@ for (const [resource, flags] of Object.entries(resources)) {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
let filter: Expression = authorizer.getFilter(resource, 1); let filter: Expression = authorizer.getFilter(resource, 1);
if (ctx.request.query.filter) 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 = { const log = {
message: `Count ${resource}`, message: `Count ${resource}`,
@@ -144,11 +165,18 @@ for (const [resource, flags] of Object.entries(resources)) {
// Exclude temporary tasks and faults // Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") { if (resource === "tasks" || resource === "faults") {
filter = and(filter, [ const p = new Expression.Parameter(Path.parse("expiry"));
"OR", filter = Expression.and(
[">=", ["PARAM", "expiry"], Date.now() + 60000], filter,
["IS NULL", ["PARAM", "expiry"]], 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); 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] = {}; const options: Parameters<typeof db.query>[2] = {};
let filter: Expression = authorizer.getFilter(resource, 2); let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter) 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.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip; if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort) if (ctx.request.query.sort)
@@ -193,11 +224,18 @@ for (const [resource, flags] of Object.entries(resources)) {
// Exclude temporary tasks and faults // Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") { if (resource === "tasks" || resource === "faults") {
filter = and(filter, [ const p = new Expression.Parameter(Path.parse("expiry"));
"OR", filter = Expression.and(
[">=", ["PARAM", "expiry"], Date.now() + 60000], filter,
["IS NULL", ["PARAM", "expiry"]], Expression.or(
]); new Expression.Binary(
">=",
p,
new Expression.Literal(Date.now() + 60000),
),
new Expression.Unary("IS NULL", p),
),
);
} }
logger.accessInfo(log); logger.accessInfo(log);
@@ -219,7 +257,10 @@ for (const [resource, flags] of Object.entries(resources)) {
const options: Parameters<typeof db.query>[2] = { projection: {} }; const options: Parameters<typeof db.query>[2] = { projection: {} };
let filter: Expression = authorizer.getFilter(resource, 2); let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter) 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.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip; if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort) if (ctx.request.query.sort)
@@ -239,25 +280,38 @@ for (const [resource, flags] of Object.entries(resources)) {
return void (ctx.status = 403); return void (ctx.status = 403);
} }
const columns: Record<string, Expression> = JSON.parse( const columnsStr: Record<string, string> = JSON.parse(
singleParam(ctx.request.query.columns), singleParam(ctx.request.query.columns),
); );
const now = Date.now();
for (const [k, v] of Object.entries(columns)) { const now = Date.now();
const e = evaluate(parse(v as string), null, now); const columns: Record<string, Expression> = Object.fromEntries(
columns[k] = e; Object.entries(columnsStr).map(([k, v]) => {
for (const p of extractParams(e)) let exp = Expression.parse(v);
if (typeof p === "string") options.projection[p] = 1; 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 // Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") { if (resource === "tasks" || resource === "faults") {
filter = and(filter, [ const p = new Expression.Parameter(Path.parse("expiry"));
"OR", filter = Expression.and(
[">=", ["PARAM", "expiry"], Date.now() + 60000], filter,
["IS NULL", ["PARAM", "expiry"]], Expression.or(
]); new Expression.Binary(
">=",
p,
new Expression.Literal(Date.now() + 60000),
),
new Expression.Unary("IS NULL", p),
),
);
} }
logger.accessInfo(log); logger.accessInfo(log);
@@ -272,45 +326,40 @@ for (const [resource, flags] of Object.entries(resources)) {
"\n"; "\n";
for await (const obj of db.query(resource, filter, options)) { for await (const obj of db.query(resource, filter, options)) {
const arr = Object.values(columns).map((exp) => { const arr = Object.values(columns).map((exp) => {
const v = evaluate(exp, obj, null, (e) => { return exp.evaluate((e) => {
if (Array.isArray(e)) { if (e instanceof Expression.Literal) return e;
if (e[0] === "PARAM") { else if (e instanceof Expression.FunctionCall) {
if (resource === "devices") { if (e.name === "NOW") return new Expression.Literal(now);
if (e[1] === "Tags") { if (e.name === "DATE_STRING") {
const tags = []; if (e.args[0] instanceof Expression.Literal)
for (const p in obj) return new Expression.Literal(
if (p.startsWith("Tags.")) new Date(e.args[0].value as number).toJSON(),
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();
}
} }
} 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 new Expression.Literal(null);
return e; }).value;
});
if (Array.isArray(v) || v == null) return "";
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
return v;
}); });
yield arr.join(",") + "\n"; yield arr.join(",") + "\n";
} }
@@ -326,11 +375,15 @@ for (const [resource, flags] of Object.entries(resources)) {
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`, filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
}; };
const filter = and(authorizer.getFilter(resource, 2), [ const filter = Expression.and(
"=", authorizer.getFilter(resource, 2),
["PARAM", RESOURCE_IDS[resource]], new Expression.Binary(
ctx.params.id, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void (ctx.status = 403);
@@ -352,11 +405,14 @@ for (const [resource, flags] of Object.entries(resources)) {
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`, filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
}; };
const filter = and(authorizer.getFilter(resource, 2), [ const filter = Expression.and(
"=", authorizer.getFilter(resource, 2),
["PARAM", RESOURCE_IDS[resource]], new Expression.Binary(
ctx.params.id, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void (ctx.status = 403);
@@ -379,11 +435,14 @@ for (const [resource, flags] of Object.entries(resources)) {
id: ctx.params.id, id: ctx.params.id,
}; };
const filter = and(authorizer.getFilter(resource, 3), [ const filter = Expression.and(
"=", authorizer.getFilter(resource, 3),
["PARAM", RESOURCE_IDS[resource]], new Expression.Binary(
ctx.params.id, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 3)) { if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void (ctx.status = 403);
@@ -467,11 +526,14 @@ router.get("/blob/files/:id", async (ctx) => {
id: id, id: id,
}; };
const filter = and(authorizer.getFilter(resource, 2), [ const filter = Expression.and(
"=", authorizer.getFilter(resource, 2),
["PARAM", RESOURCE_IDS[resource]], new Expression.Binary(
ctx.params.id, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
@@ -566,11 +628,14 @@ router.post("/devices/:id/tasks", async (ctx) => {
let statuses: { _id: string; status: string }[]; let statuses: { _id: string; status: string }[];
try { try {
const filter = and(authorizer.getFilter("devices", 3), [ const filter = Expression.and(
"=", authorizer.getFilter("devices", 3),
["PARAM", "DeviceID.ID"], new Expression.Binary(
deviceId, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
new Expression.Literal(ctx.params.id),
),
);
device = (await db.query("devices", filter).next()).value; device = (await db.query("devices", filter).next()).value;
if (!device) return void (ctx.status = 404); if (!device) return void (ctx.status = 404);
@@ -596,30 +661,31 @@ router.post("/devices/:id/tasks", async (ctx) => {
await releaseLock(`cwmp_session_${deviceId}`, token); await releaseLock(`cwmp_session_${deviceId}`, token);
} }
const now = Date.now();
const onlineThreshold = getConfig( const onlineThreshold = getConfig(
ctx.state.configSnapshot, ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold", "cwmp.deviceOnlineThreshold",
{}, 4000,
Date.now(),
(exp) => { (exp) => {
if (!Array.isArray(exp)) return exp; if (exp instanceof Expression.Literal) return exp;
if (exp[0] === "PARAM") { else if (exp instanceof Expression.Parameter) {
const p = device[exp[1]]; const p = device[exp.path.toString()];
if (p?.value) return p.value[0]; if (p != null) return new Expression.Literal(p);
} else if (exp[0] === "FUNC") { } else if (exp instanceof Expression.FunctionCall) {
if (exp[1] === "REMOTE_ADDRESS") { if (exp.name === "NOW") return new Expression.Literal(now);
if (exp.name === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) { for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`]; 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); let status = await apiFunctions.connectionRequest(deviceId, device);
if (!status) { if (!status) {
@@ -667,11 +733,14 @@ router.post("/devices/:id/tags", async (ctx) => {
tags: ctx.request.body, tags: ctx.request.body,
}; };
const filter = and(authorizer.getFilter("devices", 3), [ const filter = Expression.and(
"=", authorizer.getFilter("devices", 3),
["PARAM", "DeviceID.ID"], new Expression.Binary(
ctx.params.id, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess("devices", 3)) { if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void (ctx.status = 403);
@@ -745,11 +814,14 @@ router.put("/users/:id/password", async (ctx) => {
const newPassword = ctx.request.body.newPassword; const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) { if (ctx.state.user) {
const filter = and(authorizer.getFilter("users", 3), [ const filter = Expression.and(
"=", authorizer.getFilter("users", 3),
["PARAM", RESOURCE_IDS.users], new Expression.Binary(
username, "=",
]); new Expression.Parameter(Path.parse(RESOURCE_IDS["users"])),
new Expression.Literal(username),
),
);
const { value: res } = await db.query("users", filter).next(); const { value: res } = await db.query("users", filter).next();
if (!res) return void (ctx.status = 404); if (!res) return void (ctx.status = 404);
const validate = authorizer.getValidator("users", res); const validate = authorizer.getValidator("users", res);
+66 -136
View File
@@ -2,12 +2,11 @@ import { Script } from "node:vm";
import { Readable } from "node:stream"; import { Readable } from "node:stream";
import { Collection, ObjectId, WithoutId } from "mongodb"; import { Collection, ObjectId, WithoutId } from "mongodb";
import { encodeTag } from "../util.ts"; import { encodeTag } from "../util.ts";
import { evaluate } from "../common/expression/util.ts"; import { Fault, Task } from "../types.ts";
import { Expression, Fault, Task } from "../types.ts";
import { collections, filesBucket } from "../db/db.ts"; import { collections, filesBucket } from "../db/db.ts";
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts"; import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
import * as MongoTypes from "../db/types.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"; import { toMongoQuery } from "../db/synth.ts";
function processDeviceProjection( function processDeviceProjection(
@@ -61,21 +60,8 @@ function parseDate(d: Date): number | string {
return isNaN(n) ? "" + d : n; 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 { export interface FlatDevice {
[param: string]: FlatAttributes; [param: string]: Value;
} }
export function flattenDevice(device: Record<string, unknown>): FlatDevice { 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)) { for (const [name, tree] of Object.entries(input)) {
if (!root) { if (!root) {
if (name === "_lastInform") { if (name === "_lastInform") {
output["Events.Inform"] = { output["Events.Inform"] = parseDate(tree as Date);
value: [parseDate(tree as Date), "xsd:dateTime"], output["Events.Inform:type"] = "xsd:dateTime";
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_registered") { } else if (name === "_registered") {
output["Events.Registered"] = { output["Events.Registered"] = parseDate(tree as Date);
value: [parseDate(tree as Date), "xsd:dateTime"], output["Events.Registered:type"] = "xsd:dateTime";
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_lastBoot") { } else if (name === "_lastBoot") {
output["Events.1_BOOT"] = { output["Events.1_BOOT"] = parseDate(tree as Date);
value: [parseDate(tree as Date), "xsd:dateTime"], output["Events.1_BOOT:type"] = "xsd:dateTime";
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_lastBootstrap") { } else if (name === "_lastBootstrap") {
output["Events.0_BOOTSTRAP"] = { output["Events.0_BOOTSTRAP"] = parseDate(tree as Date);
value: [parseDate(tree as Date), "xsd:dateTime"], output["Events.0_BOOTSTRAP:type"] = "xsd:dateTime";
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_id") { } else if (name === "_id") {
output["DeviceID.ID"] = { output["DeviceID.ID"] = tree as string;
value: [tree as string, "xsd:string"], output["DeviceID.ID:type"] = "xsd:dateTime";
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_deviceId") { } else if (name === "_deviceId") {
output["DeviceID.Manufacturer"] = { output["DeviceID.Manufacturer"] = tree["_Manufacturer"];
value: [tree["_Manufacturer"], "xsd:string"], output["DeviceID.Manufacturer:type"] = "xsd:string";
valueTimestamp: timestamp, output["DeviceID.OUI"] = tree["_OUI"];
writable: false, output["DeviceID.OUI:type"] = "xsd:string";
writableTimestamp: timestamp, output["DeviceID.ProductClass"] = tree["_ProductClass"];
object: false, output["DeviceID.ProductClass:type"] = "xsd:string";
objectTimestamp: timestamp, output["DeviceID.SerialNumber"] = tree["_SerialNumber"];
}; output["DeviceID.SerialNumber:type"] = "xsd:string";
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,
};
} else if (name === "_tags") { } else if (name === "_tags") {
output["Tags"] = { output["Tags:object"] = true;
writable: false, output["Tags:writable"] = true;
writableTimestamp: timestamp,
object: true,
objectTimestamp: timestamp,
};
for (const t of tree as string[]) { for (const t of tree as string[]) {
output[`Tags.${encodeTag(t)}`] = { const et = encodeTag(t);
value: [true, "xsd:boolean"], output[`Tags.${et}`] = true;
valueTimestamp: timestamp, output[`Tags.${et}:type`] = "xsd:boolean";
writable: true, output[`Tags.${et}:writable`] = true;
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} }
} }
} }
@@ -194,40 +118,37 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
else if (+input["_timestamp"] > timestamp) else if (+input["_timestamp"] > timestamp)
childrenTimestamp = +input["_timestamp"]; childrenTimestamp = +input["_timestamp"];
const attrs: FlatAttributes = {}; const r = root ? `${root}.${name}` : name;
if (tree["_value"] != null) { if (tree["_value"] != null) {
attrs.value = [ output[r] =
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"], tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"];
tree["_type"], output[`${r}:type`] = tree["_type"];
]; output[`${r}:timestamp`] = childrenTimestamp;
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp); output[`${r}:valueTimestamp`] = +(
attrs.object = false; tree["_timestamp"] || childrenTimestamp
attrs.objectTimestamp = childrenTimestamp; );
} else if (tree["_object"] != null) { } else if (tree["_object"] != null) {
attrs.object = tree["_object"]; output[`${r}:object`] = tree["_object"];
attrs.objectTimestamp = childrenTimestamp; output[`${r}:timestamp`] = childrenTimestamp;
} }
if (tree["_writable"] != null) { if (tree["_writable"] != null) {
attrs.writable = tree["_writable"]; output[`${r}:writable`] = tree["_writable"];
attrs.writableTimestamp = childrenTimestamp; output[`${r}:timestamp`] = childrenTimestamp;
} }
if (tree["_notification"] != null) { if (tree["_notification"] != null) {
attrs.notification = tree["_notification"]; output[`${r}:notification`] = tree["_notification"];
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1; output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
} }
if (tree["_accessList"] != null) { if (tree["_accessList"] != null) {
attrs.accessList = tree["_accessList"]; output[`${r}:accessList`] = tree["_accessList"].join(",");
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1; output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
} }
const r = root ? `${root}.${name}` : name; recursive(tree, r, output, childrenTimestamp);
output[r] = attrs;
if (attrs.object || tree["object"] == null)
recursive(tree, r, output, childrenTimestamp);
} }
} }
@@ -259,14 +180,12 @@ function flattenPreset(
if (p.precondition) { if (p.precondition) {
try { try {
// Try parse to check expression validity // Try parse to check expression validity
parse(p.precondition as string); Expression.parse(p.precondition as string);
} catch { } catch {
p.precondition = convertOldPrecondition( const e = convertOldPrecondition(JSON.parse(p.precondition as string));
JSON.parse(p.precondition as string), if (e instanceof Expression.Literal && e.value)
); p.precondition = e.toString();
p.precondition = (p.precondition as string).length else p.precondition = "";
? stringify(p.precondition as Expression)
: "";
} }
} }
@@ -285,7 +204,7 @@ function flattenPreset(
) { ) {
p.provision = provision.name; p.provision = provision.name;
p.provisionArgs = provision.args 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); const preset = Object.assign({}, data);
if (!preset.precondition) preset.precondition = ""; if (!preset.precondition) preset.precondition = "";
// Try parse to check expression validity else Expression.parse(preset.precondition as string); // Try parse to check validity
parse(preset.precondition as string);
preset.weight = parseInt(preset.weight as string) || 0; preset.weight = parseInt(preset.weight as string) || 0;
@@ -361,7 +279,13 @@ export async function* query(
options?: QueryOptions, options?: QueryOptions,
): AsyncGenerator<any, void, undefined> { ): AsyncGenerator<any, void, undefined> {
options = options || {}; 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); const q = toMongoQuery(filter, resource);
if (!q) return; if (!q) return;
@@ -408,7 +332,13 @@ export async function* query(
export function count(resource: string, filter: Expression): Promise<number> { export function count(resource: string, filter: Expression): Promise<number> {
const collection = collections[resource] as Collection<any>; 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); const q = toMongoQuery(filter, resource);
if (!q) return Promise.resolve(0); if (!q) return Promise.resolve(0);
return collection.countDocuments(q); return collection.countDocuments(q);
+39 -66
View File
@@ -1,9 +1,7 @@
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts"; import { collections } from "../db/db.ts";
import * as expression from "../common/expression/util.ts"; import Expression, { Value } from "../common/expression.ts";
import { parse } from "../common/expression/parser.ts"; import { Users, Permissions, Config, UiConfig } from "../types.ts";
import { Expression, Users, Permissions, Config, UiConfig } from "../types.ts";
import { LocalCache } from "../local-cache.ts"; import { LocalCache } from "../local-cache.ts";
interface Snapshot { interface Snapshot {
@@ -26,12 +24,14 @@ async function fetchPermissions(): Promise<[string, Permissions]> {
if (!permissions[p.role]) permissions[p.role] = {}; if (!permissions[p.role]) permissions[p.role] = {};
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {}; 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] = { permissions[p.role][p.access][p.resource] = {
access: p.access, 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]; return [h, permissions];
@@ -62,33 +62,18 @@ async function fetchConfig(): Promise<[string, Config, UiConfig]> {
conf.sort((a, b) => (a._id > b._id ? 1 : -1)); conf.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex"); const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
const ui = { const ui: UiConfig = {};
filters: {},
device: {},
index: {},
overview: {},
pageSize: null,
};
const _config = {}; const _config = {};
for (const c of conf) { 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.")) { if (c._id.startsWith("ui.")) {
const keys = c._id.split("."); ui[c._id.slice(3)] = c.value;
if (!(keys[1] in ui)) continue; 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;
} }
// Evaluate expressions to simplify them
const val = Expression.parse(c.value).evaluate((e) => e);
_config[c._id] = val;
} }
return [h, _config, ui]; return [h, _config, ui];
@@ -123,47 +108,35 @@ export async function getRevision(): Promise<string> {
export function getConfig( export function getConfig(
revision: string, revision: string,
key: string, key: string,
context: Record<string, unknown>, dflt: string,
now: number, fn: (e: Expression) => Expression.Literal,
cb?: (e: Expression) => Expression, ): string;
): string | number | boolean | null { 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); const snapshot = localCache.get(revision);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = { const e = snapshot.config[key];
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT", if (!e) return dflt;
"cwmp.debug": "DEBUG", const v = e.evaluate(fn).value;
"cwmp.retryDelay": "RETRY_DELAY", if (typeof v !== typeof dflt) return dflt;
"cwmp.sessionTimeout": "SESSION_TIMEOUT", return v;
"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;
} }
export function getConfigExpression(revision: string, key: string): Expression { export function getConfigExpression(revision: string, key: string): Expression {
+8
View File
@@ -183,4 +183,12 @@ export default class VersionedMap<K, V> {
this.map.delete(k); this.map.delete(k);
} }
} }
public *[Symbol.iterator](): IterableIterator<[K, V]> {
for (const [key, revisions] of this.map) {
const last = revisions[revisions.length - 1];
if (last === NONEXISTENT) continue;
yield [key, last as V];
}
}
} }
-24
View File
@@ -22,7 +22,6 @@
"koa-jwt": "^4.0.3", "koa-jwt": "^4.0.3",
"koa-send": "^5.0.1", "koa-send": "^5.0.1",
"mongodb": "^4.16.0", "mongodb": "^4.16.0",
"parsimmon": "^1.18.1",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"bin": { "bin": {
@@ -46,7 +45,6 @@
"@types/koa-compress": "^4.0.7", "@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.7", "@types/mithril": "^2.2.7",
"@types/node": "^25.3.5", "@types/node": "^25.3.5",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"esbuild": "^0.27.4", "esbuild": "^0.27.4",
"eslint": "^10.0.3", "eslint": "^10.0.3",
@@ -3028,12 +3026,6 @@
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
}, },
"node_modules/@types/parsimmon": {
"version": "1.10.9",
"resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.9.tgz",
"integrity": "sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A==",
"dev": true
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
@@ -5323,11 +5315,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/parsimmon": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8317,12 +8304,6 @@
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
}, },
"@types/parsimmon": {
"version": "1.10.9",
"resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.9.tgz",
"integrity": "sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A==",
"dev": true
},
"@types/qs": { "@types/qs": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
@@ -9871,11 +9852,6 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
}, },
"parsimmon": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
},
"path-exists": { "path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
-2
View File
@@ -38,7 +38,6 @@
"koa-jwt": "^4.0.3", "koa-jwt": "^4.0.3",
"koa-send": "^5.0.1", "koa-send": "^5.0.1",
"mongodb": "^4.16.0", "mongodb": "^4.16.0",
"parsimmon": "^1.18.1",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },
"devDependencies": { "devDependencies": {
@@ -56,7 +55,6 @@
"@types/koa-compress": "^4.0.7", "@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.7", "@types/mithril": "^2.2.7",
"@types/node": "^25.3.5", "@types/node": "^25.3.5",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"esbuild": "^0.27.4", "esbuild": "^0.27.4",
"eslint": "^10.0.3", "eslint": "^10.0.3",
+6 -9
View File
@@ -2,13 +2,13 @@ import test from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import { EJSON } from "bson"; import { EJSON } from "bson";
import { Filter } from "mongodb"; 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 { convertOldPrecondition } from "../lib/db/util.ts";
import { toMongoQuery } from "../lib/db/synth.ts"; import { toMongoQuery } from "../lib/db/synth.ts";
void test("convertOldPrecondition", () => { void test("convertOldPrecondition", () => {
const tests = [ const tests = [
[{}, "true"], [{}, "TRUE"],
[{ test: "test" }, 'test = "test"'], [{ test: "test" }, 'test = "test"'],
[{ test: { $eq: "test" } }, 'test = "test"'], [{ test: { $eq: "test" } }, 'test = "test"'],
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'], [{ 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"'], [{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
[ [
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] }, { $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" } }, { test: { $gte: "test1", $ne: "test2" } },
@@ -54,7 +54,7 @@ void test("convertOldPrecondition", () => {
for (const t of tests) { for (const t of tests) {
assert.strictEqual( assert.strictEqual(
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)), convertOldPrecondition(t[0] as Record<string, unknown>).toString(),
t[1], t[1],
); );
} }
@@ -86,7 +86,6 @@ void test("toMongoQuery", async () => {
"Param1 <> 1657844103524", "Param1 <> 1657844103524",
{ {
"Param1._value": { $ne: 1657844103524 }, "Param1._value": { $ne: 1657844103524 },
$and: [ $and: [
{ {
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } }, "Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
@@ -114,7 +113,6 @@ void test("toMongoQuery", async () => {
}, },
}, },
], ],
[ [
"LOWER(Param1) LIKE 'value'", "LOWER(Param1) LIKE 'value'",
{ {
@@ -146,7 +144,7 @@ void test("toMongoQuery", async () => {
]; ];
for (const [expStr, expect] of queries) { for (const [expStr, expect] of queries) {
const exp = parse(expStr); const exp = Expression.parse(expStr);
let query = toMongoQuery(exp, "devices"); let query = toMongoQuery(exp, "devices");
if (query) query = EJSON.serialize(query); if (query) query = EJSON.serialize(query);
assert.deepStrictEqual(query, expect); assert.deepStrictEqual(query, expect);
@@ -156,11 +154,10 @@ void test("toMongoQuery", async () => {
["Param1 = Param2", "Right-hand operand must be a literal value"], ["Param1 = Param2", "Right-hand operand must be a literal value"],
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"], ["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
["NOW() = 1", "Left-hand operand must be a parameter"], ["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) { for (const [expStr, err] of failQueries) {
const exp = parse(expStr); const exp = Expression.parse(expStr);
assert.throws(() => toMongoQuery(exp, "devices"), { assert.throws(() => toMongoQuery(exp, "devices"), {
message: err, message: err,
}); });
+22 -19
View File
@@ -7,7 +7,7 @@ import VersionedMap from "../lib/versioned-map.ts";
import { Attributes } from "../lib/types.ts"; import { Attributes } from "../lib/types.ts";
void test("getAliasDeclarations", () => { void test("getAliasDeclarations", () => {
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b"); const path = Path.parse("a.[aa = 10 AND bb.[aaa = 100].cc = 1].b");
const decs = device.getAliasDeclarations(path, 99); const decs = device.getAliasDeclarations(path, 99);
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"]; const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
@@ -32,66 +32,69 @@ void test("unpack", () => {
changes: new Set<string>(), changes: new Set<string>(),
}; };
device.set(deviceData, Path.parse("a.1.b"), now, { device.set(deviceData, "a.1.b", now, {
value: [now, ["b", "xsd:string"]], value: [now, ["b", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.c"), now, { device.set(deviceData, "a.1.c", now, {
value: [now, ["c", "xsd:string"]], value: [now, ["c", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.a.1.a"), now, { device.set(deviceData, "a.1.a.1.a", now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.a.1.b"), now, { device.set(deviceData, "a.1.a.1.b", now, {
value: [now, ["b1", "xsd:string"]], value: [now, ["b1", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.a.1.c"), now, { device.set(deviceData, "a.1.a.1.c", now, {
value: [now, ["c1", "xsd:string"]], value: [now, ["c1", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.a.2.a"), now, { device.set(deviceData, "a.1.a.2.a", now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.a.2.b"), now, { device.set(deviceData, "a.1.a.2.b", now, {
value: [now, ["b2", "xsd:string"]], value: [now, ["b2", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.1.a.2.c"), now, { device.set(deviceData, "a.1.a.2.c", now, {
value: [now, ["c2", "xsd:string"]], value: [now, ["c2", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.b"), now, { device.set(deviceData, "a.2.b", now, {
value: [now, ["b", "xsd:string"]], value: [now, ["b", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.c"), now, { device.set(deviceData, "a.2.c", now, {
value: [now, ["c", "xsd:string"]], value: [now, ["c", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.a.1.a"), now, { device.set(deviceData, "a.2.a.1.a", now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.a.1.b"), now, { device.set(deviceData, "a.2.a.1.b", now, {
value: [now, ["b1", "xsd:string"]], value: [now, ["b1", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.a.1.c"), now, { device.set(deviceData, "a.2.a.1.c", now, {
value: [now, ["c1", "xsd:string"]], value: [now, ["c1", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.a.2.a"), now, { device.set(deviceData, "a.2.a.2.a", now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.a.2.b"), now, { device.set(deviceData, "a.2.a.2.b", now, {
value: [now, ["c1", "xsd:string"]], value: [now, ["c1", "xsd:string"]],
}); });
device.set(deviceData, Path.parse("a.2.a.2.c"), now, { device.set(deviceData, "a.2.a.2.c", now, {
value: [now, ["b1", "xsd:string"]], value: [now, ["b1", "xsd:string"]],
}); });
let unpacked: Path[]; let unpacked: Path[];
unpacked = device.unpack( unpacked = device.unpack(
deviceData, deviceData,
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a"), Path.parse("a.[b='b' AND c='c'].a.[b='b1' AND c='c1'].a"),
); );
assert.strictEqual(unpacked.length, 2); assert.strictEqual(unpacked.length, 2);
assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a"); assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a");
assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a"); assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a");
unpacked = device.unpack(deviceData, Path.parse("a.*.a.[b:c1,c:b1].a")); unpacked = device.unpack(
deviceData,
Path.parse("a.*.a.[b='c1' AND c='b1'].a"),
);
assert.strictEqual(unpacked.length, 1); assert.strictEqual(unpacked.length, 1);
assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a"); assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a");
}); });
+10 -10
View File
@@ -1,13 +1,12 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import initSqlJs from "sql.js/dist/sql-asm.js"; import initSqlJs from "sql.js/dist/sql-asm.js";
import { parse, stringify } from "../lib/common/expression/parser.ts";
import { import {
bookmarkToExpression, bookmarkToExpression,
paginate, paginate,
toBookmark, toBookmark,
} from "../lib/common/expression/pagination.ts"; } from "../lib/common/expression/pagination.ts";
import { Expression } from "../lib/types.ts"; import Expression from "../lib/common/expression.ts";
import { covers, minimize } from "../lib/common/expression/synth.ts"; import { covers, minimize } from "../lib/common/expression/synth.ts";
const VALUES = [null, -1, false, "a"]; const VALUES = [null, -1, false, "a"];
@@ -79,15 +78,15 @@ async function testPaginate(
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`) .map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
.join(", "); .join(", ");
const allMatches = await query(`${stringify(q2)} ORDER BY ${orderBy}`); const allMatches = await query(`${q2.toString()} ORDER BY ${orderBy}`);
const [fulfilled, diff] = paginate(q1, q2, sort); const [fulfilled, diff] = paginate(q1, q2, sort);
assert.ok(covers(q1, fulfilled)); assert.ok(covers(q1, fulfilled));
const fulfilledMatches = await query( const fulfilledMatches = await query(
`${stringify(fulfilled)} ORDER BY ${orderBy}`, `${fulfilled.toString()} ORDER BY ${orderBy}`,
); );
const diffMatches = await query(`${stringify(diff)} ORDER BY ${orderBy}`); const diffMatches = await query(`${diff.toString()} ORDER BY ${orderBy}`);
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]); assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
if (allMatches.length === fulfilledMatches.length) return; if (allMatches.length === fulfilledMatches.length) return;
@@ -98,14 +97,14 @@ async function testPaginate(
); );
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]); const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
const bookmarkFilter = bookmarkToExpression(bookmark, sort); const bookmarkFilter = bookmarkToExpression(bookmark, sort);
const capped = ["AND", q2, bookmarkFilter]; const capped = Expression.and(q2, bookmarkFilter);
assert.ok(!covers(q1, capped)); assert.ok(!covers(q1, capped));
const cappedMatches = await query( const cappedMatches = await query(
`(${stringify(capped)}) ORDER BY ${orderBy}`, `(${capped.toString()}) ORDER BY ${orderBy}`,
); );
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]); assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
const min = minimize(["OR", q1, capped]); const min = minimize(Expression.or(q1, capped));
await testPaginate(min, q2, sort); await testPaginate(min, q2, sort);
} }
@@ -115,6 +114,7 @@ void test("paginate", async () => {
const sortOrders = getAllSortOrders(params); const sortOrders = getAllSortOrders(params);
for (const [q1, q2] of cases) for (const [q1, q2] of cases)
for (const sort of sortOrders) for (const sort of sortOrders) {
await testPaginate(parse(q1), parse(q2), sort); await testPaginate(Expression.parse(q1), Expression.parse(q2), sort);
}
}); });
+24 -28
View File
@@ -3,67 +3,63 @@ import assert from "node:assert";
import Path from "../lib/common/path.ts"; import Path from "../lib/common/path.ts";
import PathSet from "../lib/common/path-set.ts"; import PathSet from "../lib/common/path-set.ts";
void test("depth", () => {
const pathSet = new PathSet();
assert.strictEqual(pathSet.depth, 0);
pathSet.add(Path.parse(""));
assert.strictEqual(pathSet.depth, 1);
pathSet.add(Path.parse("a"));
assert.strictEqual(pathSet.depth, 2);
});
void test("add", () => { void test("add", () => {
const pathSet = new PathSet(); const pathSet = new PathSet();
pathSet.add(Path.parse("a")); pathSet.add("a");
pathSet.add(Path.parse("a")); pathSet.add("a");
assert.strictEqual(pathSet.find(Path.parse("a"), true, true, 99).length, 1); assert.strictEqual(
pathSet.findCompat(Path.parse("a"), true, true, 99).length,
1,
);
}); });
void test("get", () => { void test("get", () => {
const pathSet = new PathSet(); const pathSet = new PathSet();
pathSet.add(Path.parse("a.*")); pathSet.add("a.*");
pathSet.add(Path.parse("a.a")); pathSet.add("a.a");
pathSet.add(Path.parse("*.*")); pathSet.add("*.*");
assert.strictEqual(pathSet.get(Path.parse("a.*")).toString(), "a.*"); assert.strictEqual(pathSet.get("a.*").toString(), "a.*");
assert.strictEqual(pathSet.get(Path.parse("*.a")), null); assert.equal(pathSet.get("*.a"), null);
}); });
void test("find", () => { void test("find", () => {
const pathSet = new PathSet(); const pathSet = new PathSet();
pathSet.add(Path.parse("a")); pathSet.add("a");
pathSet.add(Path.parse("a.*")); pathSet.add("a.*");
pathSet.add(Path.parse("a.a")); pathSet.add("a.a");
pathSet.add(Path.parse("*.a")); pathSet.add("*.a");
pathSet.add(Path.parse("*.*")); pathSet.add("*.*");
assert.deepStrictEqual( assert.deepStrictEqual(
pathSet.find(Path.parse(""), true, true, 1).map((p) => p.toString()), pathSet.findCompat(Path.root, true, true, 1).map((p) => p.toString()),
["a"], ["a"],
); );
assert.deepStrictEqual( assert.deepStrictEqual(
pathSet.find(Path.parse(""), false, false, 2).map((p) => p.toString()), pathSet.findCompat(Path.root, false, false, 2).map((p) => p.toString()),
["a", "a.*", "a.a", "*.a", "*.*"], ["a", "a.*", "a.a", "*.a", "*.*"],
); );
assert.deepStrictEqual( assert.deepStrictEqual(
pathSet.find(Path.parse("a.*"), false, false).map((p) => p.toString()), pathSet
.findCompat(Path.parse("a.*"), false, false)
.map((p) => p.toString()),
["a.*"], ["a.*"],
); );
assert.deepStrictEqual( assert.deepStrictEqual(
pathSet.find(Path.parse("a.*"), false, true).map((p) => p.toString()), pathSet.findCompat(Path.parse("a.*"), false, true).map((p) => p.toString()),
["a.*", "a.a"], ["a.*", "a.a"],
); );
assert.deepStrictEqual( assert.deepStrictEqual(
pathSet.find(Path.parse("a.*"), true, false).map((p) => p.toString()), pathSet.findCompat(Path.parse("a.*"), true, false).map((p) => p.toString()),
["a.*", "*.*"], ["a.*", "*.*"],
); );
assert.deepStrictEqual( assert.deepStrictEqual(
pathSet.find(Path.parse("a.*"), true, true).map((p) => p.toString()), pathSet.findCompat(Path.parse("a.*"), true, true).map((p) => p.toString()),
["a.*", "a.a", "*.a", "*.*"], ["a.*", "a.a", "*.a", "*.*"],
); );
}); });
+138 -15
View File
@@ -8,38 +8,161 @@ void test("parse", () => {
assert.throws(() => Path.parse(".a")); assert.throws(() => Path.parse(".a"));
assert.throws(() => Path.parse("a.")); assert.throws(() => Path.parse("a."));
assert.throws(() => Path.parse("a..b")); assert.throws(() => Path.parse("a..b"));
assert.throws(() => Path.parse("b*")); assert.doesNotThrow(() => Path.parse("b*"));
assert.throws(() => Path.parse("*b")); assert.doesNotThrow(() => Path.parse("*b"));
assert.throws(() => Path.parse("a.b c.d")); assert.throws(() => Path.parse("a.b c.d"));
assert.throws(() => Path.parse("a[")); assert.throws(() => Path.parse("a["));
assert.throws(() => Path.parse("a[b")); assert.throws(() => Path.parse("a[b"));
assert.throws(() => Path.parse("a[b:")); assert.throws(() => Path.parse("a[b:"));
assert.throws(() => Path.parse('a[b:"waef]')); assert.throws(() => Path.parse('a[b:"waef]'));
assert.doesNotThrow(() => Path.parse("[]"));
assert.doesNotThrow(() => Path.parse("[a:]"));
assert.doesNotThrow(() => Path.parse("*")); assert.doesNotThrow(() => Path.parse("*"));
assert.doesNotThrow(() => Path.parse("")); assert.throws(() => Path.parse(""));
}); });
void test("toString", () => { void test("toString", () => {
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123'); const path1 = Path.parse('abc.[ abc=123 and def=" abc " ].123');
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123'); const path2 = Path.parse('abc.[abc = 123 AND def = " abc "].123');
assert.strictEqual(path1.toString(), path2.toString()); assert.strictEqual(path1.toString(), path2.toString());
}); });
void test("slice", () => { void test("slice", () => {
const path = Path.parse("a.*.b.[x:y].c"); const path = Path.parse(`a.*.b.[x = "y"].c`);
const sliced = path.slice(1, -1); const sliced = path.slice(1, -1);
assert.strictEqual(sliced.toString(), '*.b.[x:"y"]'); assert.strictEqual(sliced.toString(), '*.b.[x = "y"]');
assert.strictEqual(sliced.alias, 0b100); assert.strictEqual(sliced.alias, 0b100);
assert.strictEqual(sliced.wildcard, 0b1); assert.strictEqual(sliced.wildcard, 0b1);
const path2 = Path.parse("a.b:c.d");
// Trim from right into colon region
assert.strictEqual(path2.slice(0, 3).toString(), "a.b:c");
assert.strictEqual(path2.slice(0, 3).colon, 1);
// Trim from right past colon region
assert.strictEqual(path2.slice(0, 2).toString(), "a.b");
assert.strictEqual(path2.slice(0, 2).colon, 0);
// Start exactly at colon boundary (all-colon result)
assert.strictEqual(path2.slice(2, 4).toString(), ":c.d");
assert.strictEqual(path2.slice(2, 4).colon, 2);
// Start past colon boundary (colon dropped)
assert.strictEqual(path2.slice(3, 4).toString(), "d");
assert.strictEqual(path2.slice(3, 4).colon, 0);
// Span across boundary from both sides
assert.strictEqual(path2.slice(1, 3).toString(), "b:c");
assert.strictEqual(path2.slice(1, 3).colon, 1);
}); });
void test("concat", () => { void test("concat", () => {
const path1 = Path.parse("a"); // Alias and wildcard propagation
const path2 = Path.parse("*.[a:b]"); const c0 = Path.parse("a").concat(Path.parse('*.[a = "b"]'));
const concat = path1.concat(path2); assert.strictEqual(c0.toString(), 'a.*.[a = "b"]');
assert.strictEqual(concat.toString(), 'a.*.[a:"b"]'); assert.strictEqual(c0.alias, 0b100);
assert.strictEqual(concat.alias, 0b100); assert.strictEqual(c0.wildcard, 0b10);
assert.strictEqual(concat.wildcard, 0b10);
// Left plain, right all-colon
const allColon = Path.parse("a:b.c").slice(1, 3);
const c1 = Path.parse("a.b").concat(allColon);
assert.strictEqual(c1.toString(), "a.b:b.c");
assert.strictEqual(c1.colon, 2);
// Left colon, right all-colon
const c2 = Path.parse("a:b").concat(Path.parse("a.b:c.d").slice(2, 4));
assert.strictEqual(c2.toString(), "a:b.c.d");
assert.strictEqual(c2.colon, 3);
// Both have mixed colon — should throw
assert.throws(() => Path.parse("a:b").concat(Path.parse("c:d")));
});
void test("old alias format", () => {
// Empty brackets
const empty = Path.parse("a.[].b");
assert.strictEqual(empty.alias, 0b10);
assert.strictEqual(empty.toString(), "a.[TRUE].b");
// Single key-value
const single = Path.parse("a.[b:c].d");
assert.strictEqual(single.alias, 0b10);
assert.strictEqual(single.toString(), 'a.[b = "c"].d');
// Multiple key-value pairs
const multi = Path.parse("a.[b:1,c:2].d");
assert.strictEqual(multi.alias, 0b10);
assert.strictEqual(multi.toString(), 'a.[b = "1" AND c = "2"].d');
// Key with empty value
const emptyVal = Path.parse("a.[b:].d");
assert.strictEqual(emptyVal.toString(), 'a.[b = ""].d');
// Value containing colons (split on first : only)
const colonVal = Path.parse("a.[b:c:d].e");
assert.strictEqual(colonVal.toString(), 'a.[b = "c:d"].e');
// Value containing spaces (trimmed)
const spaceVal = Path.parse("a.[b:hello world].c");
assert.strictEqual(spaceVal.toString(), 'a.[b = "hello world"].c');
// Unquoted values are trimmed
const trimmed = Path.parse("a.[b: hello ].c");
assert.strictEqual(trimmed.toString(), 'a.[b = "hello"].c');
// Whitespace around keys is trimmed
const keySpace = Path.parse("a.[ b : c ].d");
assert.strictEqual(keySpace.toString(), 'a.[b = "c"].d');
// Equivalence with new format
const oldFmt = Path.parse("x.[a:1,b:2].y");
const newFmt = Path.parse('x.[a = "1" AND b = "2"].y');
assert.strictEqual(oldFmt.toString(), newFmt.toString());
// Nested old-format alias
const nested = Path.parse("a.[b.[x:1].c:2].d");
assert.strictEqual(nested.toString(), 'a.[b.[x = "1"].c = "2"].d');
// New SQL format still works
const sql = Path.parse("a.[b = 1 AND c = 2].d");
assert.strictEqual(sql.toString(), "a.[b = 1 AND c = 2].d");
// Double-quoted value containing closing bracket
const quotedBracket = Path.parse('a.[b:"hello]world"].c');
assert.strictEqual(quotedBracket.toString(), 'a.[b = "hello]world"].c');
// Double-quoted value containing comma
const quotedComma = Path.parse('a.[b:"hello,world"].c');
assert.strictEqual(quotedComma.toString(), 'a.[b = "hello,world"].c');
// Double-quoted value with escape sequences (JSON semantics)
const escaped = Path.parse('a.[b:"hello\\"world"].c');
assert.strictEqual(escaped.toString(), 'a.[b = "hello\\"world"].c');
// Single-quoted value
const singleQuoted = Path.parse("a.[b:'value'].c");
assert.strictEqual(singleQuoted.toString(), 'a.[b = "value"].c');
// Invalid old format (no colon) still throws
assert.throws(() => Path.parse("[abc]"));
});
void test("slice concat round-trip", () => {
const path = Path.parse("a.b:c.d");
// Round-trips at every split position up to and including the boundary
for (let i = 1; i <= path.paramLength; i++) {
const rejoined = path.slice(0, i).concat(path.slice(i));
assert.strictEqual(rejoined.toString(), path.toString());
assert.strictEqual(rejoined.colon, path.colon);
}
// Split inside the colon region: right half loses colon,
// but concat still extends the left's attr region
const left = path.slice(0, 3);
const right = path.slice(3);
assert.strictEqual(left.colon, 1);
assert.strictEqual(right.colon, 0);
const rejoined = left.concat(right);
assert.strictEqual(rejoined.toString(), "a.b:c.d");
assert.strictEqual(rejoined.colon, 2);
}); });
+34 -14
View File
@@ -2,7 +2,7 @@ import test from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import initSqlJs from "sql.js/dist/sql-asm.js"; import initSqlJs from "sql.js/dist/sql-asm.js";
import { covers, minimize, unionDiff } from "../lib/common/expression/synth.ts"; import { covers, minimize, unionDiff } from "../lib/common/expression/synth.ts";
import { parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"]; const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
const DECIMAL_VALUES = [null, 0, -10, 10]; const DECIMAL_VALUES = [null, 0, -10, 10];
@@ -86,7 +86,7 @@ void test("minimize", async () => {
for (const c of cases) { for (const c of cases) {
const res1 = await query(c); const res1 = await query(c);
const min = stringify(minimize(parse(c), true)); const min = minimize(Expression.parse(c), true).toString();
const res2 = await query(min); const res2 = await query(min);
assert.strictEqual(setsEqual(res1, res2), true); assert.strictEqual(setsEqual(res1, res2), true);
} }
@@ -104,9 +104,9 @@ void test("unionDiff", async () => {
for (const [c1, c2] of getPermutations(cases, cases)) { for (const [c1, c2] of getPermutations(cases, cases)) {
const res1 = await query(c1); const res1 = await query(c1);
const res2 = await query(c2); const res2 = await query(c2);
const [union, diff] = unionDiff(parse(c1), parse(c2)); const [union, diff] = unionDiff(Expression.parse(c1), Expression.parse(c2));
const res3 = await query(stringify(union)); const res3 = await query(union.toString());
const res4 = await query(stringify(diff)); const res4 = await query(diff.toString());
const unionSet = new Set([...res1, ...res2]); const unionSet = new Set([...res1, ...res2]);
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r))); const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
@@ -117,15 +117,35 @@ void test("unionDiff", async () => {
}); });
void test("covers", async () => { void test("covers", async () => {
assert.strictEqual(covers(false, false), true);
assert.strictEqual(covers(false, parse("decimal > 5 AND decimal < 3")), true);
assert.strictEqual(covers(parse("true"), parse("decimal > 0")), true);
assert.strictEqual(covers(parse("true"), parse("false")), true);
assert.strictEqual(covers(parse("false"), parse("false")), true);
assert.strictEqual(covers(parse("false"), parse("decimal > 0")), false);
assert.strictEqual(covers(parse("decimal >= 0"), parse("decimal > 0")), true);
assert.strictEqual( assert.strictEqual(
covers(parse("decimal > 0"), parse("decimal >= 0")), covers(Expression.parse("false"), Expression.parse("false")),
true,
);
assert.strictEqual(
covers(
Expression.parse("false"),
Expression.parse("decimal > 5 AND decimal < 3"),
),
true,
);
assert.strictEqual(
covers(Expression.parse("true"), Expression.parse("decimal > 0")),
true,
);
assert.strictEqual(
covers(Expression.parse("true"), Expression.parse("false")),
true,
);
assert.strictEqual(
covers(Expression.parse("false"), Expression.parse("decimal > 0")),
false,
);
assert.strictEqual(
covers(Expression.parse("decimal >= 0"), Expression.parse("decimal > 0")),
true,
);
assert.strictEqual(
covers(Expression.parse("decimal > 0"), Expression.parse("decimal >= 0")),
false, false,
); );
@@ -139,7 +159,7 @@ void test("covers", async () => {
for (const [c1, c2] of cases) { for (const [c1, c2] of cases) {
const res1 = await query(c1); const res1 = await query(c1);
const res2 = await query(c2); const res2 = await query(c2);
const coversResult = covers(parse(c1), parse(c2)); const coversResult = covers(Expression.parse(c1), Expression.parse(c2));
const actuallyCovers = Array.from(res2).every((r) => res1.has(r)); const actuallyCovers = Array.from(res2).every((r) => res1.has(r));
assert.strictEqual( assert.strictEqual(
+21 -3
View File
@@ -20,19 +20,37 @@ import { contextifyComponent } from "./components.ts";
import { PermissionSet, UiConfig } from "../lib/types.ts"; import { PermissionSet, UiConfig } from "../lib/types.ts";
import drawerComponent from "./drawer-component.ts"; import drawerComponent from "./drawer-component.ts";
import { render as renderOverlay } from "./overlay.ts"; import { render as renderOverlay } from "./overlay.ts";
import Expression from "../lib/common/expression.ts";
declare global { declare global {
interface Window { interface Window {
authorizer: Authorizer; authorizer: Authorizer;
permissionSets: PermissionSet[]; permissionSets: {
[resource: string]: { access: number; validate: string; filter: string };
}[][];
username: string; username: string;
clientConfig: { ui: UiConfig }; clientConfig: UiConfig;
configSnapshot: string; configSnapshot: string;
genieacsVersion: string; genieacsVersion: string;
} }
} }
window.authorizer = new Authorizer(window.permissionSets); const permissionSets: PermissionSet[] = window.permissionSets.map((p) =>
p.map((s) =>
Object.fromEntries(
Object.entries(s).map(([resource, { access, validate, filter }]) => [
resource,
{
access,
validate: Expression.parse(validate),
filter: Expression.parse(filter),
},
]),
),
),
);
window.authorizer = new Authorizer(permissionSets);
let state; let state;
+57 -35
View File
@@ -1,26 +1,41 @@
import { ClosureComponent } from "mithril"; import { ClosureComponent } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts"; import * as taskQueue from "../task-queue.ts";
import { parse } from "../../lib/common/expression/parser.ts";
import memoize from "../../lib/common/memoize.ts"; import memoize from "../../lib/common/memoize.ts";
import { icon } from "../tailwind-utility-components.ts"; import { icon } from "../tailwind-utility-components.ts";
import { QueryResponse, evaluateExpression } from "../store.ts"; import { QueryResponse, evaluateExpression } from "../store.ts";
import debounce from "../../lib/common/debounce.ts"; import debounce from "../../lib/common/debounce.ts";
import { Expression } from "../../lib/types.ts"; import Expression, { Value } from "../../lib/common/expression.ts";
import { FlatDevice } from "../../lib/ui/db.ts"; import { FlatDevice } from "../../lib/ui/db.ts";
import Path from "../../lib/common/path.ts";
const memoizedParse = memoize(parse);
function escapeRegExp(str): string { function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
} }
const keysByDepth: WeakMap<Record<string, unknown>, string[][]> = new WeakMap(); interface Parameter {
path: Expression.Parameter;
value?: Value;
writable?: boolean;
object?: boolean;
}
function orderKeysByDepth(device: Record<string, unknown>): string[][] { const prepareParams = memoize((device: FlatDevice): Parameter[][] => {
if (keysByDepth.has(device)) return keysByDepth.get(device); const map = new Map<string, Parameter>();
const res: string[][] = [];
for (const key of Object.keys(device)) { for (const [k, v] of Object.entries(device)) {
const [param, attr] = k.split(":");
let attrs = map.get(param);
if (!attrs)
map.set(
param,
(attrs = { path: new Expression.Parameter(Path.parse(param)) }),
);
attrs[attr || "value"] = v;
}
const res: Parameter[][] = [];
for (const [key, attrs] of map) {
let count = 0; let count = 0;
for ( for (
let i = key.lastIndexOf(".", key.length - 2); let i = key.lastIndexOf(".", key.length - 2);
@@ -29,11 +44,10 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
) )
++count; ++count;
while (res.length <= count) res.push([]); while (res.length <= count) res.push([]);
res[count].push(key); res[count].push(attrs);
} }
keysByDepth.set(device, res);
return res; return res;
} });
interface Attrs { interface Attrs {
device: FlatDevice; device: FlatDevice;
@@ -51,9 +65,13 @@ const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs.device; const device = vnode.attrs.device;
const allParams = prepareParams(device);
const limit = let limit = 100;
(evaluateExpression(vnode.attrs.limit, device) as number) || 100; if (vnode.attrs.limit) {
const l = evaluateExpression(vnode.attrs.limit, device);
if (typeof l.value === "number") limit = l.value;
}
const search = m( const search = m(
"input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2", "input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2",
@@ -75,35 +93,36 @@ const component: ClosureComponent<Attrs> = () => {
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i"); re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
} }
const filteredKeys: string[] = []; const filteredParams: Parameter[] = [];
const allKeys = orderKeysByDepth(device);
let count = 0; let count = 0;
for (const keys of allKeys) { for (const keys of allParams) {
let c = 0; let c = 0;
for (const k of keys) { for (const k of keys) {
const p = device[k]; const str = k.value ? `${k.path.toString()} ${k.value}` : k;
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
if (re && !re.test(str)) continue; if (re && !re.test(str)) continue;
++c; ++c;
if (count < limit) filteredKeys.push(k); if (count < limit) filteredParams.push(k);
} }
count += c; count += c;
} }
filteredKeys.sort(); filteredParams.sort((a, b) => {
if (a.path < b.path) return -1;
if (a.path > b.path) return 1;
return 0;
});
const rows = filteredKeys.map((k) => { const rows = filteredParams.map((p) => {
const p = device[k];
const val = []; const val = [];
if (p.object === false) { if (p.value) {
val.push( val.push(
m( m(
"parameter", "parameter",
Object.assign({ device: device, parameter: memoizedParse(k) }), Object.assign({ device: device, parameter: p.path }),
), ),
); );
} else if (p.object && p.writable) { } else if (p.object && p.writable) {
if (instanceRegex.test(k)) { if (instanceRegex.test(p.path.toString())) {
val.push( val.push(
m( m(
"button", "button",
@@ -112,8 +131,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "deleteObject", name: "deleteObject",
device: device["DeviceID.ID"].value[0] as string, device: device["DeviceID.ID"] as string,
objectName: k, objectName: p.path.toString(),
}); });
}, },
}, },
@@ -133,8 +152,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "addObject", name: "addObject",
device: device["DeviceID.ID"].value[0] as string, device: device["DeviceID.ID"] as string,
objectName: k, objectName: p.path.toString(),
}); });
}, },
}, },
@@ -156,8 +175,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "getParameterValues", name: "getParameterValues",
device: device["DeviceID.ID"].value[0] as string, device: device["DeviceID.ID"] as string,
parameterNames: [k], parameterNames: [p.path.toString()],
}); });
}, },
}, },
@@ -170,7 +189,10 @@ const component: ClosureComponent<Attrs> = () => {
return m( return m(
"tr", "tr",
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: k })), m(
"td.pl-4 pr-2 py-2 truncate",
m("long-text", { text: p.path.toString() }),
),
m("td.pr-4 py-2 text-right flex justify-end", val), m("td.pr-4 py-2 text-right flex justify-end", val),
); );
}); });
@@ -195,7 +217,7 @@ const component: ClosureComponent<Attrs> = () => {
m( m(
"span.text-xs", "span.text-xs",
"Displaying ", "Displaying ",
m("span.font-medium", "" + filteredKeys.length), m("span.font-medium", "" + filteredParams.length),
" out of ", " out of ",
m("span.font-medium", "" + count), m("span.font-medium", "" + count),
" parameters", " parameters",
@@ -204,7 +226,7 @@ const component: ClosureComponent<Attrs> = () => {
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium", "a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
{ {
href: `api/devices/${encodeURIComponent( href: `api/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0], device["DeviceID.ID"],
)}.csv`, )}.csv`,
download: "", download: "",
}, },
+16 -15
View File
@@ -1,7 +1,6 @@
import { Attributes, ClosureComponent } from "mithril"; import { Attributes, ClosureComponent } from "mithril";
import { map } from "../../lib/common/expression/parser.ts";
import memoize from "../../lib/common/memoize.ts"; import memoize from "../../lib/common/memoize.ts";
import { Expression } from "../../lib/types.ts"; import Expression from "../../lib/common/expression.ts";
import { m } from "../components.ts"; import { m } from "../components.ts";
import { evaluateExpression, getTimestamp } from "../store.ts"; import { evaluateExpression, getTimestamp } from "../store.ts";
import { FlatDevice } from "../../lib/ui/db.ts"; import { FlatDevice } from "../../lib/ui/db.ts";
@@ -14,15 +13,16 @@ const evaluateAttributes = memoize(
): Attributes => { ): Attributes => {
const res: Attributes = {}; const res: Attributes = {};
for (const [k, v] of Object.entries(attrs)) { for (const [k, v] of Object.entries(attrs)) {
const vv = map(v, (e) => { const vv = v.evaluate((e) => {
if ( if (e instanceof Expression.Literal) return e;
Array.isArray(e) && else if (e instanceof Expression.FunctionCall) {
e[0] === "FUNC" && if (e.name === "ENCODEURICOMPONENT") {
e[1] === "ENCODEURICOMPONENT" const a = evaluateExpression(e.args[0], obj);
) { if (a instanceof Expression.Literal) {
const a = evaluateExpression(e[2], obj); if (a.value == null) return new Expression.Literal(null);
if (a == null) return null; return new Expression.Literal(encodeURIComponent(a.value));
return encodeURIComponent(a as string); }
}
} }
return e; return e;
}); });
@@ -47,9 +47,10 @@ const component: ClosureComponent<Attrs> = () => {
} }
const children = Object.values(vnode.attrs.components).map((c) => { const children = Object.values(vnode.attrs.components).map((c) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {}); if (c instanceof Expression)
c = evaluateExpression(c, device || {}).value;
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {}); const type = evaluateExpression(c["type"], device || {}).value;
if (!type) return null; if (!type) return null;
return m(type as string, c); return m(type as string, c);
}); });
@@ -59,8 +60,8 @@ const component: ClosureComponent<Attrs> = () => {
if (el == null) return children; if (el == null) return children;
let attrs: Attributes; let attrs: Attributes;
if (Array.isArray(el)) { if (el instanceof Expression) {
el = evaluateExpression(el, device || {}); el = evaluateExpression(el, device || {}).value;
} else if (typeof el === "object") { } else if (typeof el === "object") {
if (el["attributes"] != null) { if (el["attributes"] != null) {
attrs = evaluateAttributes( attrs = evaluateAttributes(
+4 -4
View File
@@ -19,7 +19,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "reboot", name: "reboot",
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"],
}); });
}, },
}, },
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "factoryReset", name: "factoryReset",
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"],
}); });
}, },
}, },
@@ -51,7 +51,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.stageDownload({ taskQueue.stageDownload({
name: "download", name: "download",
devices: [device["DeviceID.ID"].value[0]], devices: [device["DeviceID.ID"]],
}); });
}, },
}, },
@@ -66,7 +66,7 @@ const component: ClosureComponent = (): Component => {
title: "Delete device", title: "Delete device",
onclick: () => { onclick: () => {
if (!confirm("Deleting this device. Are you sure?")) return; if (!confirm("Deleting this device. Are you sure?")) return;
const deviceId = device["DeviceID.ID"].value[0]; const deviceId = device["DeviceID.ID"];
store store
.deleteResource("devices", deviceId) .deleteResource("devices", deviceId)
+13 -6
View File
@@ -3,17 +3,24 @@ import { m } from "../components.ts";
import * as store from "../store.ts"; import * as store from "../store.ts";
import * as notifications from "../notifications.ts"; import * as notifications from "../notifications.ts";
import { stringify } from "../../lib/common/yaml.ts"; import { stringify } from "../../lib/common/yaml.ts";
import Expression from "../../lib/common/expression.ts";
import Path from "../../lib/common/path.ts";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const deviceId = device["DeviceID.ID"].value[0]; const deviceId = device["DeviceID.ID"];
const faults = store.fetch("faults", [ const p = new Expression.Parameter(Path.parse("_id"));
"AND", const exp = Expression.and(
[">", ["PARAM", "_id"], `${deviceId}:`], new Expression.Binary(">", p, new Expression.Literal(`${deviceId}:`)),
["<", ["PARAM", "_id"], `${deviceId}:zzzz`], new Expression.Binary(
]); "<",
p,
new Expression.Literal(`${deviceId}:zzzz`),
),
);
const faults = store.fetch("faults", exp);
const headers = [ const headers = [
"Channel", "Channel",
+5 -3
View File
@@ -1,18 +1,20 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import { evaluateExpression } from "../store.ts"; import { evaluateExpression } from "../store.ts";
import Expression from "../../lib/common/expression.ts";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
let deviceId; let deviceId;
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
if (device) deviceId = device["DeviceID.ID"].value[0]; if (device) deviceId = device["DeviceID.ID"];
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map((c) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {}); if (c instanceof Expression)
c = evaluateExpression(c, device ?? {}).value;
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {}); const type = evaluateExpression(c["type"], device ?? {}).value;
if (!type) return null; if (!type) return null;
const attrs = Object.assign({}, vnode.attrs, c); const attrs = Object.assign({}, vnode.attrs, c);
return m(type as string, attrs); return m(type as string, attrs);
+12 -9
View File
@@ -1,20 +1,23 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import config from "../config.ts"; import { overview } from "../config.ts";
import { evaluateExpression } from "../store.ts"; import { evaluateExpression } from "../store.ts";
const CHARTS = config.ui.overview.charts; const CHARTS = overview.charts;
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const chartName = evaluateExpression(vnode.attrs["chart"], device || {}); const chartName = evaluateExpression(
const chart = CHARTS[chartName as string] as Record<string, unknown>; vnode.attrs["chart"],
device ?? {},
).value;
const chart = CHARTS[chartName as string];
if (!chart) return null; if (!chart) return null;
for (const slice of Object.values(chart.slices)) { for (const slice of chart.slices) {
const filter = slice["filter"]; const filter = slice.filter;
if (evaluateExpression(filter, device || {})) { if (evaluateExpression(filter, device ?? {}).value) {
const dot = m( const dot = m(
"svg.inline", "svg.inline",
{ {
@@ -28,10 +31,10 @@ const component: ClosureComponent = (): Component => {
cx: "0.5em", cx: "0.5em",
cy: "0.5em", cy: "0.5em",
r: "0.4em", r: "0.4em",
fill: evaluateExpression(slice["color"], null), fill: slice.color,
}), }),
); );
return m("span", dot, evaluateExpression(slice["label"], null)); return m("span", dot, slice.label);
} }
} }
return null; return null;
+7 -3
View File
@@ -2,7 +2,7 @@ import { ClosureComponent, VnodeDOM } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import { QueryResponse, evaluateExpression } from "../store.ts"; import { QueryResponse, evaluateExpression } from "../store.ts";
import { FlatDevice } from "../../lib/ui/db.ts"; import { FlatDevice } from "../../lib/ui/db.ts";
import { Expression } from "../../lib/types.ts"; import Expression from "../../lib/common/expression.ts";
interface Attrs { interface Attrs {
device: FlatDevice; device: FlatDevice;
@@ -19,7 +19,11 @@ const component: ClosureComponent<Attrs> = () => {
const device = vnode.attrs.device; const device = vnode.attrs.device;
const rows = Object.values(vnode.attrs.parameters).map((parameter) => { const rows = Object.values(vnode.attrs.parameters).map((parameter) => {
const type = evaluateExpression(parameter.type, device); let type = "parameter";
if (parameter.type) {
const t = evaluateExpression(parameter.type, device);
if (typeof t.value === "string") type = t.value;
}
const p = m.context( const p = m.context(
{ {
device: device, device: device,
@@ -45,7 +49,7 @@ const component: ClosureComponent<Attrs> = () => {
}, },
m( m(
"dt.text-sm font-medium text-stone-500", "dt.text-sm font-medium text-stone-500",
evaluateExpression(parameter["label"], device), evaluateExpression(parameter["label"], device).value,
), ),
m("dd.text-sm text-stone-900 col-span-2", p), m("dd.text-sm text-stone-900 col-span-2", p),
); );
+40 -33
View File
@@ -2,47 +2,45 @@ import { ClosureComponent } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts"; import * as taskQueue from "../task-queue.ts";
import { QueryResponse, evaluateExpression } from "../store.ts"; import { QueryResponse, evaluateExpression } from "../store.ts";
import * as expressionParser from "../../lib/common/expression/parser.ts";
import { icon } from "../tailwind-utility-components.ts"; import { icon } from "../tailwind-utility-components.ts";
import { FlatDevice } from "../../lib/ui/db.ts"; import { FlatDevice } from "../../lib/ui/db.ts";
import { Expression } from "../../lib/types.ts"; import Expression from "../../lib/common/expression.ts";
import Path from "../../lib/common/path.ts";
interface Attrs { interface Attrs {
device: FlatDevice; device: FlatDevice;
parameter: Expression; parameter: Expression;
label: Expression; label: Expression;
childParameters: Record<string, { label: string; parameter: Expression }>; childParameters: Record<string, { label: Expression; parameter: Expression }>;
filter?: Expression; filter?: Expression;
deviceQuery: QueryResponse; deviceQuery: QueryResponse;
} }
const component: ClosureComponent<Attrs> = () => { const component: ClosureComponent<Attrs> = () => {
let object: Path;
let parameters: { label: Expression; parameter: Expression }[];
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const obj = vnode.attrs.parameter; const obj = vnode.attrs.parameter;
if (!Array.isArray(obj) || obj[0] !== "PARAM") if (!(obj instanceof Expression.Parameter))
throw new Error("Object must be a parameter path"); throw new Error("Object must be a parameter path");
vnode.state["object"] = obj[1]; object = obj.path;
vnode.state["parameters"] = Object.values(vnode.attrs.childParameters); parameters = Object.values(vnode.attrs.childParameters);
}, },
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs.device; const device = vnode.attrs.device;
const object = evaluateExpression(vnode.state["object"], device);
const parameters = vnode.state["parameters"];
if (typeof object !== "string" || !device[object]) return null;
const instances: Set<string> = new Set(); const instances: Set<string> = new Set();
const prefix = `${object}.`; const prefix = `${object.toString()}.`;
for (const p in device) { for (const p in device) {
if (p.startsWith(prefix)) { if (!p.startsWith(prefix)) continue;
const i = p.indexOf(".", prefix.length); if (p.lastIndexOf(":") !== -1) continue;
if (i === -1) instances.add(p); const i = p.indexOf(".", prefix.length);
else instances.add(p.slice(0, i)); if (i === -1) instances.add(p);
} else instances.add(p.slice(0, i));
} }
const headers = Object.values(parameters).map((p, i) => { const headers = parameters.map((p, i) => {
const padding = i ? "px-3" : "pl-6 pr-3"; const padding = i ? "px-3" : "pl-6 pr-3";
return m( return m(
@@ -53,7 +51,7 @@ const component: ClosureComponent<Attrs> = () => {
"py-3.5 text-left text-sm font-semibold text-stone-500 " + "py-3.5 text-left text-sm font-semibold text-stone-500 " +
padding, padding,
}, },
evaluateExpression(p["label"], device), evaluateExpression(p.label, device).value,
); );
}); });
@@ -63,24 +61,33 @@ const component: ClosureComponent<Attrs> = () => {
const rows = []; const rows = [];
for (const i of instances) { for (const i of instances) {
let filter = "filter" in vnode.attrs ? vnode.attrs.filter : true; let filter: Expression =
"filter" in vnode.attrs
? vnode.attrs.filter
: new Expression.Literal(true);
filter = expressionParser.map(filter, (e) => { const root = Path.parse(i);
if (Array.isArray(e) && e[0] === "PARAM") filter = filter.evaluate((e) => {
return ["PARAM", ["||", i, ".", e[1]]]; if (e instanceof Expression.Parameter)
return new Expression.Parameter(root.concat(e.path));
return e; return e;
}); });
if (!evaluateExpression(filter, device)) continue; if (!evaluateExpression(filter, device).value) continue;
const row = parameters.map((p, j) => { const row = parameters.map((p, j) => {
const padding = j ? "px-3" : "pl-6 pr-3"; const padding = j ? "px-3" : "pl-6 pr-3";
const param = expressionParser.map(p.parameter, (e) => { const param = p.parameter.evaluate((e) => {
if (Array.isArray(e) && e[0] === "PARAM") if (e instanceof Expression.Parameter)
return ["PARAM", ["||", i, ".", e[1]]]; return new Expression.Parameter(root.concat(e.path));
return e; return e;
}); });
let type = "parameter";
if (p["type"] instanceof Expression)
type = evaluateExpression(p["type"], device).value + "";
return m( return m(
"td", "td",
{ {
@@ -91,7 +98,7 @@ const component: ClosureComponent<Attrs> = () => {
device: device, device: device,
parameter: param, parameter: param,
}, },
p.type || "parameter", type,
Object.assign({}, p, { Object.assign({}, p, {
device: device, device: device,
parameter: param, parameter: param,
@@ -101,7 +108,7 @@ const component: ClosureComponent<Attrs> = () => {
); );
}); });
if (device[i].writable === true) { if (device[i + ":writable"]) {
row.push( row.push(
m( m(
"td", "td",
@@ -116,7 +123,7 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "deleteObject", name: "deleteObject",
device: device["DeviceID.ID"].value[0] as string, device: device["DeviceID.ID"] as string,
objectName: i, objectName: i,
}); });
}, },
@@ -147,7 +154,7 @@ const component: ClosureComponent<Attrs> = () => {
); );
} }
if (device[object].writable === true) { if (device[object.toString() + ":writable"]) {
rows.push( rows.push(
m( m(
"tr", "tr",
@@ -161,8 +168,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "addObject", name: "addObject",
device: device["DeviceID.ID"].value[0] as string, device: device["DeviceID.ID"] as string,
objectName: object, objectName: object.toString(),
}); });
}, },
}, },
+42 -48
View File
@@ -2,54 +2,52 @@ import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts"; import * as taskQueue from "../task-queue.ts";
import * as store from "../store.ts"; import * as store from "../store.ts";
import { evaluate } from "../../lib/common/expression/util.ts"; import Expression, { Value } from "../../lib/common/expression.ts";
import memoize from "../../lib/common/memoize.ts"; import memoize from "../../lib/common/memoize.ts";
import timeAgo from "../timeago.ts"; import timeAgo from "../timeago.ts";
import { icon } from "../tailwind-utility-components.ts"; import { icon } from "../tailwind-utility-components.ts";
const evaluateParam = memoize((exp, obj, now: number) => { const evaluateParam = memoize(
let timestamp = now; (
exp = evaluate(exp, null, now, (e) => { exp: Expression,
if (!Array.isArray(e)) return e; obj: any,
for (let i = 1; i < e.length; ++i) { now: number,
if ( ): { value: Value; timestamp: number; parameter: string } => {
Array.isArray(e[i]) && let timestamp = now;
e[i][0] === "PARAM" && const valueMap: Map<Expression.Literal, string> = new Map();
!Array.isArray(e[i][1]) const lit = exp.evaluate((e): Expression.Literal => {
) { if (e instanceof Expression.Literal) return e;
let v = null; if (e instanceof Expression.Parameter) {
const p = obj[e[i][1]]; let v = obj[e.path.toString()];
if (p?.value) { if (v) {
v = p.value[0]; timestamp = Math.min(
timestamp = Math.min(timestamp, p.valueTimestamp); timestamp,
obj[e.path.toString() + ":valueTimestamp"] ?? 0,
);
const t = obj[e.path.toString() + ":type"];
if (t === "xsd:dateTime" && typeof v === "number")
v = new Date(v).toLocaleString();
const val = new Expression.Literal(v);
valueMap.set(val, e.path.toString());
return val;
}
} else if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(now);
else if (e.name === "DATE_STRING") {
const v = e.args[0];
if (v instanceof Expression.Literal) {
return new Expression.Literal(
new Date(v.value as string | number).toLocaleString(),
);
}
} }
e = e.slice();
e[i] = v;
} }
} return new Expression.Literal(null);
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2])) });
return new Date(e[2]).toLocaleString();
return e;
});
let parameter = null; return { value: lit.value, timestamp, parameter: valueMap.get(lit) };
let value = null; },
);
if (!Array.isArray(exp)) {
value = exp;
} else if (exp[0] === "PARAM") {
const p = obj[exp[1]];
if (p?.value) {
timestamp = p.valueTimestamp;
value = p.value[0];
parameter = exp[1];
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toLocaleString();
}
}
return { value, timestamp, parameter };
});
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
@@ -65,7 +63,7 @@ const component: ClosureComponent = (): Component => {
if (value == null) return null; if (value == null) return null;
let edit; let edit;
if (device[parameter]?.writable) { if (device[parameter + ":writable"]) {
edit = m( edit = m(
"button", "button",
{ {
@@ -73,13 +71,9 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.stageSpv({ taskQueue.stageSpv({
name: "setParameterValues", name: "setParameterValues",
devices: [device["DeviceID.ID"].value[0]], devices: [device["DeviceID.ID"]],
parameterValues: [ parameterValues: [
[ [parameter, device[parameter], device[parameter + ":type"]],
parameter,
device[parameter].value[0],
device[parameter].value[1],
],
], ],
}); });
}, },
+14 -6
View File
@@ -1,13 +1,20 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent } from "mithril";
import { m } from "../components.ts"; import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts"; import * as taskQueue from "../task-queue.ts";
import * as store from "../store.ts"; import * as store from "../store.ts";
import * as notifications from "../notifications.ts"; import * as notifications from "../notifications.ts";
import Expression from "../../lib/common/expression.ts";
import { FlatDevice } from "../../lib/ui/db.ts";
const component: ClosureComponent = (): Component => { interface Attrs {
device: FlatDevice;
parameters: Record<string, Expression>;
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs.device;
return m( return m(
"button", "button",
@@ -19,15 +26,16 @@ const component: ClosureComponent = (): Component => {
e.target.disabled = true; e.target.disabled = true;
const params = Object.values(vnode.attrs["parameters"]) const params = Object.values(vnode.attrs["parameters"])
.map((exp) => { .map((exp) => {
if (!Array.isArray(exp) || exp[0] !== "PARAM") return null; if (exp instanceof Expression.Parameter)
return store.evaluateExpression(exp[1], device) as string; return exp.path.toString();
return null;
}) })
.filter((exp) => !!exp); .filter((exp) => !!exp);
const task = { const task = {
name: "getParameterValues", name: "getParameterValues",
parameterNames: params, parameterNames: params,
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"] as string,
}; };
taskQueue taskQueue
+5 -4
View File
@@ -4,7 +4,7 @@ import * as notifications from "../notifications.ts";
import * as store from "../store.ts"; import * as store from "../store.ts";
import { icon } from "../tailwind-utility-components.ts"; import { icon } from "../tailwind-utility-components.ts";
import { decodeTag } from "../../lib/util.ts"; import { decodeTag } from "../../lib/util.ts";
import { Expression } from "../../lib/types.ts"; import Expression from "../../lib/common/expression.ts";
import { FlatDevice } from "../../lib/ui/db.ts"; import { FlatDevice } from "../../lib/ui/db.ts";
interface Attrs { interface Attrs {
@@ -22,7 +22,8 @@ const component: ClosureComponent<Attrs> = () => {
const tags = []; const tags = [];
for (const p of Object.keys(device)) for (const p of Object.keys(device))
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5))); if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
tags.push(decodeTag(p.slice(5)));
tags.sort(); tags.sort();
@@ -60,7 +61,7 @@ const component: ClosureComponent<Attrs> = () => {
"flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white", "flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
onclick: (e) => { onclick: (e) => {
e.target.disabled = true; e.target.disabled = true;
const deviceId = device["DeviceID.ID"].value[0] as string; const deviceId = device["DeviceID.ID"] as string;
store store
.updateTags(deviceId, { [tag]: false }) .updateTags(deviceId, { [tag]: false })
.then(() => { .then(() => {
@@ -103,7 +104,7 @@ const component: ClosureComponent<Attrs> = () => {
"flex-shrink-0 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white", "flex-shrink-0 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
onclick: (e) => { onclick: (e) => {
e.target.disabled = true; e.target.disabled = true;
const deviceId = device["DeviceID.ID"].value[0] as string; const deviceId = device["DeviceID.ID"] as string;
const tag = prompt(`Enter tag to assign to device:`); const tag = prompt(`Enter tag to assign to device:`);
if (!tag) { if (!tag) {
e.target.disabled = false; e.target.disabled = false;
+3 -3
View File
@@ -5,7 +5,7 @@ import * as notifications from "./notifications.ts";
import putFormComponent from "./put-form-component.ts"; import putFormComponent from "./put-form-component.ts";
import uiConfigComponent from "./ui-config-component.ts"; import uiConfigComponent from "./ui-config-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import { parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts"; import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts";
import { icon } from "./tailwind-utility-components.ts"; import { icon } from "./tailwind-utility-components.ts";
@@ -30,7 +30,7 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
if (!id.match(regex)) return void resolve({ _id: "Invalid ID" }); if (!id.match(regex)) return void resolve({ _id: "Invalid ID" });
try { try {
object.value = stringify(parse(object.value || "")); object.value = Expression.parse(object.value || "").toString();
} catch { } catch {
return void resolve({ return void resolve({
value: "Config value must be valid expression", value: "Config value must be valid expression",
@@ -232,7 +232,7 @@ export const component: ClosureComponent = (): Component => {
}, },
); );
const confs = store.fetch("config", true); const confs = store.fetch("config", new Expression.Literal(true));
let newConfig; let newConfig;
const subs = []; const subs = [];
+111 -1
View File
@@ -1,3 +1,113 @@
export default window.clientConfig; import Expression from "../lib/common/expression.ts";
export const configSnapshot = window.configSnapshot; export const configSnapshot = window.configSnapshot;
export const genieacsVersion = window.genieacsVersion; export const genieacsVersion = window.genieacsVersion;
type Filters = { label: string; parameter: Expression; type: string }[];
type pageSize = number;
type overview = {
charts: {
[name: string]: {
label: string;
slices: {
label: string;
filter: Expression;
color: string;
}[];
};
};
groups: {
label: string;
charts: string[];
}[];
};
type Index = {
label: string;
type?: string;
parameter: Expression;
unsortable: boolean;
raw: NestedRecord;
}[];
type NestedRecord = { [k: string]: Expression | NestedRecord };
const conf: NestedRecord = {};
for (const [key, value] of Object.entries(window.clientConfig)) {
const exp = Expression.parse(value).evaluate((e) => e);
let ref = conf;
const keyParts = key.split(".");
while (keyParts.length > 1) {
const k = keyParts.shift();
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
ref = ref[k] as NestedRecord;
}
ref[keyParts[0]] = exp;
}
export const filters: Filters = [];
export let pageSize: number = 10;
export const overview: overview = { charts: {}, groups: [] };
export const index: Index = [];
export let device: NestedRecord = {};
for (const obj of Object.values(conf["filters"] || {})) {
let label = "";
let parameter: Expression = new Expression.Literal(false);
let type = "string";
if (obj["label"] instanceof Expression.Literal)
label = obj["label"].value as string;
if (obj["parameter"] instanceof Expression) parameter = obj["parameter"];
if (obj["type"] instanceof Expression.Literal)
type = obj["type"].value as string;
filters.push({ label, parameter, type });
}
for (const obj of Object.values(conf["index"] || {})) {
let label = "";
let parameter: Expression = new Expression.Literal(null);
let unsortable = false;
let type = "";
if (obj["label"] instanceof Expression.Literal)
label = obj["label"].value as string;
if (obj["type"] instanceof Expression.Literal)
type = obj["type"].value as string;
if (obj["parameter"] instanceof Expression) parameter = obj["parameter"];
if (obj["unsortable"] instanceof Expression.Literal)
unsortable = obj["unsortable"].value as boolean;
index.push({ label, type, parameter, unsortable, raw: obj });
}
for (const obj of Object.values(conf["overview"]?.["groups"] || {})) {
let label = "";
const charts: string[] = [];
if (obj["label"] instanceof Expression.Literal)
label = obj["label"].value as string;
for (const chart of Object.values(obj["charts"] || {})) {
if (chart instanceof Expression.Literal) charts.push(chart.value as string);
}
overview.groups.push({ label, charts });
}
for (const [name, obj] of Object.entries(conf["overview"]?.["charts"] || {})) {
const slices: { label: string; filter: Expression; color: string }[] = [];
for (const slice of Object.values(obj["slices"] || {})) {
let label = "";
let filter: Expression = new Expression.Literal(false);
let color = "";
if (slice["label"] instanceof Expression.Literal)
label = slice["label"].value as string;
if (slice["filter"] instanceof Expression) filter = slice["filter"];
if (slice["color"] instanceof Expression.Literal)
color = slice["color"].value as string;
slices.push({ label, filter, color });
}
let label = "";
if (obj["label"] instanceof Expression.Literal)
label = obj["label"].value as string;
overview.charts[name] = { label, slices };
}
if (conf["pageSize"] instanceof Expression.Literal)
pageSize = +conf["pageSize"].value || 10;
device = conf["device"] as NestedRecord;
+20 -9
View File
@@ -1,7 +1,9 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { device as deviceConfig } from "./config.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import Expression from "../lib/common/expression.ts";
import Path from "../lib/common/path.ts";
export function init( export function init(
args: Record<string, unknown>, args: Record<string, unknown>,
@@ -14,16 +16,25 @@ export function init(
return Promise.resolve({ return Promise.resolve({
deviceId: args.id, deviceId: args.id,
deviceFilter: ["=", ["PARAM", "DeviceID.ID"], args.id], deviceFilter: new Expression.Binary(
"=",
new Expression.Parameter(Path.parse("DeviceID.ID")),
new Expression.Literal(args.id as string),
),
}); });
} }
export const component: ClosureComponent = (): Component => { interface Attrs {
deviceId: string;
deviceFilter: Expression;
}
export const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`; document.title = `${vnode.attrs.deviceId} - Devices - GenieACS`;
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]); const dev = store.fetch("devices", vnode.attrs.deviceFilter);
if (!dev.value.length) { if (!dev.value.length) {
if (!dev.fulfilling) { if (!dev.fulfilling) {
return m( return m(
@@ -38,15 +49,15 @@ export const component: ClosureComponent = (): Component => {
); );
} }
const conf = config.ui.device; const conf = deviceConfig;
const cmps = []; const cmps = [];
for (const c of Object.values(conf)) { for (const c of Object.values(conf)) {
cmps.push( cmps.push(
m.context( m.context(
{ device: dev.value[0], deviceQuery: dev }, { device: dev.value[0], deviceQuery: dev },
store.evaluateExpression(c["type"], null) as string, store.evaluateExpression(c["type"], {}).value as string,
c, c as any,
), ),
); );
} }
+68 -56
View File
@@ -1,63 +1,72 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Children } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE, index as indexConfig } from "./config.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import { queueTask, stageDownload } from "./task-queue.ts"; import { queueTask, stageDownload } from "./task-queue.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
import { parse, stringify, map } from "../lib/common/expression/parser.ts"; import Expression, { extractPaths } from "../lib/common/expression.ts";
import { evaluate, extractParams } from "../lib/common/expression/util.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
const PAGE_SIZE = config.ui.pageSize || 10; const memoizedGetSortable = memoize((p: Expression) => {
const expressionParams = extractPaths(p);
const memoizedParse = memoize(parse); if (expressionParams.length === 1) return expressionParams[0];
const memoizedJsonParse = memoize(JSON.parse);
const memoizedGetSortable = memoize((p) => {
const expressionParams = extractParams(p);
if (expressionParams.length === 1) {
const param = evaluate(expressionParams[0]);
if (typeof param === "string") return param;
}
return null; return null;
}); });
const getDownloadUrl = memoize((filter, indexParameters) => { const getDownloadUrl = memoize(
const columns = {}; (
for (const p of indexParameters) filter: Expression,
columns[store.evaluateExpression(p.label, null) as string] = stringify( indexParameters: { label: string; parameter: Expression }[],
p.parameter, ) => {
); const columns = {};
return `api/devices.csv?${m.buildQueryString({ for (const p of indexParameters) columns[p.label] = p.parameter.toString();
filter: stringify(filter), return `api/devices.csv?${m.buildQueryString({
columns: JSON.stringify(columns), filter: filter.toString(),
})}`; columns: JSON.stringify(columns),
}); })}`;
},
);
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("devices", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"devices",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
export function init( export function init(args: Record<string, unknown>): Promise<Attrs> {
args: Record<string, unknown>,
): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!window.authorizer.hasAccess("devices", 2)) if (!window.authorizer.hasAccess("devices", 2))
return void reject(new Error("You are not authorized to view this page")); return void reject(new Error("You are not authorized to view this page"));
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let filter: Expression = null;
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let sort: Record<string, number> = null;
const indexParameters = Object.values(config.ui.index); if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
const indexParameters = indexConfig;
if (!indexParameters.length) { if (!indexParameters.length) {
indexParameters.push({ indexParameters.push({
label: "ID", label: "ID",
parameter: ["PARAM", "DeviceID.ID"], parameter: Expression.parse("DeviceID.ID"),
unsortable: false,
raw: {},
}); });
} }
resolve({ filter, indexParameters, sort }); resolve({ filter, indexParameters, sort });
@@ -220,11 +229,17 @@ function renderActions(selected: Set<string>): Children {
return buttons; return buttons;
} }
export const component: ClosureComponent = (): Component => { interface Attrs {
indexParameters: typeof indexConfig;
filter?: Expression;
sort?: Record<string, number>;
}
export const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
document.title = "Devices - GenieACS"; document.title = "Devices - GenieACS";
const attributes = vnode.attrs["indexParameters"]; const attributes = vnode.attrs.indexParameters;
function showMore(): void { function showMore(): void {
vnode.state["showCount"] = vnode.state["showCount"] =
@@ -232,22 +247,22 @@ export const component: ClosureComponent = (): Component => {
m.redraw(); m.redraw();
} }
function onFilterChanged(filter): void { function onFilterChanged(filter: Expression): void {
const ops = { filter }; const ops = {};
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs.sort) ops["sort"] = vnode.attrs.sort;
m.route.set("/devices", ops); m.route.set("/devices", ops);
} }
const sort = vnode.attrs["sort"] const sort = vnode.attrs.sort || {};
? memoizedJsonParse(vnode.attrs["sort"])
: {};
const sortAttributes = {}; const sortAttributes = {};
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i]; const attr = attributes[i];
if (attr.unsortable) continue; if (attr.unsortable) continue;
const param = memoizedGetSortable(attr.parameter); const param = memoizedGetSortable(attr.parameter);
if (param) sortAttributes[i] = sort[param] || 0; if (param) sortAttributes[i] = sort[param.toString()] || 0;
} }
function onSortChange(sortedAttrs): void { function onSortChange(sortedAttrs): void {
@@ -256,17 +271,16 @@ export const component: ClosureComponent = (): Component => {
const param = memoizedGetSortable( const param = memoizedGetSortable(
attributes[Math.abs(index) - 1].parameter, attributes[Math.abs(index) - 1].parameter,
); );
_sort[param] = Math.sign(index); _sort[param.toString()] = Math.sign(index);
} }
const ops = { sort: JSON.stringify(_sort) }; const ops = { sort: JSON.stringify(_sort) };
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"]; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/devices", ops); m.route.set("/devices", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const devs = store.fetch("devices", filter, { const devs = store.fetch("devices", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
@@ -280,15 +294,15 @@ export const component: ClosureComponent = (): Component => {
return m.context( return m.context(
{ device: device, parameter: attr.parameter }, { device: device, parameter: attr.parameter },
attr.type || "parameter", attr.type || "parameter",
attr, attr.raw,
); );
}; };
const attrs = {}; const attrs = {};
attrs["attributes"] = attributes.map((a) => ({ attrs["attributes"] = attributes.map((a) => ({
...a, ...a,
label: store.evaluateExpression(a.label, null), label: a.label,
type: store.evaluateExpression(a.type, null), type: a.type,
})); }));
attrs["data"] = devs.value; attrs["data"] = devs.value;
attrs["total"] = count.value; attrs["total"] = count.value;
@@ -301,9 +315,7 @@ export const component: ClosureComponent = (): Component => {
return m( return m(
"a.text-cyan-700 hover:text-cyan-900", "a.text-cyan-700 hover:text-cyan-900",
{ {
href: `#!/devices/${encodeURIComponent( href: `#!/devices/${encodeURIComponent(device["DeviceID.ID"])}`,
device["DeviceID.ID"].value[0],
)}`,
}, },
"Show", "Show",
); );
+2 -1
View File
@@ -18,6 +18,7 @@ import {
queueTask, queueTask,
StageTask, StageTask,
} from "./task-queue.ts"; } from "./task-queue.ts";
import Expression from "../lib/common/expression.ts";
const invalid: WeakSet<StageTask> = new WeakSet(); const invalid: WeakSet<StageTask> = new WeakSet();
@@ -95,7 +96,7 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
function renderStagingDownload(task: StageTask): Children { function renderStagingDownload(task: StageTask): Children {
if (!task.fileName || !task.fileType) invalid.add(task); if (!task.fileName || !task.fileType) invalid.add(task);
else invalid.delete(task); else invalid.delete(task);
const files = store.fetch("files", true); const files = store.fetch("files", new Expression.Literal(true));
let oui = ""; let oui = "";
let productClass = ""; let productClass = "";
for (const d of task.devices) { for (const d of task.devices) {
+30 -18
View File
@@ -1,18 +1,15 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
import { stringify as yamlStringify } from "../lib/common/yaml.ts"; import { stringify as yamlStringify } from "../lib/common/yaml.ts";
import Expression from "../lib/common/expression.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
@@ -33,15 +30,27 @@ const getDownloadUrl = memoize((filter) => {
} }
return `api/faults.csv?${m.buildQueryString({ return `api/faults.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("faults", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"faults",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -66,9 +75,11 @@ export function init(
new Error("You are not authorized to view this page"), new Error("You are not authorized to view this page"),
); );
} }
let filter: Expression = null;
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let sort: Record<string, number> = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return Promise.resolve({ filter, sort }); return Promise.resolve({ filter, sort });
} }
@@ -84,7 +95,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/faults", ops); m.route.set("/faults", ops);
} }
@@ -108,10 +121,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/faults", ops); m.route.set("/faults", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const faults = store.fetch("faults", filter, { const faults = store.fetch("faults", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
+30 -17
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
@@ -9,11 +9,8 @@ import putFormComponent from "./put-form-component.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes: { const attributes: {
@@ -46,10 +43,22 @@ const formData = {
.concat([{ id: "file", label: "File", type: "file" }]), .concat([{ id: "file", label: "File", type: "file" }]),
}; };
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("files", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"files",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -87,7 +96,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {}; const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id; for (const attr of attributes) cols[attr.label] = attr.id;
return `api/files.csv?${m.buildQueryString({ return `api/files.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
@@ -101,8 +110,11 @@ export function init(
); );
} }
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let filter: Expression = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let sort: Record<string, number> = null;
if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return Promise.resolve({ filter, sort }); return Promise.resolve({ filter, sort });
} }
@@ -118,7 +130,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/files", ops); m.route.set("/files", ops);
} }
@@ -140,10 +154,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/files", ops); m.route.set("/files", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const files = store.fetch("files", filter, { const files = store.fetch("files", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
+51 -33
View File
@@ -1,10 +1,9 @@
import m, { ClosureComponent } from "mithril"; import m, { ClosureComponent } from "mithril";
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
import Autocomplete from "./autocomplete-compnent.ts"; import Autocomplete from "./autocomplete-compnent.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { validQuery } from "../lib/db/synth.ts"; import { validQuery } from "../lib/db/synth.ts";
import { Expression } from "../lib/types.ts"; import Expression from "../lib/common/expression.ts";
const getAutocomplete = memoize((resource) => { const getAutocomplete = memoize((resource) => {
const labels = smartQuery.getLabels(resource); const labels = smartQuery.getLabels(resource);
@@ -22,20 +21,35 @@ const getAutocomplete = memoize((resource) => {
return autocomplete; return autocomplete;
}); });
function parseFilter(resource, f): Expression { function parseFilter(resource: string, f: string): Expression {
let exp; let exp;
if (/^[\s0-9a-zA-Z]+:/.test(f)) { if (/^[\s0-9a-zA-Z]+:/.test(f)) {
const k = f.split(":", 1)[0]; const k = f.split(":", 1)[0];
const v = f.slice(k.length + 1).trim(); const v = f.slice(k.length + 1).trim();
exp = ["FUNC", "Q", k.trim(), v]; exp = new Expression.FunctionCall("Q", [
new Expression.Literal(k.trim()),
new Expression.Literal(v),
]);
} else { } else {
exp = parse(f); exp = Expression.parse(f);
} }
const unpacked = map(exp, (e) => { const unpacked = exp.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC") { if (e instanceof Expression.FunctionCall) {
if (e[1] === "Q") return smartQuery.unpack(resource, e[2], e[3]); if (e.name === "NOW") return new Expression.Literal(Date.now());
else if (e[1] === "NOW") return Date.now(); else if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
const r = smartQuery.unpack(
resource,
e.args[0].value as string,
e.args[1].value as string,
);
return r;
}
}
} }
return e; return e;
}); });
@@ -46,28 +60,33 @@ function parseFilter(resource, f): Expression {
return exp; return exp;
} }
function stringifyFilter(f: Expression): string { function splitFilter(filter: Expression): string[] {
if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q")
return `${f[2]}: ${f[3]}`;
return stringify(f);
}
function splitFilter(filter: string): string[] {
if (!filter) return [""]; if (!filter) return [""];
const list: string[] = []; if (filter instanceof Expression.Literal && filter.value) return [""];
const f = parse(filter); const list: Expression[] = [filter];
if (Array.isArray(f) && f[0] === "AND") const res: string[] = [];
for (const ff of f.slice(1)) list.push(stringifyFilter(ff)); while (list.length) {
else list.push(stringifyFilter(f)); const f = list.pop();
if (f instanceof Expression.Binary && f.operator === "AND") {
list.push(f.right);
list.push(f.left);
} else if (f instanceof Expression.FunctionCall && f.name === "Q") {
const l = f.args[0] as Expression.Literal;
const r = f.args[1] as Expression.Literal;
res.push(`${l.value}: ${r.value}`);
} else {
res.push(f.toString());
}
}
list.push(""); res.push("");
return list; return res;
} }
interface Attrs { interface Attrs {
resource: string; resource: string;
filter: string; filter?: Expression;
onChange: (filter: string) => void; onChange: (filter: Expression) => void;
} }
const component: ClosureComponent<Attrs> = (initialVnode) => { const component: ClosureComponent<Attrs> = (initialVnode) => {
@@ -80,23 +99,22 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
filterTouched = false; filterTouched = false;
filterInvalid = 0; filterInvalid = 0;
filterList = filterList.filter((f) => f); filterList = filterList.filter((f) => f);
const list = filterList.map((f, idx) => { let filter: Expression = new Expression.Literal(true);
for (const [idx, f] of filterList.entries()) {
try { try {
return parseFilter(attrs.resource, f); filter = Expression.and(filter, parseFilter(attrs.resource, f));
} catch { } catch {
filterInvalid |= 1 << idx; filterInvalid |= 1 << idx;
} }
return null; }
});
filterList.push(""); filterList.push("");
if (filterInvalid) { if (filterInvalid) {
m.redraw(); m.redraw();
return; return;
} }
if (list.length === 0) attrs.onChange(""); if (!filterList.length) attrs.onChange(null);
else if (list.length > 1) attrs.onChange(stringify(["AND", ...list])); else attrs.onChange(filter);
else attrs.onChange(stringify(list[0]));
} }
return { return {
@@ -120,7 +138,7 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
"appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm"; "appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm";
if (idx === 0) classNames += " rounded-t-md"; if (idx === 0) classNames += " rounded-t-md";
if (idx === filterList.length - 1) classNames += " rounded-b-md"; if (idx === filterList.length - 1) classNames += " rounded-b-md";
if (filterInvalid & (1 << idx)) classNames += " text-red-700"; if (filterInvalid & (1 << idx)) classNames += " !text-red-700";
return m(`input`, { return m(`input`, {
type: "text", type: "text",
+4 -4
View File
@@ -63,7 +63,7 @@ function renderTable(
checked: records.length && selected.size === records.length, checked: records.length && selected.size === records.length,
onchange: (e) => { onchange: (e) => {
for (const record of records) { for (const record of records) {
const id = record["_id"] || record["DeviceID.ID"].value[0]; const id = record["_id"] ?? record["DeviceID.ID"];
if (e.target.checked) selected.add(id); if (e.target.checked) selected.add(id);
else selected.delete(id); else selected.delete(id);
} }
@@ -148,7 +148,7 @@ function renderTable(
// Table rows // Table rows
const rows = []; const rows = [];
for (const record of records) { for (const record of records) {
const id = record["_id"] || record["DeviceID.ID"].value[0]; const id = record["_id"] ?? record["DeviceID.ID"];
const tds = []; const tds = [];
const isSelected = selected.has(id); const isSelected = selected.has(id);
if (buttons.length) { if (buttons.length) {
@@ -224,7 +224,7 @@ function renderTable(
{ {
class: isSelected ? "bg-stone-50" : "", class: isSelected ? "bg-stone-50" : "",
onclick: (e) => { onclick: (e) => {
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { if (e.target.closest("input, button, a")) {
e.redraw = false; e.redraw = false;
return; return;
} }
@@ -339,7 +339,7 @@ const component: ClosureComponent = (): Component => {
const _selected = new Set<string>(); const _selected = new Set<string>();
for (const record of data) { for (const record of data) {
const id = record["_id"] || record["DeviceID.ID"].value[0]; const id = record["_id"] ?? record["DeviceID.ID"];
if (selected.has(id)) _selected.add(id); if (selected.has(id)) _selected.add(id);
} }
+25 -28
View File
@@ -1,31 +1,29 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { overview } from "./config.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import pieChartComponent from "./pie-chart-component.ts"; import pieChartComponent from "./pie-chart-component.ts";
const GROUPS = config.ui.overview.groups || {}; const GROUPS = overview.groups;
const CHARTS = {}; const CHARTS: typeof overview.charts = {};
for (const group of Object.values(GROUPS)) { for (const group of GROUPS) {
for (const chartName of Object.values(group["charts"]) as string[]) for (const chartName of group.charts)
CHARTS[chartName] = config.ui.overview.charts[chartName]; CHARTS[chartName] = overview.charts[chartName];
} }
function queryCharts(charts: Record<string, unknown>): Record<string, unknown> { function queryCharts(charts: typeof overview.charts): typeof charts {
charts = Object.assign({}, charts); charts = Object.assign({}, charts);
for (let [chartName, chart] of Object.entries(charts)) { for (let [chartName, chart] of Object.entries(charts)) {
charts[chartName] = chart = Object.assign({}, chart); charts[chartName] = chart = { ...chart };
chart["slices"] = Object.assign({}, chart["slices"]); chart.slices = chart.slices.map((s) => ({ ...s }));
for (let [sliceName, slice] of Object.entries(chart["slices"])) { for (const slice of chart.slices) {
const filter = slice["filter"]; slice["count"] = store.count("devices", slice.filter);
chart["slices"][sliceName] = slice = Object.assign({}, slice);
slice["count"] = store.count("devices", filter);
} }
} }
return charts; return charts;
} }
export function init(): Promise<Record<string, unknown>> { export function init(): Promise<{ charts: typeof overview.charts }> {
if (!window.authorizer.hasAccess("devices", 1)) { if (!window.authorizer.hasAccess("devices", 1)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page"), new Error("You are not authorized to view this page"),
@@ -35,37 +33,36 @@ export function init(): Promise<Record<string, unknown>> {
return Promise.resolve({ charts: queryCharts(CHARTS) }); return Promise.resolve({ charts: queryCharts(CHARTS) });
} }
export const component: ClosureComponent = (): Component => { interface Attrs {
charts: typeof overview.charts;
}
export const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
document.title = "Overview - GenieACS"; document.title = "Overview - GenieACS";
const children = []; const children = [];
for (const group of Object.values(GROUPS)) { for (const group of GROUPS) {
if (group["label"]) { if (group.label) {
children.push( children.push(
m( m("h1.text-xl font-medium text-stone-900 mb-5", group["label"]),
"h1.text-xl font-medium text-stone-900 mb-5",
store.evaluateExpression(group["label"], null),
),
); );
} }
const groupChildren = []; const groupChildren = [];
for (const chartName of Object.values(group["charts"]) as string[]) { for (const chartName of group.charts) {
const chart = vnode.attrs["charts"][chartName]; const chart = vnode.attrs.charts[chartName];
const chartChildren = []; const chartChildren = [];
if (chart.label) { if (chart.label) {
chartChildren.push( chartChildren.push(
m( m(
"h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center", "h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center",
store.evaluateExpression(chart.label, null), chart.label,
), ),
); );
} }
const attrs = {}; chartChildren.push(m(pieChartComponent, { chart }));
attrs["chart"] = chart;
chartChildren.push(m(pieChartComponent, attrs));
groupChildren.push( groupChildren.push(
m( m(
+33 -19
View File
@@ -1,6 +1,6 @@
import { Children, ClosureComponent, Component } from "mithril"; import { Children, ClosureComponent, Component } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
@@ -8,12 +8,10 @@ import putFormComponent from "./put-form-component.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
const PAGE_SIZE = config.ui.pageSize || 10; const memoizedParse = memoize((str) => Expression.parse(str));
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
@@ -60,10 +58,22 @@ function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
return lines; return lines;
} }
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("permissions", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"permissions",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -89,7 +99,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
if (object.filter) { if (object.filter) {
try { try {
object.filter = stringify(memoizedParse(object.filter)); object.filter = memoizedParse(object.filter).toString();
} catch { } catch {
return void resolve({ return void resolve({
filter: "Filter must be valid expression", filter: "Filter must be valid expression",
@@ -99,7 +109,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
if (object.validate) { if (object.validate) {
try { try {
object.validate = stringify(memoizedParse(object.validate)); object.validate = memoizedParse(object.validate).toString();
} catch { } catch {
return void resolve({ return void resolve({
validate: "Validate must be valid expression", validate: "Validate must be valid expression",
@@ -163,7 +173,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {}; const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id; for (const attr of attributes) cols[attr.label] = attr.id;
return `api/permissions.csv?${m.buildQueryString({ return `api/permissions.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
@@ -176,8 +186,11 @@ export function init(
new Error("You are not authorized to view this page"), new Error("You are not authorized to view this page"),
); );
} }
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let filter: Expression = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let sort: Record<string, number> = null;
if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return Promise.resolve({ filter, sort }); return Promise.resolve({ filter, sort });
} }
@@ -193,7 +206,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/permissions", ops); m.route.set("/permissions", ops);
} }
@@ -218,10 +233,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/permissions", ops); m.route.set("/permissions", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const permissions = store.fetch("permissions", filter, { const permissions = store.fetch("permissions", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
+21 -14
View File
@@ -1,12 +1,8 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Children } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import { stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import memoize from "../lib/common/memoize.ts";
import * as store from "./store.ts";
const memoizedStringify = memoize(stringify); function drawChart(chartData: Attrs["chart"]): Children {
function drawChart(chartData): Children {
const slices = chartData.slices; const slices = chartData.slices;
const total: number = Array.from(Object.values(chartData.slices)).reduce( const total: number = Array.from(Object.values(chartData.slices)).reduce(
(a: number, s) => a + (s["count"]["value"] || 0), (a: number, s) => a + (s["count"]["value"] || 0),
@@ -27,10 +23,10 @@ function drawChart(chartData): Children {
m( m(
"td", "td",
m("span.inline-block w-3 h-3 border border-stone-200 mr-1", { m("span.inline-block w-3 h-3 border border-stone-200 mr-1", {
style: `background-color: ${store.evaluateExpression(slice["color"], null)} !important;`, style: `background-color: ${slice.color} !important;`,
}), }),
), ),
m("td.w-full", store.evaluateExpression(slice["label"], null)), m("td.w-full", slice.label),
m( m(
"td.text-stone-500 text-right tabular-nums", "td.text-stone-500 text-right tabular-nums",
`${Math.round(percent * 100)}%`, `${Math.round(percent * 100)}%`,
@@ -41,7 +37,7 @@ function drawChart(chartData): Children {
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2", "a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
{ {
href: `#!/devices/?${m.buildQueryString({ href: `#!/devices/?${m.buildQueryString({
filter: memoizedStringify(slice["filter"]), filter: slice.filter.toString(),
})}`, })}`,
}, },
slice["count"]["value"] || 0, slice["count"]["value"] || 0,
@@ -67,7 +63,7 @@ function drawChart(chartData): Children {
paths.push( paths.push(
m("path.stroke-white stroke-1", { m("path.stroke-white stroke-1", {
d: sketch, d: sketch,
fill: store.evaluateExpression(slice["color"], null), fill: slice.color,
}), }),
); );
@@ -81,7 +77,7 @@ function drawChart(chartData): Children {
"a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden", "a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden",
{ {
"xlink:href": `#!/devices/?${m.buildQueryString({ "xlink:href": `#!/devices/?${m.buildQueryString({
filter: memoizedStringify(slice["filter"]), filter: slice.filter.toString(),
})}`, })}`,
}, },
[ [
@@ -137,10 +133,21 @@ function drawChart(chartData): Children {
); );
} }
const component: ClosureComponent = (): Component => { interface Attrs {
chart: {
label: string;
slices: {
label: string;
filter: Expression;
color: string;
}[];
};
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: (vnode) => {
return drawChart(vnode.attrs["chart"]); return drawChart(vnode.attrs.chart);
}, },
}; };
}; };
+36 -19
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children, Vnode } from "mithril"; import { ClosureComponent, Component, Children, Vnode } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
@@ -9,11 +9,9 @@ import putFormComponent from "./put-form-component.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
const PAGE_SIZE = config.ui.pageSize || 10; const memoizedParse = memoize((str) => Expression.parse(str));
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
@@ -27,10 +25,22 @@ const attributes = [
{ id: "provisionArgs", label: "Arguments", type: "textarea" }, { id: "provisionArgs", label: "Arguments", type: "textarea" },
]; ];
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("presets", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"presets",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -71,7 +81,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
if (object.precondition) { if (object.precondition) {
try { try {
object.precondition = stringify(memoizedParse(object.precondition)); object.precondition = memoizedParse(object.precondition).toString();
} catch { } catch {
return void resolve({ return void resolve({
precondition: "Precondition must be valid expression", precondition: "Precondition must be valid expression",
@@ -134,7 +144,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {}; const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id; for (const attr of attributes) cols[attr.label] = attr.id;
return `api/presets.csv?${m.buildQueryString({ return `api/presets.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
@@ -148,8 +158,11 @@ export function init(
); );
} }
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let filter: Expression = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let sort: Record<string, number> = null;
if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return Promise.resolve({ filter, sort }); return Promise.resolve({ filter, sort });
} }
@@ -165,7 +178,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/presets", ops); m.route.set("/presets", ops);
} }
@@ -197,10 +212,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/presets", ops); m.route.set("/presets", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const presets = store.fetch("presets", filter, { const presets = store.fetch("presets", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
@@ -220,7 +234,10 @@ export const component: ClosureComponent = (): Component => {
"instances", "instances",
]); ]);
const provisions = store.fetch("provisions", true); const provisions = store.fetch(
"provisions",
new Expression.Literal(true),
);
if (provisions.fulfilled) { if (provisions.fulfilled) {
for (const p of provisions.value) { for (const p of provisions.value) {
userDefinedProvisions.add(p["_id"]); userDefinedProvisions.add(p["_id"]);
+30 -17
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
@@ -9,12 +9,9 @@ import putFormComponent from "./put-form-component.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import { loadCodeMirror } from "./dynamic-loader.ts"; import { loadCodeMirror } from "./dynamic-loader.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
@@ -22,10 +19,22 @@ const attributes = [
{ id: "script", label: "Script", type: "code" }, { id: "script", label: "Script", type: "code" },
]; ];
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("provisions", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"provisions",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {}; const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id; for (const attr of attributes) cols[attr.label] = attr.id;
return `api/provisions.csv?${m.buildQueryString({ return `api/provisions.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
@@ -118,8 +127,11 @@ export function init(
); );
} }
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let filter: Expression = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let sort: Record<string, number> = null;
if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadCodeMirror() loadCodeMirror()
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/provisions", ops); m.route.set("/provisions", ops);
} }
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/provisions", ops); m.route.set("/provisions", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const provisions = store.fetch("provisions", filter, { const provisions = store.fetch("provisions", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
+150 -72
View File
@@ -1,51 +1,103 @@
import config from "./config.ts"; import { filters } from "./config.ts";
import { Expression } from "../lib/types.ts"; import Expression from "../lib/common/expression.ts";
import { encodeTag } from "../lib/util.ts"; import { encodeTag } from "../lib/util.ts";
import Path from "../lib/common/path.ts";
const resources = { const resources = {
devices: {}, devices: {},
faults: { faults: {
Device: { parameter: ["PARAM", "device"], type: "string" }, Device: {
Channel: { parameter: ["PARAM", "channel"], type: "string" }, parameter: new Expression.Parameter(Path.parse("device")),
Code: { parameter: ["PARAM", "code"], type: "string" }, type: "string",
Retries: { parameter: ["PARAM", "retries"], type: "number" }, },
Timestamp: { parameter: ["PARAM", "timestamp"], type: "timestamp" }, Channel: {
parameter: new Expression.Parameter(Path.parse("channel")),
type: "string",
},
Code: {
parameter: new Expression.Parameter(Path.parse("code")),
type: "string",
},
Retries: {
parameter: new Expression.Parameter(Path.parse("retries")),
type: "number",
},
Timestamp: {
parameter: new Expression.Parameter(Path.parse("timestamp")),
type: "timestamp",
},
}, },
presets: { presets: {
ID: { parameter: ["PARAM", "_id"], type: "string" }, ID: {
Channel: { parameter: ["PARAM", "channel"], type: "string" }, parameter: new Expression.Parameter(Path.parse("_id")),
Weight: { parameter: ["PARAM", "weight"], type: "number" }, type: "string",
},
Channel: {
parameter: new Expression.Parameter(Path.parse("channel")),
type: "string",
},
Weight: {
parameter: new Expression.Parameter(Path.parse("weight")),
type: "number",
},
}, },
provisions: { provisions: {
ID: { parameter: ["PARAM", "_id"], type: "string" }, ID: {
parameter: new Expression.Parameter(Path.parse("_id")),
type: "string",
},
}, },
virtualParameters: { virtualParameters: {
ID: { parameter: ["PARAM", "_id"], type: "string" }, ID: {
parameter: new Expression.Parameter(Path.parse("_id")),
type: "string",
},
}, },
files: { files: {
ID: { parameter: ["PARAM", "_id"], type: "string" }, ID: {
Type: { parameter: ["PARAM", "metadata.fileType"], type: "string" }, parameter: new Expression.Parameter(Path.parse("_id")),
OUI: { parameter: ["PARAM", "metadata.oui"], type: "string" }, type: "string",
},
Type: {
parameter: new Expression.Parameter(Path.parse("metadata.fileType")),
type: "string",
},
OUI: {
parameter: new Expression.Parameter(Path.parse("metadata.oui")),
type: "string",
},
"Product class": { "Product class": {
parameter: ["PARAM", "metadata.productClass"], parameter: new Expression.Parameter(Path.parse("metadata.productClass")),
type: "string",
},
Version: {
parameter: new Expression.Parameter(Path.parse("metadata.version")),
type: "string", type: "string",
}, },
Version: { parameter: ["PARAM", "metadata.version"], type: "string" },
}, },
permissions: { permissions: {
Role: { parameter: ["PARAM", "role"], type: "string" }, Role: {
Resource: { parameter: ["PARAM", "resource"], type: "string" }, parameter: new Expression.Parameter(Path.parse("role")),
Access: { parameter: ["PARAM", "access"], type: "number" }, type: "string",
},
Resource: {
parameter: new Expression.Parameter(Path.parse("resource")),
type: "string",
},
Access: {
parameter: new Expression.Parameter(Path.parse("access")),
type: "number",
},
},
users: {
Username: {
parameter: new Expression.Parameter(Path.parse("_id")),
type: "string",
},
}, },
users: { Username: { parameter: ["PARAM", "_id"], type: "string" } },
}; };
for (const v of Object.values( for (const v of filters) {
config.ui.filters as Record<
string,
{ label: string; parameter: string; type: string }
>,
)) {
resources.devices[v.label] = { resources.devices[v.label] = {
parameter: v.parameter, parameter: v.parameter,
type: (v.type || "").split(",").map((s) => s.trim()), type: (v.type || "").split(",").map((s) => s.trim()),
@@ -70,7 +122,7 @@ function queryNumber(param: Expression, value: string): Expression {
const v = parseInt(value); const v = parseInt(value);
if (v !== +value) return null; if (v !== +value) return null;
return [op, param, v]; return new Expression.Binary(op, param, new Expression.Literal(v));
} }
function queryTimestamp(param: Expression, value: string): Expression { function queryTimestamp(param: Expression, value: string): Expression {
@@ -86,52 +138,71 @@ function queryTimestamp(param: Expression, value: string): Expression {
let v = parseInt(value); let v = parseInt(value);
if (v !== +value) v = Date.parse(value); if (v !== +value) v = Date.parse(value);
if (isNaN(v)) return null; if (isNaN(v)) return null;
return [op, param, v]; return new Expression.Binary(op, param, new Expression.Literal(v));
} }
function queryString(param: Expression, value: string): Expression { function queryString(param: Expression, value: string): Expression {
return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()]; return new Expression.Binary(
"LIKE",
new Expression.FunctionCall("LOWER", [param]),
new Expression.Literal(value.toLowerCase()),
);
} }
function queryStringCaseSensitive( function queryStringCaseSensitive(
param: Expression, param: Expression,
value: string, value: string,
): Expression { ): Expression {
return ["LIKE", param, value]; return new Expression.Binary("LIKE", param, new Expression.Literal(value));
} }
function queryStringMonoCase(param: Expression, value: string): Expression { function queryStringMonoCase(param: Expression, value: string): Expression {
return [ return Expression.or(
"OR", new Expression.Binary(
["LIKE", param, value.toLowerCase()], "LIKE",
["LIKE", param, value.toUpperCase()], param,
]; new Expression.Literal(value.toLowerCase()),
),
new Expression.Binary(
"LIKE",
param,
new Expression.Literal(value.toUpperCase()),
),
);
} }
function queryMac(param: Expression, value: string): Expression { function queryMac(param: Expression, value: string): Expression {
value = value.replace(/[^a-f0-9]/gi, "").toLowerCase(); value = value.replace(/[^a-f0-9]/gi, "").toLowerCase();
if (!value) return null; if (!value) return null;
if (value.length === 12) { if (value.length === 12) {
return [ value = value.replace(/(..)(?!$)/g, "$1:");
"OR", return Expression.or(
["=", param, value.replace(/(..)(?!$)/g, "$1:").toLowerCase()], new Expression.Binary(
["=", param, value.replace(/(..)(?!$)/g, "$1:").toUpperCase()], "=",
]; param,
new Expression.Literal(value.toLowerCase()),
),
new Expression.Binary(
"=",
param,
new Expression.Literal(value.toUpperCase()),
),
);
} }
return [ param = new Expression.FunctionCall("LOWER", [param]);
"OR", return Expression.or(
[ new Expression.Binary(
"LIKE", "LIKE",
["FUNC", "LOWER", param], param,
`%${value.replace(/(..)(?!$)/g, "$1:")}%`, new Expression.Literal(`%${value.replace(/(..)(?!$)/g, "$1:")}%`),
], ),
[ new Expression.Binary(
"LIKE", "LIKE",
["FUNC", "LOWER", param], param,
`%${value.replace(/(.)(.)/g, "$1:$2")}%`, new Expression.Literal(`%${value.replace(/(.)(.)/g, "$1:$2")}%`),
], ),
]; );
} }
function queryMacWildcard(param: Expression, value: string): Expression { function queryMacWildcard(param: Expression, value: string): Expression {
@@ -143,23 +214,32 @@ function queryMacWildcard(param: Expression, value: string): Expression {
p.replace(/(.)(.)/gi, "$1:$2"), p.replace(/(.)(.)/gi, "$1:$2"),
]); ]);
const res = new Set(); const set = new Set<string>();
for (let i = 0; i < 2 ** groups.length; ++i) { for (let i = 0; i < 2 ** groups.length; ++i) {
const r = groups.map((g, j) => g[(i >> j) & 1]).join("%"); const r = groups.map((g, j) => g[(i >> j) & 1]).join("%");
if (/^[a-f0-9]:/i.test(r) || /:[a-f0-9]$/i.test(r)) continue; if (/^[a-f0-9]:/i.test(r) || /:[a-f0-9]$/i.test(r)) continue;
res.add(r.toLocaleLowerCase()); set.add(r.toLocaleLowerCase());
res.add(r.toUpperCase()); set.add(r.toUpperCase());
} }
if (!res.size) return queryStringMonoCase(param, value); if (!set.size) return queryStringMonoCase(param, value);
const clauses = [...res].map((r) => ["LIKE", param, r]); let res: Expression = new Expression.Literal(false);
if (clauses.length === 1) return clauses[0]; for (const s of set) {
return ["OR", ...clauses]; res = Expression.or(
res,
new Expression.Binary("LIKE", param, new Expression.Literal(s)),
);
}
return res;
} }
function queryTag(tag: string): Expression { function queryTag(tag: string): Expression {
const t = encodeTag(tag); const t = encodeTag(tag);
return ["IS NOT NULL", ["PARAM", `Tags.${t}`]]; return new Expression.Unary(
"IS NOT NULL",
new Expression.Parameter(Path.parse(`Tags.${t}`)),
);
} }
export function getTip(resource: string, label: string): string { export function getTip(resource: string, label: string): string {
@@ -214,21 +294,21 @@ export function unpack(
if (!resources[resource]) return null; if (!resources[resource]) return null;
const type = resources[resource][label].type; const type = resources[resource][label].type;
value = value.trim(); value = value.trim();
const res: Expression = ["OR"]; let res: Expression = new Expression.Literal(false);
if (type.length === 0 || type.includes("number")) { if (type.length === 0 || type.includes("number")) {
const q = queryNumber(resources[resource][label].parameter, value); const q = queryNumber(resources[resource][label].parameter, value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.length === 0 || type.includes("string")) { if (type.length === 0 || type.includes("string")) {
const q = queryString(resources[resource][label].parameter, value); const q = queryString(resources[resource][label].parameter, value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.length === 0 || type.includes("timestamp")) { if (type.length === 0 || type.includes("timestamp")) {
const q = queryTimestamp(resources[resource][label].parameter, value); const q = queryTimestamp(resources[resource][label].parameter, value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.includes("string-casesensitive")) { if (type.includes("string-casesensitive")) {
@@ -236,30 +316,28 @@ export function unpack(
resources[resource][label].parameter, resources[resource][label].parameter,
value, value,
); );
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.includes("string-monocase")) { if (type.includes("string-monocase")) {
const q = queryStringMonoCase(resources[resource][label].parameter, value); const q = queryStringMonoCase(resources[resource][label].parameter, value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.includes("mac")) { if (type.includes("mac")) {
const q = queryMac(resources[resource][label].parameter, value); const q = queryMac(resources[resource][label].parameter, value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.includes("mac-wildcard")) { if (type.includes("mac-wildcard")) {
const q = queryMacWildcard(resources[resource][label].parameter, value); const q = queryMacWildcard(resources[resource][label].parameter, value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (type.includes("tag")) { if (type.includes("tag")) {
const q = queryTag(value); const q = queryTag(value);
if (q) res.push(q); if (q) res = Expression.or(res, q);
} }
if (res.length <= 1) return null; return res;
else if (res.length === 2) return res[1];
else return res;
} }
+85 -41
View File
@@ -1,8 +1,8 @@
import m from "mithril"; import m from "mithril";
import { stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import { or, and, evaluate } from "../lib/common/expression/util.ts"; import Path from "../lib/common/path.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
import { Expression, Task } from "../lib/types.ts"; import { Task } from "../lib/types.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
import { configSnapshot, genieacsVersion } from "./config.ts"; import { configSnapshot, genieacsVersion } from "./config.ts";
import { QueueTask } from "./task-queue.ts"; import { QueueTask } from "./task-queue.ts";
@@ -14,7 +14,30 @@ import {
toBookmark, toBookmark,
} from "../lib/common/expression/pagination.ts"; } from "../lib/common/expression/pagination.ts";
const memoizedStringify = memoize(stringify); function evaluate(exp: Expression, timestamp: number): Expression;
function evaluate(
exp: Expression,
timestamp: number,
obj: Record<string, unknown>,
): Expression.Literal;
function evaluate(
exp: Expression,
timestamp: number,
obj?: Record<string, unknown>,
): Expression {
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(timestamp);
} else if (e instanceof Expression.Parameter && obj) {
let v = obj[e.path.toString()];
if (v == null) return new Expression.Literal(null);
if (typeof v === "object") v = v["value"]?.[0];
return new Expression.Literal(v as any);
}
return e;
});
}
const memoizedEvaluate = memoize(evaluate); const memoizedEvaluate = memoize(evaluate);
let fulfillTimestamp = 0; let fulfillTimestamp = 0;
@@ -59,7 +82,7 @@ for (const r of [
objects: new Map(), objects: new Map(),
count: new Map(), count: new Map(),
fetch: new Map(), fetch: new Map(),
combinedFilter: null, combinedFilter: new Expression.Literal(false) as Expression,
}; };
} }
@@ -215,13 +238,11 @@ export async function xhrRequest(
} }
export function unpackExpression(exp: Expression): Expression { export function unpackExpression(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp; return memoizedEvaluate(exp, fulfillTimestamp + clockSkew);
const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
return e;
} }
export function count(resourceType: string, filter: Expression): QueryResponse { export function count(resourceType: string, filter: Expression): QueryResponse {
const filterStr = memoizedStringify(filter); const filterStr = filter.toString();
let queryResponse = resources[resourceType].count.get(filterStr); let queryResponse = resources[resourceType].count.get(filterStr);
if (queryResponse) return queryResponse; if (queryResponse) return queryResponse;
@@ -271,7 +292,8 @@ function compareFunction(sort: {
function findMatches(resourceType, filter, sort, limit): any[] { function findMatches(resourceType, filter, sort, limit): any[] {
let value = []; let value = [];
for (const obj of resources[resourceType].objects.values()) for (const obj of resources[resourceType].objects.values())
if (evaluate(filter, obj, fulfillTimestamp + clockSkew)) value.push(obj); if (evaluate(filter, fulfillTimestamp + clockSkew, obj).value)
value.push(obj);
value = value.sort(compareFunction(sort)); value = value.sort(compareFunction(sort));
if (limit) value = value.slice(0, limit); if (limit) value = value.slice(0, limit);
@@ -284,7 +306,6 @@ export function fetch(
filter: Expression, filter: Expression,
options: { limit?: number; sort?: { [param: string]: number } } = {}, options: { limit?: number; sort?: { [param: string]: number } } = {},
): QueryResponse { ): QueryResponse {
const filterStr = memoizedStringify(filter);
const sort = Object.assign({}, options.sort); const sort = Object.assign({}, options.sort);
const limit = options.limit || 0; const limit = options.limit || 0;
@@ -292,7 +313,7 @@ export function fetch(
sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1; sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1;
else sort["_id"] = sort["_id"] || 1; else sort["_id"] = sort["_id"] || 1;
const key = `${filterStr}:${limit}:${JSON.stringify(sort)}`; const key = `${filter.toString()}:${limit}:${JSON.stringify(sort)}`;
let queryResponse = resources[resourceType].fetch.get(key); let queryResponse = resources[resourceType].fetch.get(key);
if (queryResponse) return queryResponse; if (queryResponse) return queryResponse;
@@ -308,7 +329,10 @@ export function fetch(
); );
const matches = findMatches(resourceType, satisfied, sort, limit); const matches = findMatches(resourceType, satisfied, sort, limit);
queries.value.set(queryResponse, matches); queries.value.set(queryResponse, matches);
if (!diff || (limit && matches.length >= limit)) if (
(diff instanceof Expression.Literal && !diff.value) ||
(limit && matches.length >= limit)
)
queries.fulfilled.set(queryResponse, fulfillTimestamp); queries.fulfilled.set(queryResponse, fulfillTimestamp);
else queries.unsatisfied.set(queryResponse, diff); else queries.unsatisfied.set(queryResponse, diff);
return queryResponse; return queryResponse;
@@ -336,7 +360,7 @@ export function fulfill(accessTimestamp: number): void {
url: url:
`api/${resourceType}/?` + `api/${resourceType}/?` +
m.buildQueryString({ m.buildQueryString({
filter: memoizedStringify(filter), filter: filter.toString(),
}), }),
extract: (xhr) => { extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized"); if (xhr.status === 403) throw new Error("Not authorized");
@@ -393,7 +417,7 @@ export function fulfill(accessTimestamp: number): void {
url: url:
`api/${resourceType}/?` + `api/${resourceType}/?` +
m.buildQueryString({ m.buildQueryString({
filter: memoizedStringify(filter), filter: filter.toString(),
limit: 1, limit: 1,
skip: limit - 1, skip: limit - 1,
sort: JSON.stringify(sort), sort: JSON.stringify(sort),
@@ -419,16 +443,19 @@ export function fulfill(accessTimestamp: number): void {
let updated = false; let updated = false;
const allPromises2 = []; const allPromises2 = [];
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) { for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
let combinedFilter = null; let combinedFilter = new Expression.Literal(false) as Expression;
for (const queryResponse of toFetch) { for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew); filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
if (bookmark) if (bookmark)
filter = and(filter, bookmarkToExpression(bookmark, sort)); filter = Expression.and(
combinedFilter = or(combinedFilter, filter); filter,
bookmarkToExpression(bookmark, sort),
);
combinedFilter = Expression.or(combinedFilter, filter);
} }
const [union, diff] = unionDiff( const [union, diff] = unionDiff(
@@ -436,19 +463,18 @@ export function fulfill(accessTimestamp: number): void {
combinedFilter, combinedFilter,
); );
if (!diff) { if (diff instanceof Expression.Literal && !diff.value) {
for (const queryResponse of toFetch) { for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate( filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
filter,
null,
fulfillTimestamp + clockSkew,
);
const limit = queries.limit.get(queryResponse); const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
if (bookmark) if (bookmark)
filter = and(filter, bookmarkToExpression(bookmark, sort)); filter = Expression.and(
filter,
bookmarkToExpression(bookmark, sort),
);
queries.value.set( queries.value.set(
queryResponse, queryResponse,
@@ -462,7 +488,11 @@ export function fulfill(accessTimestamp: number): void {
} }
let deleted = new Set<string>(); let deleted = new Set<string>();
if (!resources[resourceType].combinedFilter) if (
resources[resourceType].combinedFilter instanceof
Expression.Literal &&
!resources[resourceType].combinedFilter.value
)
deleted = new Set(resources[resourceType].objects.keys()); deleted = new Set(resources[resourceType].objects.keys());
const combinedFilterDiff = diff; const combinedFilterDiff = diff;
@@ -474,15 +504,12 @@ export function fulfill(accessTimestamp: number): void {
url: url:
`api/${resourceType}/?` + `api/${resourceType}/?` +
m.buildQueryString({ m.buildQueryString({
filter: memoizedStringify(combinedFilterDiff), filter: combinedFilterDiff.toString(),
}), }),
background: false, background: false,
}).then((res) => { }).then((res) => {
for (const r of res as any[]) { for (const r of res as any[]) {
const id = const id = r["DeviceID.ID"] ?? r["_id"];
resourceType === "devices"
? r["DeviceID.ID"].value[0]
: r["_id"];
resources[resourceType].objects.set(id, r); resources[resourceType].objects.set(id, r);
deleted.delete(id); deleted.delete(id);
} }
@@ -490,7 +517,8 @@ export function fulfill(accessTimestamp: number): void {
for (const d of deleted) { for (const d of deleted) {
const obj = resources[resourceType].objects.get(d); const obj = resources[resourceType].objects.get(d);
if ( if (
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew) evaluate(combinedFilterDiff, fulfillTimestamp + clockSkew, obj)
.value
) )
resources[resourceType].objects.delete(d); resources[resourceType].objects.delete(d);
} }
@@ -502,7 +530,10 @@ export function fulfill(accessTimestamp: number): void {
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
if (bookmark) if (bookmark)
filter = and(filter, bookmarkToExpression(bookmark, sort)); filter = Expression.and(
filter,
bookmarkToExpression(bookmark, sort),
);
queries.value.set( queries.value.set(
queryResponse, queryResponse,
@@ -530,7 +561,7 @@ export function setTimestamp(t: number): void {
if (t > fulfillTimestamp) { if (t > fulfillTimestamp) {
fulfillTimestamp = t; fulfillTimestamp = t;
for (const resource of Object.values(resources)) for (const resource of Object.values(resources))
resource.combinedFilter = null; resource.combinedFilter = new Expression.Literal(false);
} }
} }
@@ -607,23 +638,32 @@ export function putResource(
} }
export function queryConfig(pattern = "%"): Promise<any[]> { export function queryConfig(pattern = "%"): Promise<any[]> {
const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]); const filter = new Expression.Binary(
"LIKE",
new Expression.Parameter(Path.parse("_id")),
new Expression.Literal(pattern),
);
return xhrRequest({ return xhrRequest({
method: "GET", method: "GET",
url: `api/config/?${m.buildQueryString({ filter: filter })}`, url: `api/config/?${m.buildQueryString({ filter: filter.toString() })}`,
background: true, background: true,
}); });
} }
export function resourceExists(resource: string, id: string): Promise<number> { export function resourceExists(resource: string, id: string): Promise<number> {
const param = resource === "devices" ? "DeviceID.ID" : "_id"; const param = resource === "devices" ? "DeviceID.ID" : "_id";
const filter = ["=", ["PARAM", param], id]; const filter = new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(param)),
new Expression.Literal(id),
);
return xhrRequest({ return xhrRequest({
method: "HEAD", method: "HEAD",
url: url:
`api/${resource}/?` + `api/${resource}/?` +
m.buildQueryString({ m.buildQueryString({
filter: memoizedStringify(filter), filter: filter.toString(),
}), }),
extract: (xhr) => { extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized"); if (xhr.status === 403) throw new Error("Not authorized");
@@ -636,12 +676,16 @@ export function resourceExists(resource: string, id: string): Promise<number> {
}); });
} }
export function evaluateExpression(exp: Expression): Expression;
export function evaluateExpression( export function evaluateExpression(
exp: Expression, exp: Expression,
obj: Record<string, unknown>, obj: Record<string, unknown>,
): Expression.Literal;
export function evaluateExpression(
exp: Expression,
obj?: Record<string, unknown>,
): Expression { ): Expression {
if (!Array.isArray(exp)) return exp; return memoizedEvaluate(exp, fulfillTimestamp + clockSkew, obj);
return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
} }
export function changePassword( export function changePassword(
+2 -2
View File
@@ -4,7 +4,7 @@ import * as store from "./store.ts";
import { yaml } from "./dynamic-loader.ts"; import { yaml } from "./dynamic-loader.ts";
import * as configFunctions from "./config-functions.ts"; import * as configFunctions from "./config-functions.ts";
import codeEditorComponent from "./code-editor-component.ts"; import codeEditorComponent from "./code-editor-component.ts";
import { parse } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
function putActionHandler(prefix: string[], dataYaml: string): Promise<any> { function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -27,7 +27,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
} }
// Try parse to ensure valid expressions // Try parse to ensure valid expressions
for (const v of Object.values(updated)) parse(v as string); for (const v of Object.values(updated)) Expression.parse(v as string);
store store
.queryConfig(`${prefix.join(".")}.%`) .queryConfig(`${prefix.join(".")}.%`)
+34 -18
View File
@@ -1,6 +1,6 @@
import { Children, ClosureComponent, Component } from "mithril"; import { Children, ClosureComponent, Component } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
import memoize from "../lib/common/memoize.ts"; import memoize from "../lib/common/memoize.ts";
@@ -8,13 +8,10 @@ import putFormComponent from "./put-form-component.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import changePasswordComponent from "./change-password-component.ts"; import changePasswordComponent from "./change-password-component.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
@@ -22,10 +19,22 @@ const attributes = [
{ id: "roles", label: "Roles", type: "multi", options: [] }, { id: "roles", label: "Roles", type: "multi", options: [] },
]; ];
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("users", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"users",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -120,7 +129,7 @@ const getDownloadUrl = memoize((filter) => {
for (const attr of attributes) cols[attr.label] = attr.id; for (const attr of attributes) cols[attr.label] = attr.id;
return `api/users.csv?${m.buildQueryString({ return `api/users.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
@@ -134,8 +143,11 @@ export function init(
); );
} }
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let filter: Expression = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let sort: Record<string, number> = null;
if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return Promise.resolve({ filter, sort }); return Promise.resolve({ filter, sort });
} }
@@ -151,7 +163,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/users", ops); m.route.set("/users", ops);
} }
@@ -176,10 +190,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/users", ops); m.route.set("/users", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const users = store.fetch("users", filter, { const users = store.fetch("users", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
@@ -189,7 +202,10 @@ export const component: ClosureComponent = (): Component => {
const count = store.count("users", filter); const count = store.count("users", filter);
// Getting the roles // Getting the roles
const permissions = store.fetch("permissions", true); const permissions = store.fetch(
"permissions",
new Expression.Literal(true),
);
if (permissions.fulfilled) { if (permissions.fulfilled) {
for (const attr of attributes) { for (const attr of attributes) {
if (attr.id === "roles") if (attr.id === "roles")
+30 -17
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components.ts"; import { m } from "./components.ts";
import config from "./config.ts"; import { pageSize as PAGE_SIZE } from "./config.ts";
import filterComponent from "./filter-component.ts"; import filterComponent from "./filter-component.ts";
import * as store from "./store.ts"; import * as store from "./store.ts";
import * as notifications from "./notifications.ts"; import * as notifications from "./notifications.ts";
@@ -9,12 +9,9 @@ import putFormComponent from "./put-form-component.ts";
import indexTableComponent from "./index-table-component.ts"; import indexTableComponent from "./index-table-component.ts";
import * as overlay from "./overlay.ts"; import * as overlay from "./overlay.ts";
import * as smartQuery from "./smart-query.ts"; import * as smartQuery from "./smart-query.ts";
import { map, parse, stringify } from "../lib/common/expression/parser.ts"; import Expression from "../lib/common/expression.ts";
import { loadCodeMirror } from "./dynamic-loader.ts"; import { loadCodeMirror } from "./dynamic-loader.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
@@ -22,10 +19,22 @@ const attributes = [
{ id: "script", label: "Script", type: "code" }, { id: "script", label: "Script", type: "code" },
]; ];
const unpackSmartQuery = memoize((query) => { const unpackSmartQuery = memoize((query: Expression) => {
return map(query, (e) => { return query.evaluate((e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") if (e instanceof Expression.FunctionCall) {
return smartQuery.unpack("virtualParameters", e[2], e[3]); if (e.name === "Q") {
if (
e.args[0] instanceof Expression.Literal &&
e.args[1] instanceof Expression.Literal
) {
return smartQuery.unpack(
"virtualParameters",
e.args[0].value as string,
e.args[1].value as string,
);
}
}
}
return e; return e;
}); });
}); });
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {}; const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id; for (const attr of attributes) cols[attr.label] = attr.id;
return `api/virtualParameters.csv?${m.buildQueryString({ return `api/virtualParameters.csv?${m.buildQueryString({
filter: stringify(filter), filter: filter.toString(),
columns: JSON.stringify(cols), columns: JSON.stringify(cols),
})}`; })}`;
}); });
@@ -118,8 +127,11 @@ export function init(
); );
} }
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; let filter: Expression = null;
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; let sort: Record<string, number> = null;
if (args.hasOwnProperty("filter"))
filter = Expression.parse(args["filter"] as string);
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadCodeMirror() loadCodeMirror()
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
} }
function onFilterChanged(filter): void { function onFilterChanged(filter): void {
const ops = { filter }; const ops = {};
if (!(filter instanceof Expression.Literal && filter.value))
ops["filter"] = filter.toString();
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"]; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/virtualParameters", ops); m.route.set("/virtualParameters", ops);
} }
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/virtualParameters", ops); m.route.set("/virtualParameters", ops);
} }
let filter = vnode.attrs["filter"] const filter = unpackSmartQuery(
? memoizedParse(vnode.attrs["filter"]) vnode.attrs["filter"] ?? new Expression.Literal(true),
: true; );
filter = unpackSmartQuery(filter);
const virtualParameters = store.fetch("virtualParameters", filter, { const virtualParameters = store.fetch("virtualParameters", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,