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
+3969 -3373

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)
lib/ All backend logic
common/ Shared utilities (expression engine, Path, errors)
expression.ts Expression class hierarchy (base + subclasses)
expression/ Expression parser, evaluator, normalizer, minimizer
cwmp/ CWMP-service-specific DB and caching
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,
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
important abstraction in the codebase. It is a recursive, Lisp-like S-expression
where scalars are literal values and arrays are operations (the operator at
index 0, operands following). For example:
The `Expression` class (defined in `lib/common/expression.ts`) is the most
important abstraction in the codebase. It uses a typed class hierarchy:
`Expression.Literal` (wraps `string | number | boolean | null`),
`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:
@@ -101,23 +105,28 @@ Expressions are used pervasively:
The expression pipeline flows through four modules:
1. `parser.ts` -- Parses a SQL-like text syntax into the AST (uses Parsimmon).
Also provides `map()` / `mapAsync()` tree-walking primitives and
`stringify()` for serialization.
1. `parser.ts` -- Parses a SQL-like text syntax into the AST using a hand-rolled
recursive descent parser with a `Cursor`-based scanner. Also provides
`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
arithmetic over bigints. Ensures equivalent expressions have the same
canonical form (e.g., `a + 2 > b` and `a > b - 2` normalize identically).
arithmetic over native `bigint` values. The `Polynomial` class extends
`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
(espresso-iisojs). Converts expressions to minimal sum-of-products form with
three-valued logic (true/false/null). Handles domain-specific constraints
like comparison ordering and LIKE pattern relationships.
4. `util.ts` -- Runtime evaluation. Resolves `PARAM` references against concrete
data, evaluates operators, and supports partial evaluation (returns a reduced
expression if some values are unknown).
4. `evaluate.ts` -- Runtime evaluation. The `reduce()` function evaluates
operators on literal values and supports partial evaluation (returns a
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
for readability. `pagination.ts` implements cursor-based pagination by
generating filter expressions from sort-key bookmarks.
`pagination.ts` implements cursor-based pagination by generating filter
expressions from sort-key bookmarks.
### The Path System (`lib/common/path.ts`, `lib/common/path-set.ts`)
@@ -127,17 +136,22 @@ query-based addressing.
Key design decisions:
- **Interned/flyweight** -- All `Path` instances are cached. `Path.parse()` is
the only way to create them; the constructor is protected. Two-generation LRU
cache rotated every 120 seconds.
- **Cached** -- `Path.parse()` caches instances in a two-generation LRU cache
rotated every 120 seconds. The constructor is public (used directly by the
parser and by methods like `slice()`, `concat()`, and `stripAlias()`).
- **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.
`PathSet` is a multi-indexed collection of paths supporting pattern-matching
queries. It indexes paths by length, by segment-at-position, and by string
representation. The `find()` method uses set intersection to efficiently match
wildcard patterns.
queries. It maintains separate `paramSegmentIndex` and `attrSegmentIndex` arrays
(one `Map<string, Set<Path>>` per position), plus a `stringIndex` map. The
`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`)
@@ -272,13 +286,17 @@ and CSV export.
`lib/ui/db.ts` translates between the UI's flat parameter representation and
MongoDB's nested document structure. The `flattenDevice()` function is the key
transformation: it recursively walks the nested device document and produces a
flat key-value map (e.g.,
`Device.WiFi.SSID.1.Name -> {value: [...], writable: true}`).
flat key-value map with colon-delimited attribute keys (e.g.,
`"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
hash-based revision tracking. The `getConfig()` function resolves config
expressions with dynamic context and backward compatibility for legacy config
keys.
hash-based revision tracking. The `getConfig()` function uses typed overloads --
it takes a typed default value (`string`, `number`, or `boolean`) and an
expression evaluation callback, returning a value of the same type as the
default.
### Database Layer (`lib/db/`)
+1 -7
View File
@@ -107,13 +107,7 @@ const packageDotJsonPlugin = {
const inlineDepsPlugin = {
name: "inlineDeps",
setup(build) {
const deps = [
"parsimmon",
"espresso-iisojs",
"@codemirror",
"mithril",
"yaml",
];
const deps = ["espresso-iisojs", "@codemirror", "mithril", "yaml"];
const depFiles = new Set();
build.onResolve({ filter: /^[^.]/ }, async (args) => {
if (args.pluginData === "inlineDeps") return undefined;
+1 -1
View File
@@ -23,7 +23,7 @@ export default defineConfig(
},
},
rules: {
"no-shadow": ["error", { allow: ["err"] }],
"@typescript-eslint/no-shadow": ["error", { allow: ["err"] }],
"handle-callback-err": "error",
"prefer-arrow-callback": "error",
"no-buffer-constructor": "error",
+65 -74
View File
@@ -31,8 +31,8 @@ import {
udpConnectionRequest,
xmppConnectionRequest,
} from "./connection-request.ts";
import { Expression, Task } from "./types.ts";
import { evaluate } from "./common/expression/util.ts";
import { Task } from "./types.ts";
import Expression, { Value } from "./common/expression.ts";
import { hashPassword } from "./auth.ts";
import { flattenDevice } from "./ui/db.ts";
import { ResourceLockedError } from "./common/errors.ts";
@@ -42,7 +42,7 @@ const XMPP_CONFIGURED = !!config.get("XMPP_JID");
export async function connectionRequest(
deviceId: string,
device?: Record<string, { value?: [boolean | number | string, string] }>,
device?: Record<string, Value>,
): Promise<string> {
if (!device) {
const res = await collections.devices.findOne({ _id: deviceId });
@@ -58,47 +58,36 @@ export async function connectionRequest(
password;
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
connectionRequestUrl = (device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
connectionRequestUrl =
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"] ||
"";
udpConnectionRequestAddress =
device[
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
] || {}
).value || [""])[0];
stunEnable = ((
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || {}
).value || [""])[0];
connReqJabberId = ((
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {}
).value || [""])[0];
username = ((
] || "";
stunEnable =
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || "";
connReqJabberId =
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || "";
username =
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
] || {}
).value || [""])[0];
password = ((
] || "";
password =
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
] || {}
).value || [""])[0];
] || "";
} else {
connectionRequestUrl = (device[
"Device.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
).value || [""])[0];
stunEnable = ((device["Device.ManagementServer.STUNEnable"] || {})
.value || [""])[0];
connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {})
.value || [""])[0];
username = ((
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
).value || [""])[0];
password = ((
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
).value || [""])[0];
connectionRequestUrl =
device["Device.ManagementServer.ConnectionRequestURL"] || "";
udpConnectionRequestAddress =
device["Device.ManagementServer.UDPConnectionRequestAddress"] || "";
stunEnable = device["Device.ManagementServer.STUNEnable"] || "";
connReqJabberId = device["Device.ManagementServer.ConnReqJabberID"] || "";
username =
device["Device.ManagementServer.ConnectionRequestUsername"] || "";
password =
device["Device.ManagementServer.ConnectionRequestPassword"] || "";
}
let remoteAddress;
try {
@@ -107,68 +96,70 @@ export async function connectionRequest(
return "Invalid connection request URL";
}
const evalCallback = (exp): Expression => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
let name = exp[1];
const snapshot = await getRevision();
const now = Date.now();
const evalCallback = (exp: Expression): Expression => {
if (exp instanceof Expression.Parameter) {
let name = exp.path.toString();
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress") return remoteAddress;
else if (name === "username") return username;
else if (name === "password") return password;
const p = device[name];
if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
else if (exp[1] === "USERNAME") return username;
else if (exp[1] === "PASSWORD") return password;
else if (name === "remoteAddress")
return new Expression.Literal(remoteAddress);
else if (name === "username") return new Expression.Literal(username);
else if (name === "password") return new Expression.Literal(password);
return new Expression.Literal(device[name] ?? null);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return new Expression.Literal(now);
else if (exp.name === "REMOTE_ADDRESS")
return new Expression.Literal(remoteAddress);
else if (exp.name === "USERNAME") return new Expression.Literal(username);
else if (exp.name === "PASSWORD") return new Expression.Literal(password);
}
return exp;
};
const snapshot = await getRevision();
const now = Date.now();
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
const configCallback = (exp: Expression): Expression.Literal => {
const e = evalCallback(exp);
if (e instanceof Expression.Literal) return e;
return new Expression.Literal(null);
};
const UDP_CONNECTION_REQUEST_PORT = getConfig(
snapshot,
"cwmp.udpConnectionRequestPort",
{},
now,
evalCallback,
0,
configCallback,
);
const CONNECTION_REQUEST_TIMEOUT = +getConfig(
const CONNECTION_REQUEST_TIMEOUT = getConfig(
snapshot,
"cwmp.connectionRequestTimeout",
{},
now,
evalCallback,
2000,
configCallback,
);
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = getConfig(
snapshot,
"cwmp.connectionRequestAllowBasicAuth",
{},
now,
evalCallback,
false,
configCallback,
);
let authExp: Expression = getConfigExpression(
snapshot,
"cwmp.connectionRequestAuth",
);
if (authExp === undefined) {
authExp = [
"FUNC",
"AUTH",
["PARAM", "username"],
["PARAM", "password"],
] as Expression;
if (!authExp) {
authExp = new Expression.FunctionCall("AUTH", [
new Expression.FunctionCall("USERNAME", []),
new Expression.FunctionCall("PASSWORD", []),
]);
}
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);
if (udpConnectionRequestAddress && +stunEnable) {
+23 -23
View File
@@ -1,5 +1,5 @@
import { PermissionSet, Expression } from "../types.ts";
import { evaluate, or } from "./expression/util.ts";
import { PermissionSet } from "../types.ts";
import Expression from "./expression.ts";
export default class Authorizer {
declare private permissionSets: PermissionSet[];
@@ -43,12 +43,12 @@ export default class Authorizer {
if (this.getFilterCache.has(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 perm of permissionSet) {
if (perm[resourceType]) {
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))
return this.validatorCache.get(resource);
const validators: Expression[] = [];
let validators: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
@@ -73,7 +73,7 @@ export default class Authorizer {
perm[resourceType].access >= 3 &&
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,
any: any,
): boolean => {
if (!validators.length) return false;
const object = {
mutationType,
mutation,
@@ -92,31 +90,33 @@ export default class Authorizer {
options: any,
};
const valueFunction = (paramName): any => {
const entry = paramName.split(".", 1)[0];
paramName = paramName.slice(entry.length + 1);
const now = Date.now();
const res = validators.evaluate((exp) => {
if (exp instanceof Expression.Literal) return exp;
if (exp instanceof Expression.Parameter) {
if (exp.path.colon) return new Expression.Literal(null);
const entry = exp.path.segments[0] as string;
const paramName = exp.path.slice(1);
let value = null;
if (["mutation", "options"].includes(entry)) {
value = object[entry];
for (const seg of paramName.split(".")) {
for (const seg of paramName.segments) {
if (value == null) break;
if (typeof value !== "object") value = null;
else value = value[seg];
else value = value[seg as string];
}
} else if (object[entry]) {
if (paramName) value = object[entry][paramName];
if (paramName.length) value = object[entry][paramName.toString()];
else value = object[entry];
}
return new Expression.Literal(value);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return new Expression.Literal(now);
}
return new Expression.Literal(null);
}).value;
return value;
};
const res = evaluate(
validators.length > 1 ? ["OR", validators] : validators[0],
valueFunction,
Date.now(),
);
return !Array.isArray(res) && !!res;
return !!res;
};
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;
}
+223 -225
View File
@@ -1,22 +1,15 @@
import * as BI from "./bigint.ts";
import { Expression } from "../../types.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);
import Expression from "../expression.ts";
import { reduce } from "./evaluate.ts";
class Indeterminates {
declare public map: Map<string, number>;
declare public sortedKeys: string[];
declare public map: Map<Expression, number>;
declare public sortedKeys: Expression[];
public constructor(str?: string) {
public constructor(exp?: Expression) {
this.map = new Map();
if (str) {
this.map.set(str, 1);
this.sortedKeys = [str];
if (exp) {
this.map.set(exp, 1);
this.sortedKeys = [exp];
} else {
this.sortedKeys = [];
}
@@ -37,27 +30,31 @@ class Indeterminates {
const res = new Indeterminates();
res.sortedKeys = indeterminates1.sortedKeys.slice();
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) {
const v = res.map.get(key);
if (!v) {
const k = strMap.get(key.toString());
if (!k) {
res.map.set(key, val);
res.sortedKeys.push(key);
} else {
const v2 = val + v;
const v2 = val + res.map.get(k);
if (!v2) {
res.map.delete(key);
res.sortedKeys = res.sortedKeys.filter((s) => s !== key);
res.map.delete(k);
res.sortedKeys = res.sortedKeys.filter((s) => s !== k);
} else {
res.map.set(key, v2);
res.map.set(k, v2);
}
}
}
res.sortedKeys.sort((a, b) => {
if (a.length !== b.length) return b.length - a.length;
else if (a > b) return 1;
else if (a < b) return -1;
const str1 = a.toString();
const str2 = b.toString();
if (str1.length !== str2.length) return str2.length - str1.length;
else if (str1 > str2) return 1;
else if (str1 < str2) return -1;
return 0;
});
@@ -73,10 +70,10 @@ class Indeterminates {
const k2 = b.sortedKeys[i];
const w2 = b.map.get(k2);
if (w1 !== w2) return w2 - w1;
if (k1.length > k2.length) return -1;
else if (k1.length < k2.length) return 1;
else if (k1 > k2) return 1;
else if (k1 < k2) return -1;
if (k1.toString().length > k2.toString().length) return -1;
else if (k1.toString().length < k2.toString().length) return 1;
else if (k1.toString() > k2.toString()) return 1;
else if (k1.toString() < k2.toString()) return -1;
}
return 0;
}
@@ -84,26 +81,35 @@ class Indeterminates {
interface Term {
indeterminates: Indeterminates;
coefficientNumerator: BI.bigint;
coefficientDenominator: BI.bigint;
coefficientNumerator: bigint;
coefficientDenominator: bigint;
}
function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
while (BI.ne(b, ZERO)) {
function findGcd(a: bigint, b: bigint): bigint {
while (b !== 0n) {
const t = b;
b = BI.rem(a, b);
b = a % b;
a = t;
}
return a;
}
class Polynomial {
class Polynomial extends Expression {
declare public terms: Term[];
public constructor(terms: Term[]) {
super();
this.terms = terms;
}
map(): Polynomial {
return this;
}
async mapAsync(): Promise<Polynomial> {
return this;
}
public static simplifyTerms(terms: Term[]): Term[] {
const ts = terms
.slice()
@@ -115,40 +121,37 @@ class Polynomial {
const t1 = ts[i - 1];
const t2 = ts[i];
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
const numerator = BI.add(
BI.mul(t1.coefficientNumerator, t2.coefficientDenominator),
BI.mul(t2.coefficientNumerator, t1.coefficientDenominator),
);
const numerator =
t1.coefficientNumerator * t2.coefficientDenominator +
t2.coefficientNumerator * t1.coefficientDenominator;
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const denominator =
t1.coefficientDenominator * t2.coefficientDenominator;
const gcd = findGcd(numerator, denominator);
ts[i] = {
indeterminates: t2.indeterminates,
coefficientNumerator: BI.div(numerator, gcd),
coefficientDenominator: BI.div(denominator, gcd),
coefficientNumerator: numerator / gcd,
coefficientDenominator: denominator / gcd,
};
ts[i - 1] = {
indeterminates: t1.indeterminates,
coefficientNumerator: ZERO,
coefficientNumerator: 0n,
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 {
const indeterminates = new Indeterminates(JSON.stringify(indeterminate));
const indeterminates = new Indeterminates(indeterminate);
const terms = [
{
indeterminates: indeterminates,
coefficientNumerator: ONE,
coefficientDenominator: ONE,
coefficientNumerator: 1n,
coefficientDenominator: 1n,
},
];
return new Polynomial(terms);
@@ -156,15 +159,12 @@ class Polynomial {
public static fromConstant(constant: number): Polynomial {
const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
let numerator = BI.BigInt("0b" + int);
if (constant < 0) numerator = BI.mul(numerator, NEGATIVE_ONE);
let denominator = ONE;
let numerator = BigInt("0b" + int);
if (constant < 0) numerator = numerator / -1n;
let denominator = 1n;
if (frac) {
denominator = BI.exp(TWO, BI.BigInt(frac.length));
numerator = BI.add(
BI.mul(numerator, denominator),
BI.BigInt("0b" + frac),
);
denominator = 2n ** BigInt(frac.length);
numerator = numerator * denominator + BigInt("0b" + frac);
}
const terms = [
@@ -181,7 +181,7 @@ class Polynomial {
public negation(): Polynomial {
const terms = this.terms.map((t) => ({
indeterminates: t.indeterminates,
coefficientNumerator: BI.mul(t.coefficientNumerator, NEGATIVE_ONE),
coefficientNumerator: t.coefficientNumerator * -1n,
coefficientDenominator: t.coefficientDenominator,
}));
@@ -217,14 +217,9 @@ class Polynomial {
for (const t1 of this.terms) {
for (const t2 of rhs.terms) {
const numerator = BI.mul(
t1.coefficientNumerator,
t2.coefficientNumerator,
);
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const numerator = t1.coefficientNumerator * t2.coefficientNumerator;
const denominator =
t1.coefficientDenominator * t2.coefficientDenominator;
const gcd = findGcd(numerator, denominator);
terms.push({
@@ -232,8 +227,8 @@ class Polynomial {
t1.indeterminates,
t2.indeterminates,
),
coefficientNumerator: BI.div(numerator, gcd),
coefficientDenominator: BI.div(denominator, gcd),
coefficientNumerator: numerator / gcd,
coefficientDenominator: denominator / gcd,
});
}
}
@@ -245,41 +240,49 @@ class Polynomial {
return this.multiply(rhs.reciprocal());
}
public toString(): string {
const add: string[] = [];
public toExpression(): Expression {
const add: Expression[] = [];
for (const t of this.terms) {
const coefficient =
BI.toNumber(t.coefficientNumerator) /
BI.toNumber(t.coefficientDenominator);
Number(t.coefficientNumerator) / Number(t.coefficientDenominator);
const mul: string[] = [];
const mul: Expression[] = [];
if (t.indeterminates.sortedKeys.length) {
for (const k of t.indeterminates.sortedKeys) {
const w = t.indeterminates.map.get(k);
for (let i = Math.abs(w); i > 0; --i) {
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(",")}]`);
else add.push(mul["0"]);
while (mul.length > 1) {
const r = mul.pop();
const l = mul.pop();
mul.push(new Expression.Binary("*", l, r));
}
add.push(mul[0]);
} else {
add.push(coefficient.toString());
add.push(new Expression.Literal(coefficient));
}
}
if (!add.length) return "0";
else if (add.length === 1) return add[0];
else return `["+",${add.join(",")}]`;
while (add.length > 1) {
const r = add.pop();
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 = {
"=": "=",
"<>": "<>",
@@ -289,156 +292,163 @@ const SWAPPED_OPS = {
"<=": ">=",
};
function normalizeCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
const op = exp[0];
if (op === "FUNC" && exp[1] === "COALESCE") {
const res: Expression[] = ["CASE"];
for (let i = 2; i < exp.length; ++i)
res.push(normalizeCallback(["IS NOT NULL", exp[i]]), exp[i]);
return normalizeCallback(res);
}
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];
}),
function cartesianProduct<T>(arrays: T[][]): T[][] {
return arrays.reduce<T[][]>(
(acc, cur) => acc.flatMap((a) => cur.map((item) => [...a, item])),
[[]],
);
if (!Array.isArray(w) && w) break;
}
res = res2;
}
}
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 (e == null) return null;
function toPolynomial(e: Expression): Polynomial {
if (e instanceof Polynomial) return e;
if (typeof e === "number") return Polynomial.fromConstant(e);
if (typeof e === "string")
return Polynomial.fromConstant(parseFloat(e) || 0);
if (typeof e === "boolean") return Polynomial.fromConstant(+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 {
if (exp instanceof Expression.FunctionCall) {
if (exp.name === "COALESCE") {
let e: Expression = new Expression.Literal(null);
for (let i = exp.args.length - 1; i >= 0; --i) {
e = new Expression.Conditional(
normalizeCallback(new Expression.Unary("IS NOT NULL", exp.args[i])),
exp.args[i],
e,
);
}
return normalizeCallback(e);
}
}
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);
if (exp instanceof Expression.Conditional) {
let e = exp;
if (e.condition instanceof Polynomial)
e = new Expression.Conditional(
e.condition.toExpression(),
e.then,
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 args.reduce(
(previousValue, currentValue) => previousValue.add(currentValue),
ADDITIVE_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 e;
}
return args.reduce(
(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);
const combs: [Expression, Expression][][] = [];
const callback: (e: Expression) => Expression = (e) => {
if (e instanceof Expression.Conditional) {
combs[combs.length - 1].push([e.condition, e.then]);
callback(e.otherwise);
} else {
combs[combs.length - 1].push([new Expression.Literal(true), e]);
}
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 e;
};
exp.map((e) => {
combs.push([]);
callback(e);
return e;
});
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((previousValue, currentValue) =>
previousValue.divide(currentValue),
) as unknown as Expression;
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
if (exp[1] == null || exp[2] == null) return null;
return res;
}
if (exp instanceof Expression.Binary) {
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 (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];
else if (typeof exp[2] === "number") rhs = Polynomial.fromConstant(exp[2]);
if (exp.left instanceof Polynomial) lhs = exp.left;
else if (exp.left instanceof Expression.Literal) {
if (exp.left.value == null) return exp.left;
else if (typeof exp.left.value === "number")
lhs = Polynomial.fromConstant(exp.left.value);
}
if (exp.right instanceof Polynomial) rhs = exp.right;
else if (exp.right instanceof Expression.Literal) {
if (exp.right.value == null) return exp.right;
else if (typeof exp.right.value === "number")
rhs = Polynomial.fromConstant(exp.right.value);
}
if (lhs || rhs) {
if (!lhs) lhs = Polynomial.fromIndeterminate(exp[1]);
if (!rhs) rhs = Polynomial.fromIndeterminate(exp[2]);
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) {
exp = [op, JSON.parse(lhs.toString()), JSON.parse(rhs.toString())];
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;
const n = lhs.terms[0].coefficientNumerator;
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([
{
@@ -463,30 +473,18 @@ function normalizeCallback(exp: Expression): Expression {
rhs = rhs.reciprocal();
}
if (flipOp < 0) exp = [SWAPPED_OPS[op], lhs, rhs];
else exp = [op, lhs, rhs];
if (flipOp > 0) exp = new Expression.Binary(exp.operator, lhs, rhs);
else exp = new Expression.Binary(SWAPPED_OPS[exp.operator], lhs, rhs);
}
}
}
// Restore polynomial expressions
exp = exp.map((e) =>
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
exp = evaluateCallback(exp);
exp = exp.map(fromPolynomial);
return exp;
}
export default function normalize(expr: Expression): Expression {
expr = map(expr, 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;
export default function normalize(exp: Expression): Expression {
return fromPolynomial(exp.evaluate(normalizeCallback));
}
+37 -25
View File
@@ -1,8 +1,8 @@
import { complement } from "espresso-iisojs";
import { Expression } from "../../types.ts";
import Expression from "../expression.ts";
import normalize from "./normalize.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>;
@@ -27,25 +27,35 @@ export function bookmarkToExpression(
): Expression {
return Object.entries(sort)
.reverse()
.reduce((cur, kv) => {
.reduce(
(cur, kv) => {
const [param, asc] = kv;
const p = new Expression.Parameter(Path.parse(param));
const b = new Expression.Literal(bookmark[param]);
if (asc < 0) {
if (bookmark[param] == null) {
return or(
["IS NOT NULL", ["PARAM", param]],
and(["IS NULL", ["PARAM", param]], cur),
return Expression.or(
new Expression.Unary("IS NOT NULL", p),
Expression.and(new Expression.Unary("IS NULL", p), cur),
);
}
let f = null;
f = or(f, [">", ["PARAM", param], bookmark[param]]);
return or(f, and(["=", ["PARAM", param], bookmark[param]], 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 = ["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));
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),
);
}
}, true as Expression);
},
new Expression.Literal(true) as Expression,
);
}
function getCover(
@@ -60,7 +70,7 @@ function getCover(
const nextCov: Minterm = [];
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));
cov.push((isNull << 2) ^ 2);
}
@@ -68,15 +78,15 @@ function getCover(
for (const m of minterm) {
const clause = context.getClause(m >>> 2);
if (clause instanceof Clause.IsNull) {
const lhs = clause.operand.expression();
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
if (lhs[1] !== param) continue;
if (!(clause.operand instanceof Clause.Exp)) continue;
if (!(clause.operand.exp instanceof Expression.Parameter)) continue;
if (clause.operand.exp.path.toString() !== param) continue;
nextCov.push(m);
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
} else if (clause instanceof Clause.Compare) {
const lhs = clause.lhs.expression();
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
if (lhs[1] !== param) continue;
if (!(clause.lhs instanceof Clause.Exp)) continue;
if (!(clause.lhs.exp instanceof Expression.Parameter)) continue;
if (clause.lhs.exp.path.toString() !== param) continue;
if (!(m & 1) && sort > 0) continue;
nextCov.push(m);
@@ -117,18 +127,20 @@ export function paginate(
sort: Record<string, number>,
): [Expression, Expression] {
fetched = normalize(fetched);
if (!fetched) return [false, toFetch];
if (fetched instanceof Expression.Literal && !fetched.value)
return [new Expression.Literal(false), 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 synth2 = Clause.fromExpression(toFetch);
const context = new SynthContext();
const expr1Minterms = synth1.true(context);
const expr2MintermsC = complement(synth2.true(context));
const expr1Minterms = synth1.getMinterms(context, 0b100);
const expr2MintermsC = synth2.getMinterms(context, 0b011);
const gaps = context.sanitizeMinterms(
complement([
+492 -394
View File
@@ -1,5 +1,166 @@
import parsimmon from "parsimmon";
import { Expression } from "../../types.ts";
import Path from "../path.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").
function interpretEscapes(str): string {
@@ -21,423 +182,330 @@ function interpretEscapes(str): string {
});
}
export function map<T>(exp: Expression, callback: (e: Expression) => T): T {
if (!Array.isArray(exp)) return callback(exp);
export function parseExpression(cursor: Cursor, presedence = 0): Expression {
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;
for (let i = 1; i < exp.length; ++i) {
const sub = map(exp[i], callback);
if (sub !== exp[i]) {
clone = clone || exp.slice();
clone[i] = sub;
cursor.skipwhitespace();
if (cursor.charCode !== CHAR_CLOSE_PAREN)
throw new Error("Expected ')'" + cursor.pos + " " + cursor.charCode);
cursor.step();
} else if (char === CHAR_DOUBLE_QUOTE) {
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();
const op = findOperator(cursor2);
const p = PRECEDENCE[op];
if (p <= presedence) return lhs;
if (op === "IS NULL") {
lhs = new Expression.Unary("IS NULL", lhs);
} else if (op === "IS NOT NULL") {
lhs = new Expression.Unary("IS NOT NULL", lhs);
} else if (BINARY_OPERATORS.includes(op)) {
lhs = new Expression.Binary(op, lhs, parseExpression(cursor2, p));
} else if (op) throw new Error("Unrecognized operator: " + op);
else break;
cursor.sync(cursor2);
}
return lhs;
}
export async function mapAsync(
exp: Expression,
callback: (e: Expression) => Promise<Expression>,
): Promise<Expression> {
if (!Array.isArray(exp)) return callback(exp);
let clone;
for (let i = 1; i < exp.length; ++i) {
const sub = await mapAsync(exp[i], callback);
if (sub !== exp[i]) {
clone = clone || exp.slice();
clone[i] = sub;
// Parse old-format alias value: unquoted, double-quoted, or single-quoted.
// Returns the parsed string value. Cursor is advanced past the value.
function parseOldAliasValue(cursor: Cursor): string {
cursor.walk((c) => c <= 32); // skip leading whitespace
const c = cursor.charCode;
if (c === CHAR_DOUBLE_QUOTE) {
// Double-quoted: use JSON.parse semantics (handles \", \n, \uXXXX etc.)
// Descend with no boundaries so commas/brackets inside quotes are not
// treated as terminators.
const start = cursor.pos;
cursor.step();
cursor.descend([]);
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();
}
// Parse old-format alias content: key:value,key:value,...
// Cursor should be positioned at the start of bracket content
// (after '[', with ']' set as boundary).
// Returns an Expression: Binary("AND", ...) chain of Binary("=", param, literal).
// Empty content returns Literal(true).
function parseOldAlias(cursor: Cursor): Expression {
cursor.skipwhitespace();
if (!cursor.charCode) return new Expression.Literal(true);
let result: Expression | null = null;
for (;;) {
cursor.skipwhitespace();
// Parse key as a path (supports nested aliases via parsePath).
// Add colon and comma as boundaries so the path stops there.
cursor.descend([CHAR_COLON, CHAR_COMMA], false);
const keyPath = parsePath(cursor);
cursor.ascend();
// After ascend, check for the colon separator
cursor.skipwhitespace();
if ((cursor.charCode as number) !== CHAR_COLON)
throw new Error("Expected ':'");
cursor.step();
// Parse value (reads until boundary , or ])
cursor.descend([CHAR_COMMA], false);
const value = parseOldAliasValue(cursor);
cursor.ascend();
const pair = new Expression.Binary(
"=",
new Expression.Parameter(keyPath),
new Expression.Literal(value),
);
result = result ? new Expression.Binary("AND", result, pair) : pair;
cursor.skipwhitespace();
if ((cursor.charCode as number) !== CHAR_COMMA) break;
cursor.step();
}
return result;
}
export function parsePath(cur: Cursor): Path {
const segments: (string | Expression)[] = [];
let colon = 0;
const cur2 = cur.fork();
for (;;) {
let char = cur2.charCode;
let exp: Expression;
if (char === CHAR_OPEN_BRACKET) {
if (cur2.pos !== cur.pos) throw new Error("Invalid path");
cur2.step();
cur2.descend([CHAR_CLOSE_BRACKET]);
// Try parsing as expression first
const cur3 = cur2.fork();
let err: unknown;
try {
exp = parseExpression(cur3);
} catch (e) {
err = e;
}
// Fall back to old alias format if not a valid expression or produced
// a bare Parameter (old format like [a:1] mis-parsed as a path).
if (err || exp instanceof Expression.Parameter) {
exp = parseOldAlias(cur2);
} else {
cur2.sync(cur3);
}
cur2.ascend();
char = cur2.charCode;
if (char !== CHAR_CLOSE_BRACKET) throw new Error("Expected ']'");
char = cur2.step().charCode;
}
return callback(clone || exp);
}
if (!PATH_CHARS.has(char)) {
if (cur2.pos <= cur.pos) throw new Error("Invalid path");
function binaryLeft(
operatorsParser: parsimmon.Parser<string>,
nextParser: parsimmon.Parser<Expression>,
): parsimmon.Parser<Expression> {
return parsimmon.seqMap(
nextParser,
parsimmon.seq(operatorsParser, nextParser).many(),
(first, rest) =>
rest.reduce((acc, ch) => {
const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
return [op, acc, another];
}, first),
);
}
const lang = parsimmon.createLanguage({
ComparisonOperator: function () {
return parsimmon
.alt(
parsimmon.string(">="),
parsimmon.string("<>"),
parsimmon.string("<="),
parsimmon.string("="),
parsimmon.string(">"),
parsimmon.string("<"),
)
.skip(parsimmon.optWhitespace);
},
LikeOperator: function () {
return parsimmon
.alt(
parsimmon.regexp(/like/i).result("LIKE").desc("LIKE"),
parsimmon
.regexp(/not\s+like/i)
.result("NOT LIKE")
.desc("NOT LIKE"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
IsNullOperator: function () {
return parsimmon
.alt(
parsimmon
.regexp(/is\s+null/i)
.result("IS NULL")
.desc("IS NULL"),
parsimmon
.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),
);
},
WhenPair: function (r) {
return parsimmon.seq(
parsimmon
.regexp(/when/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("WHEN")
.then(r.Expression),
parsimmon
.regexp(/then/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("THEN")
.then(r.Expression),
);
},
CaseStatement: function (r) {
return parsimmon.seqMap(
parsimmon
.regexp(/case/i)
.result("CASE")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("CASE"),
r.WhenPair.many(),
parsimmon
.regexp(/else/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("ELSE")
.then(r.Expression)
.map((e) => [[true, e]])
.fallback(null)
.skip(
parsimmon
.regex(/end/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)),
)
.skip(parsimmon.optWhitespace),
(...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);
if (char === CHAR_COLON) {
if (colon) throw new Error("Multiple colons");
colon = segments.length + 1;
}
return binaryLeft(
r.OrOperator,
binaryLeft(
r.AndOperator,
unary(r.NotOperator, r.Comparison.or(r.ValueExpression)),
),
).trim(parsimmon.optWhitespace);
},
});
if (exp) segments.push(exp);
else segments.push(cur.read(cur2));
export function parse(str: string): Expression {
if (!str) return null;
return lang.Expression.tryParse(str);
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[] {
if (!str) return [];
return lang.ExpressionList.tryParse(str);
}
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})`;
export function stringifyExpression(exp: Expression, level = 0): string {
function wrap(e: string, op: string): string {
if (PRECEDENCE[op] <= level) 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(
`${exp[1]}(${exp
.slice(2)
.map((e) => stringify(e))
.join(", ")})`,
`NOT ${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])}`,
"NOT",
);
} else if (op === "PARAM") {
if (typeof exp[1] === "string") {
return wrap(exp[1]);
} else if (Array.isArray(exp[1]) && exp[1][0] === "||") {
} else if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") {
return wrap(
exp[1]
.slice(1)
.map((p) => {
if (typeof p === "string") return p;
else return `{${stringify(p)}}`;
})
.join(""),
);
} else {
return wrap(`{${stringify(exp[1])}}`);
}
} 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],
)}`,
`${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])} ${exp.operator}`,
exp.operator,
);
}
} 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;
} else if (exp instanceof Expression.Binary) {
const op = exp.operator;
if (!(op in PRECEDENCE)) throw new Error("Invalid operator");
return wrap(
`${stringifyExpression(exp.left, PRECEDENCE[op] - 1)} ${exp.operator} ${stringifyExpression(exp.right, PRECEDENCE[op])}`,
op,
);
} else if (exp instanceof Expression.Parameter) {
return exp.path.toString();
} else if (exp instanceof Expression.FunctionCall) {
return `${exp.name}(${exp.args.map((a) => stringifyExpression(a)).join(", ")})`;
} else if (exp instanceof Expression.Conditional) {
let str = `CASE WHEN ${stringifyExpression(exp.condition)} THEN ${stringifyExpression(exp.then)}`;
if (
exp.otherwise instanceof Expression.Literal &&
exp.otherwise.value == null
)
str += " END";
else if (exp.otherwise instanceof Expression.Conditional)
str += stringifyExpression(exp.otherwise).slice(4);
else str += ` ELSE ${stringifyExpression(exp.otherwise)} END`;
return str;
}
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[] {
@@ -457,3 +525,33 @@ export function parseLikePattern(pat: string, esc: string): string[] {
}
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);
}
+291 -370
View File
@@ -1,7 +1,7 @@
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 { map, parseLikePattern } from "./parser.ts";
type Minterm = number[];
@@ -27,7 +27,7 @@ export abstract class SynthContextBase<
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[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canRaise(idx: number, set: Set<number>): boolean {
@@ -67,36 +67,34 @@ export abstract class SynthContextBase<
}
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
if (!Array.isArray(exp)) return;
const op = exp[0];
if (op === "IS NULL" || op === "IS NOT NULL") return;
if (op === "FUNC") {
if (exp[1] === "NOW") {
return;
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") {
yield* findIsNullDeps(exp[2]);
} else if (exp[1] === "ROUND") {
for (const e of exp.slice(2, 4)) yield* findIsNullDeps(e);
return;
if (exp instanceof Expression.Literal) return;
else if (exp instanceof Expression.Unary) {
if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") return;
yield* findIsNullDeps(exp.operand);
} else if (exp instanceof Expression.Binary) {
yield* findIsNullDeps(exp.left);
yield* findIsNullDeps(exp.right);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return;
else if (exp.name === "LOWER" || exp.name === "UPPER")
yield* findIsNullDeps(exp.args[0]);
else if (exp.name === "ROUND") {
for (const e of exp.args.slice(0, 2)) yield* findIsNullDeps(e);
}
} else if (op !== "PARAM") {
for (const e of exp.slice(1)) yield* findIsNullDeps(e);
return;
}
yield exp;
} else yield exp;
}
export abstract class Clause {
private _isNullable: Set<string>;
protected _expression: Expression;
abstract true(context: SynthContextBase<Clause>): Minterm[];
abstract false(context: SynthContextBase<Clause>): Minterm[];
abstract null(context: SynthContextBase<Clause>): Minterm[];
abstract getMinterms(
context: SynthContextBase<Clause>,
res: number,
): Minterm[];
expression(): Expression {
if (this._expression !== undefined) return this._expression;
const context = createSynthContext();
const minterms = this.true(context);
const minterms = this.getMinterms(context, 0b100);
const minimized = context.minimize(minterms);
this._expression = context.toExpression(minimized) as Expression;
return this._expression;
@@ -120,7 +118,7 @@ export abstract class Clause {
}
}
toString(): string {
return JSON.stringify(this.expression());
return this.expression().toString();
}
}
@@ -130,68 +128,11 @@ export namespace Clause {
constructor(public operand: Clause) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.false(context);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.true(context);
}
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]);
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
let r = res & 0b001;
if (res & 0b010) r |= 0b100;
if (res & 0b100) r |= 0b010;
return this.operand.getMinterms(context, r);
}
}
@@ -199,175 +140,43 @@ export namespace Clause {
constructor(public operands: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[][] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof And) {
operands.push(...s.operands);
continue;
}
const m = s.true(context);
if (!m.length) return [];
if (m.length === 1 && !m[0].length) continue;
minterms.push(m);
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
false(context: SynthContextBase<Clause>): Minterm[] {
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
if (!res) return [];
if (res === 0b111) return [[]];
if (res === 0b110)
return [
...this.getMinterms(context, 0b010),
...this.getMinterms(context, 0b100),
];
if (!(res & 0b010)) return complement(this.getMinterms(context, ~res));
const minterms: Minterm[] = [];
for (const s of this.operands) {
const m = s.false(context);
for (const o of this.operands) {
const m = o.getMinterms(context, res);
if (m.length === 1 && !m[0].length) return [[]];
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;
}
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 {
private _boolean: Clause;
constructor(public operand: Clause) {
super();
this._boolean = new Not(new Not(this));
}
true(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.null(context);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return complement(this.true(context));
}
null(): Minterm[] {
return [];
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
const minterms: Minterm[] = [];
if ((res & 0b110) === 0b110) return [[]];
if (res & 0b100)
minterms.push(...this.operand.getMinterms(context, 0b001));
if (res & 0b010)
minterms.push(...this.operand.getMinterms(context, 0b110));
return minterms;
}
isBoolean(): boolean {
return true;
@@ -378,7 +187,7 @@ export namespace Clause {
expression(): Expression {
const nullables = [...this.operand.getNullables()];
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();
}
}
@@ -387,26 +196,26 @@ export namespace Clause {
constructor(public exp: Expression) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, true);
if (this.exp) return [[]];
return [];
}
false(context: SynthContextBase<Clause>): Minterm[] {
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 [[]];
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
if (!(this.exp instanceof Expression.Literal))
return context.getMinterms(this, res);
if (this.exp.value == null) return res & 0b001 ? [[]] : [];
if (this.exp.value && res & 0b100) return [[]];
if (!this.exp.value && res & 0b010) return [[]];
return [];
}
expression(): Expression {
return this.exp;
}
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();
}
true(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, true);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, false);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.lhs.null(context);
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
if (!res) return [];
if (res === 0b111) return [[]];
if (res === 0b001 || res === 0b110)
return this.lhs.getMinterms(context, res);
return context.getMinterms(this, res);
}
*getNullables(): IterableIterator<IsNull> {
yield* this.lhs.getNullables();
@@ -434,7 +242,11 @@ export namespace Clause {
return true;
}
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();
let caseSensitive = true;
let contradiction = false;
if (Array.isArray(exp)) {
if (exp[0] === "FUNC") {
const func = exp[1];
if (func === "UPPER" || func === "LOWER") {
const p = func === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
if (exp instanceof Expression.FunctionCall) {
if (exp.name === "UPPER" || exp.name === "LOWER") {
const p =
exp.name === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
if (p === rhs) caseSensitive = false;
else contradiction = true;
lhs = new Exp(exp[2]);
}
lhs = new Exp(exp.args[0]);
}
}
this.lhs = lhs;
@@ -469,14 +279,13 @@ export namespace Clause {
this.caseSensitive = caseSensitive;
this.contradiction = contradiction;
}
true(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, true);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, false);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.lhs.null(context);
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
res = res & 0b111;
if (!res) return [];
if (res === 0b111) return [[]];
if (res === 0b001 || res === 0b110)
return this.lhs.getMinterms(context, res);
return context.getMinterms(this, res);
}
isBoolean(): boolean {
return true;
@@ -491,67 +300,157 @@ export namespace Clause {
let lhs = this.lhs.expression();
if (this.contradiction) {
if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "LOWER", lhs];
else lhs = ["FUNC", "UPPER", lhs];
lhs = new Expression.FunctionCall("LOWER", [lhs]);
else lhs = new Expression.FunctionCall("UPPER", [lhs]);
} else if (!this.caseSensitive) {
if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "UPPER", lhs];
else lhs = ["FUNC", "LOWER", lhs];
lhs = new Expression.FunctionCall("UPPER", [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 {
const res = map(exp, (e) => {
if (!Array.isArray(e)) return new Clause.Exp(e);
let op = e[0];
let negate = true;
let res: Clause;
let negate = false;
if (exp instanceof Expression.Unary) {
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";
else if (op === "IS NOT NULL") op = "IS NULL";
else if (op === "<>") op = "=";
else if (op === ">=") op = "<";
else if (op === "<=") op = ">";
else negate = false;
let clause: Clause;
if (op === "IS NULL") {
clause = new Clause.IsNull(e[1]);
} else if (op === "NOT") {
clause = new Clause.Not(e[1]);
if (op === "AND") {
res = new Clause.And([
fromExpression(exp.left),
fromExpression(exp.right),
]);
} else if (op === "OR") {
clause = new Clause.Or(e.slice(1));
} else if (op === "AND") {
clause = new Clause.And(e.slice(1));
} else if (op === "CASE") {
clause = new Clause.Case(e.slice(1));
} else if (op === "LIKE") {
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
const esc = e[3] instanceof Exp ? e[3].expression() : null;
if (typeof rhs === "string" && typeof esc === "string")
clause = new Clause.Like(e[1], rhs, esc);
else if (typeof rhs === "string" && esc == null)
clause = new Clause.Like(e[1], rhs);
} else if (op === ">" || op === "<" || op === "=") {
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
if (["boolean", "number", "string"].includes(typeof rhs)) {
clause = new Clause.Compare(
e[1],
op,
rhs as boolean | number | string,
negate = true;
res = new Clause.And([
new Clause.Not(fromExpression(exp.left)),
new Clause.Not(fromExpression(exp.right)),
]);
} else if (exp.right instanceof Expression.Literal) {
if (["=", ">", "<"].includes(op)) {
if (
["boolean", "number", "string"].includes(typeof exp.right.value)
) {
res = new Compare(
fromExpression(exp.left),
op as ">" | "<" | "=",
exp.right.value,
);
}
} else if (op === "LIKE") {
if (typeof exp.right.value === "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) {
const args = e.slice(1).map((a) => (a as Clause).expression());
clause = new Clause.Exp([op, ...args]);
}
if (negate) clause = new Clause.Not(clause);
return clause;
});
if (!res) res = new Exp(exp);
if (negate) res = new Not(res);
return res;
}
}
@@ -576,11 +475,24 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
super();
}
getMinterms(clause: Clause, res: true | false | null): number[][] {
getMinterms(clause: Clause, res: number): number[][] {
const v = this.getVar(clause);
if (res === true) return [[(v << 2) ^ 3]];
else if (res === false) return [[(v << 2) ^ 1]];
else return [[v << 2, (v << 2) ^ 2]];
switch (res & 0b111) {
case 0b100:
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[][] {
@@ -594,9 +506,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
) as Clause.Compare[];
// Comparisons
for (const [, clauses] of groupBy(comparisons, (c) =>
JSON.stringify(c.lhs),
)) {
for (const [, clauses] of groupBy(comparisons, (c) => c.lhs.toString())) {
const lhs = clauses[0].lhs;
const values = new Set(clauses.map((c) => c.rhs));
const valuesSorted = [...values].sort((a, b) => {
@@ -657,7 +567,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
(c) => c instanceof 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) {
const l1 = clauses[i1];
if (l1.contradiction) {
@@ -724,11 +634,15 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
}
if (clause instanceof Clause.IsNull) {
const str = clause.operand.toString();
if (str === '["PARAM","DeviceID.ID"]' || str === '["PARAM","_id"]')
if (clause.operand instanceof Clause.Exp) {
if (clause.operand.exp instanceof Expression.Parameter) {
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)));
}
@@ -808,11 +722,9 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
}
toExpression(sop: number[][]): Expression {
if (!sop.length) return false;
const res: Expression[] = [];
let res: Expression = new Expression.Literal(false);
for (const s of sop) {
if (!s.length) return true;
const conjs: Expression[] = [];
let conjs: Expression = new Expression.Literal(true);
for (const i of s) {
const clause = this.getClause(i >>> 2);
if (!clause) throw new Error("Invalid literal");
@@ -823,7 +735,8 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
const isNullVars = [...clause.getNullables()].map((c) =>
this.getVar(c),
);
conjs.push(
conjs = Expression.and(
conjs,
this.toExpression([
[i ^ 3],
...isNullVars.map((n) => [(n << 2) ^ 3]),
@@ -832,31 +745,38 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
continue;
}
let expr = clause.expression() as Expression;
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
if (
Array.isArray(expr) &&
expr[0] === "NOT" &&
Array.isArray(expr[1])
) {
const e: Expression[] = expr[1];
if (e[0] === "IS NULL") expr = ["IS NOT NULL", ...e.slice(1)];
else if (e[0] === "LIKE") expr = ["NOT LIKE", ...e.slice(1)];
else if (e[0] === "=") expr = ["<>", ...e.slice(1)];
else if (e[0] === "<>") expr = ["=", ...e.slice(1)];
else if (e[0] === ">") expr = ["<=", ...e.slice(1)];
else if (e[0] === ">=") expr = ["<", ...e.slice(1)];
else if (e[0] === "<") expr = [">=", ...e.slice(1)];
else if (e[0] === "<=") expr = [">", ...e.slice(1)];
else if (e[0] === "NOT") expr = e[1];
let expr = clause.expression();
if (!(i & 1) !== !(i & 2)) expr = new Expression.Unary("NOT", expr);
if (expr instanceof Expression.Unary && expr.operator === "NOT") {
const e = expr.operand;
if (e instanceof Expression.Unary) {
if (e.operator === "IS NULL")
expr = new Expression.Unary("IS NOT NULL", e.operand);
else if (e.operator === "IS NOT NULL")
expr = new Expression.Unary("IS NULL", e.operand);
else if (e.operator === "NOT") expr = e.operand;
} else if (e instanceof Expression.Binary) {
if (e.operator === "LIKE")
expr = new Expression.Binary("NOT LIKE", 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);
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);
}
if (conjs.length > 1) res.push(["AND", ...conjs]);
else res.push(conjs[0]);
conjs = Expression.and(conjs, expr);
}
if (res.length > 1) return ["OR", ...res];
return res[0];
res = Expression.or(res, conjs);
}
return res;
}
}
@@ -925,11 +845,12 @@ export function unionDiff(
): [Expression, Expression] {
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);
if (!expr1) {
if (expr1 instanceof Expression.Literal && !expr1.value) {
const e = synth2.expression();
return [e, e];
}
@@ -939,8 +860,8 @@ export function unionDiff(
const context = new SynthContext();
const expr2Minterms = synth2.true(context);
const expr1Minterms = synth1.true(context);
const expr2Minterms = synth2.getMinterms(context, 0b100);
const expr1Minterms = synth1.getMinterms(context, 0b100);
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
@@ -953,16 +874,16 @@ export function unionDiff(
export function covers(expr1: Expression, expr2: Expression): boolean {
expr2 = normalize(expr2);
if (!expr2) return true;
if (expr2 instanceof Expression.Literal && !expr2.value) return true;
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 synt2 = Clause.fromExpression(expr2);
const context = new SynthContext();
const expr1Minterms = synt1.true(context);
const expr2Minterms = synt2.true(context);
const expr1Minterms = synt1.getMinterms(context, 0b100);
const expr2Minterms = synt2.getMinterms(context, 0b100);
return tautology([
...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);
}
+67 -63
View File
@@ -1,40 +1,32 @@
import Path from "./path.ts";
export default class PathSet {
declare private lengthIndex: Set<Path>[];
declare private fragmentIndex: Map<string, Set<Path>>[];
declare private stringIndex: Map<string, Path>;
private paramSegmentIndex: Map<string, Set<Path>>[] = [];
private attrSegmentIndex: Map<string, Set<Path>>[] = [];
private stringIndex: Map<string, Path> = new Map();
public constructor() {
this.lengthIndex = [];
this.fragmentIndex = [];
this.stringIndex = new Map();
}
public constructor() {}
public get depth(): number {
return this.lengthIndex.length;
}
public add(path: Path): Path {
public add(pathStr: string): Path {
let path: Path = this.get(pathStr);
if (path) return path;
path = Path.parse(pathStr);
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);
while (this.lengthIndex.length <= path.length) {
this.lengthIndex.push(new Set());
// fragmentIndex is one less than lengthIndex
if (this.lengthIndex.length > 1) this.fragmentIndex.push(new Map());
}
while (this.paramSegmentIndex.length < path.paramLength)
this.paramSegmentIndex.push(new Map());
const lengthIndex = this.lengthIndex[path.length];
lengthIndex.add(path);
while (this.attrSegmentIndex.length < path.attrLength)
this.attrSegmentIndex.push(new Map());
for (let i = 0; i < path.length; ++i) {
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);
if (!fragmentIndexSet) {
@@ -48,63 +40,75 @@ export default class PathSet {
return path;
}
public get(path: Path): Path {
return this.stringIndex.get(path.toString()) || null;
public get(path: string): Path {
return this.stringIndex.get(path);
}
public find(
public findCompat(
path: Path,
superset = false,
subset = false,
depth: number = path.length,
depth = path.length,
): 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.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 p = this.get(path);
return p ? [p] : [];
const indexes: [Set<Path>, Set<Path>][] = [];
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);
if (!lengthIndex.length) return [];
indexes.sort((a, b) => a[0].size + a[1].size - (b[0].size + b[1].size));
let res;
for (let i = len - 1; i >= 0; --i) {
let fragmentIndexSet2;
const fragmentIndex = this.fragmentIndex[i];
let res: Path[];
if ((path.wildcard >> i) & 1) {
if (subset) continue;
} else if (superset) {
fragmentIndexSet2 = fragmentIndex.get("*");
}
if (!indexes.length) res = [...this.stringIndex.values()];
else res = [...indexes[0][0], ...indexes[0][1]];
const fragment = path.segments[i] as string;
const fragmentIndexSet1 = fragmentIndex.get(fragment);
const paramCover = ~paramLengthMask & paramMask;
const attrCover = ~attrLengthMask & attrMask;
if (!fragmentIndexSet1) {
if (!fragmentIndexSet2) return [];
if (!res) res = [...fragmentIndexSet2];
else res = res.filter((r) => fragmentIndexSet2.has(r));
} else if (!fragmentIndexSet2) {
if (!res) res = [...fragmentIndexSet1];
else res = res.filter((r) => fragmentIndexSet1.has(r));
} else {
if (!res) {
res = [...fragmentIndexSet1, ...fragmentIndexSet2];
} else {
res = res.filter(
(r) => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r),
(p) =>
(1 << p.paramLength) & paramCover && (1 << p.attrLength) & attrCover,
);
}
}
if (!res.length) return res;
}
if (!res) res = [].concat(...lengthIndex.map((a) => [...a]));
else res = res.filter((r) => lengthIndex.some((a) => a.has(r)));
for (let i = 1; i < indexes.length; ++i) {
const [idx1, idx2] = indexes[i];
res = res.filter((p) => idx1.has(p) || idx2.has(p));
}
return res;
}
+71 -166
View File
@@ -1,162 +1,31 @@
type Segments = (string | Alias)[];
type Alias = [Path, string][];
import Expression from "./expression.ts";
import { Cursor, parsePath } from "./expression/parser.ts";
type Segments = (string | Expression)[];
let cache1 = 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 {
declare public readonly segments: Segments;
declare public readonly colon: number;
declare public readonly wildcard: number;
declare public readonly alias: number;
declare protected _string: string;
declare protected _stringIndex: number[];
protected static parseAliasValue(
pattern: string,
index: number,
): { 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;
}
}
constructor(segments: Segments, colon: number) {
if (!(colon <= segments.length)) throw new Error("Invalid path");
if (segments.length > 32) throw new Error("Path too long");
Object.freeze(segments);
return { index, segments };
}
protected constructor(segments: Segments) {
let alias = 0;
let wildcard = 0;
const arr = segments.map((s, i) => {
if (Array.isArray(s)) {
if (s instanceof Expression) {
alias |= 1 << i;
const parts = s.map(
(al) => `${al[0].toString()}:${JSON.stringify(al[1])}`,
);
return `[${parts.join(",")}]`;
return `[${s.toString()}]`;
} else if (s === "*") {
wildcard |= 1 << i;
}
@@ -167,23 +36,27 @@ export default class Path {
const stringIndex = arr.map((s, i) => (offset += s.length) + i);
this.segments = segments;
this.colon = colon;
this.wildcard = wildcard;
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;
}
public static parse(str: string): Path {
let path = cache1.get(str);
public static parse(input: string): Path {
let path = cache1.get(input);
if (!path) {
path = cache2.get(str);
path = cache2.get(input);
if (!path) {
const { index, segments } = Path.parsePath(str, 0);
if (index < str.length) throw new Error("Invalid parameter path");
path = new Path(segments);
if (path.toString() !== str) cache1.set(path.toString(), path);
const cursor = new Cursor(input);
path = parsePath(cursor);
if (cursor.charCode) throw new Error("Unexpected character");
if (path.toString() !== input) cache1.set(path.toString(), path);
}
cache1.set(str, path);
cache1.set(input, path);
}
return path;
}
@@ -192,6 +65,14 @@ export default class Path {
return this.segments.length;
}
public get paramLength(): number {
return this.segments.length - this.colon;
}
public get attrLength(): number {
return this.colon;
}
public toString(): string {
return this._string;
}
@@ -200,25 +81,27 @@ export default class Path {
if (start < 0) start = Math.max(0, this.segments.length + start);
if (end < 0) end = Math.max(0, this.segments.length + end);
let str;
if (start >= end) {
str = "";
} else {
const i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
if (start >= end) return Path.root;
let i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
// Include the ":" when slicing exactly at the colon boundary
if (this.colon && start === this.segments.length - this.colon) --i1;
const i2 =
end <= this.segments.length
? this._stringIndex[end - 1]
: this._string.length;
str = this._string.slice(i1, i2);
}
const str = this._string.slice(i1, i2);
let path = cache1.get(str);
if (!path) {
path = cache2.get(str);
if (!path) {
const segments = this.segments.slice(start, end);
Object.freeze(segments);
path = new Path(segments);
const colon =
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);
}
@@ -230,15 +113,28 @@ export default class Path {
if (!path2._string) return this;
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);
if (!path) {
path = cache2.get(str);
if (!path) {
const segments = this.segments.concat(path2.segments);
Object.freeze(segments);
path = new Path(segments);
path = new Path(segments, colon);
}
cache1.set(str, path);
}
@@ -248,21 +144,30 @@ export default class Path {
public stripAlias(): Path {
if (!this.alias) return this;
const segments = this.segments.map((s) => (Array.isArray(s) ? "*" : s));
const str = segments.join(".");
const segments = this.segments.map((s) =>
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);
if (!path) {
path = cache2.get(str);
if (!path) {
Object.freeze(segments);
path = new Path(segments);
path = new Path(segments, this.colon);
}
cache1.set(str, path);
}
return path;
}
static root = new Path([], 0);
}
const interval = setInterval(() => {
+24 -21
View File
@@ -1,8 +1,7 @@
import * as crypto from "node:crypto";
import * as dgram from "node:dgram";
import * as http from "node:http";
import { evaluateAsync } from "./common/expression/util.ts";
import { Expression } from "./types.ts";
import Expression, { Value } from "./common/expression.ts";
import * as auth from "./auth.ts";
import * as extensions from "./extensions.ts";
import * as debug from "./debug.ts";
@@ -13,29 +12,33 @@ import * as logger from "../lib/logger.ts";
async function extractAuth(
exp: Expression,
dflt: any,
dflt: Value,
): Promise<[string, string, Expression]> {
let username, password;
const _exp = await evaluateAsync(
exp,
{},
0,
let username: string, password: string;
const _exp = await exp.evaluateAsync(
async (e: Expression): Promise<Expression> => {
if (!username && Array.isArray(e) && e[0] === "FUNC") {
if (e[1] === "EXT") {
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null;
if (e instanceof Expression.Parameter)
return new Expression.Literal(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++)
if (Array.isArray(e[i])) return null;
const { fault, value } = await extensions.run(e.slice(2));
return fault ? null : value;
} else if (e[1] === "AUTH") {
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) {
username = e[2] || "";
password = e[3] || "";
const { fault, value } = await extensions.run(args);
if (fault) return new Expression.Literal(null);
return new Expression.Literal(value);
} else if (e.name === "AUTH") {
if (e.args.every((a) => a instanceof Expression.Literal)) {
username = `${e.args[0].value ?? ""}`;
password = `${e.args[1].value ?? ""}`;
}
return new Expression.Literal(dflt);
}
return dflt;
}
}
return e;
+89 -83
View File
@@ -10,11 +10,7 @@ import * as config from "./config.ts";
import { generateDeviceId, once, setTimeoutPromise } from "./util.ts";
import * as soap from "./soap.ts";
import * as session from "./session.ts";
import {
evaluateAsync,
evaluate,
extractParams,
} from "./common/expression/util.ts";
import Expression, { Value } from "./common/expression.ts";
import * as cache from "./cache.ts";
import * as lock from "./lock.ts";
import * as localCache from "./cwmp/local-cache.ts";
@@ -39,7 +35,6 @@ import {
AcsRequest,
SessionFault,
Fault,
Expression,
SoapMessage,
InformRequest,
Preset,
@@ -80,7 +75,7 @@ async function authenticate(
sessionContext.cacheSnapshot,
"cwmp.auth",
);
if (authExpression == null) return true;
if (!authExpression) return true;
let authentication;
@@ -107,56 +102,65 @@ async function authenticate(
authentication["body"] = body;
}
const res = await evaluateAsync(
authExpression,
{},
sessionContext.timestamp,
const res = await authExpression.evaluateAsync(
async (e: Expression): Promise<Expression> => {
e = session.configContextCallback(sessionContext, e);
if (Array.isArray(e) && e[0] === "FUNC") {
if (e[1] === "EXT") {
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null;
if (e instanceof Expression.Parameter)
return new Expression.Literal(null);
for (let i = 4; i < e.length; i++)
if (Array.isArray(e[i])) return null;
if (e instanceof Expression.FunctionCall) {
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));
return fault ? null : value;
} else if (e[1] === "AUTH") {
const username = e[2];
const password = e[3];
const args = e.args.map((a) => a.value.toString());
if (typeof args[0] !== "string" || typeof args[1] !== "string")
return new Expression.Literal(null);
const { fault, value } = await extensions.run(args);
if (fault) return new Expression.Literal(null);
return new Expression.Literal(value);
} else if (e.name === "AUTH") {
if (e.args.every((a) => a instanceof Expression.Literal)) {
const username = e.args[0].value;
const password = e.args[1].value;
if (username != null && password != null && authentication) {
if (authentication["method"] === "Basic") {
return (
authentication["username"] === e[2] &&
authentication["password"] === e[3]
return new Expression.Literal(
authentication["username"] === username.toString() &&
authentication["password"] === password.toString(),
);
}
if (authentication["method"] === "Digest") {
const expected = auth.digest(
username,
username.toString(),
REALM,
password,
password.toString(),
authentication["nonce"],
"POST",
authentication["uri"],
authentication["qop"],
authentication["body"],
body,
authentication["cnonce"],
authentication["nc"],
);
return expected === authentication["response"];
return new Expression.Literal(
expected === authentication["response"],
);
}
}
return false;
}
return new Expression.Literal(false);
}
}
return e;
},
);
if (res && !Array.isArray(res)) return true;
if (res instanceof Expression.Literal) return !!res.value;
return false;
}
@@ -315,8 +319,7 @@ async function inform(
const cookiesPath = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.cookiesPath",
{},
sessionContext.timestamp,
"",
(e) => session.configContextCallback(sessionContext, e),
);
@@ -406,8 +409,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
const RETRY_DELAY = +localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.retryDelay",
{},
sessionContext.timestamp,
300,
(e) => session.configContextCallback(sessionContext, e),
);
@@ -434,14 +436,14 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
deviceData.attributes.revision = 1;
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 t = attrs?.value[1][0] as number;
if (t >= sessionContext.timestamp)
deviceEvents[p.segments[1] as string] = true;
}
const parameters: { [name: string]: Path } = {};
const parameters = new Set<string>();
const filteredPresets: Preset[] = [];
for (const preset of presets) {
@@ -470,29 +472,31 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
continue;
}
filteredPresets.push(preset);
for (const k of extractParams(
evaluate(preset.precondition, null, sessionContext.timestamp),
)) {
const pre = { ...preset };
const evalCallback = (e: Expression): Expression => {
if (e instanceof Expression.FunctionCall && e.name === "NOW")
return new Expression.Literal(sessionContext.timestamp);
if (e instanceof Expression.Parameter) {
// Mark channel in case of fault during fetching precondition
sessionContext.channels[preset.channel] = 0;
if (typeof k === "string") parameters[k] = Path.parse(k);
}
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);
}
}
parameters.add(e.path.toString());
}
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) => ({
path: v,
const declarations = [...parameters].map((v) => ({
path: Path.parse(v),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
@@ -520,22 +524,21 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
session.addProvisions(sessionContext, whiteList, whiteListProvisions);
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) {
if (
evaluate(p.precondition, {}, sessionContext.timestamp, (e) =>
session.configContextCallback(sessionContext, e),
)
) {
if (p.precondition.evaluate(evalCallback2).value) {
const provs = p.provisions.map((pp) => [
pp[0],
...pp
.slice(1)
.map((arg) =>
evaluate(arg, {}, sessionContext.timestamp, (e) =>
session.configContextCallback(sessionContext, e),
),
),
]) as [string, ...Expression[]][];
.map((arg) => (arg as Expression).evaluate(evalCallback2).value),
]) as [string, ...Value[]][];
if (blackList[p.channel] === 2) {
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> {
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
const cacheSnapshot = await localCache.getRevision();
const debugEnabled = !!localCache.getConfig(
const debugEnabled = localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: remoteAddress,
},
Date.now(),
false,
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return remoteAddress;
if (e instanceof Expression.FunctionCall) {
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;
},
);
@@ -1030,8 +1035,7 @@ async function processRequest(
sessionContext.debug = !!localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.debug",
{},
sessionContext.timestamp,
false,
(e) => session.configContextCallback(sessionContext, e),
);
@@ -1039,8 +1043,7 @@ async function processRequest(
sessionContext.timeout = +localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.sessionTimeout",
{},
sessionContext.timestamp,
30,
(e) => session.configContextCallback(sessionContext, e),
);
}
@@ -1282,16 +1285,19 @@ async function clientError(
deviceId = sessionContext.deviceId;
} else {
const cacheSnapshot = await localCache.getRevision();
debugEnabled = !!localCache.getConfig(
const remoteAddress = getRequestOrigin(httpRequest).remoteAddress;
debugEnabled = localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
{
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
},
Date.now(),
false,
(e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
return getRequestOrigin(httpRequest).remoteAddress;
if (e instanceof Expression.FunctionCall) {
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;
},
);
+21 -26
View File
@@ -7,7 +7,6 @@ import {
Task,
Operation,
} from "../types.ts";
import Path from "../common/path.ts";
import { collections } from "../db/db.ts";
import { optimizeProjection } from "../db/util.ts";
import * as MongoTypes from "../db/types.ts";
@@ -23,21 +22,17 @@ function compareAccessLists(list1: string[], list2: string[]): boolean {
export async function fetchDevice(
id: string,
timestamp: number,
): Promise<[Path, number, Attributes?][]> {
const res: [Path, number, Attributes?][] = [
): Promise<[string, number, Attributes?][]> {
const res: [string, number, Attributes?][] = [
["Events", timestamp, { object: [timestamp, 1], writable: [timestamp, 0] }],
[
Path.parse("Events"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
],
[
Path.parse("DeviceID"),
"DeviceID",
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
],
];
const device = await collections.devices.findOne({ _id: id });
const device = (await collections.devices.findOne({ _id: id })) as any;
if (!device) return null;
function storeParams(
@@ -77,7 +72,7 @@ export async function fetchDevice(
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
try {
res.push([Path.parse(path), t, attrs]);
res.push([path, t, attrs]);
} catch {
// The path parser is now more strict so we might be in a situation where
// the database contains invalid paths from before this change So here we
@@ -86,7 +81,7 @@ export async function fetchDevice(
splits[splits.length - 1] =
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
path = splits.join(".");
res.push([Path.parse(path), t, attrs]);
res.push([path, t, attrs]);
return;
}
@@ -98,17 +93,17 @@ export async function fetchDevice(
}
if (obj["_object"] && obj["_timestamp"])
res.push([Path.parse(path + ".*"), obj["_timestamp"]]);
res.push([path + ".*", obj["_timestamp"]]);
}
const ts: number = +device["_timestamp"] || 0;
if (ts) res.push([Path.parse("*"), ts]);
if (ts) res.push(["*", ts]);
for (const [k, v] of Object.entries(device)) {
switch (k) {
case "_lastInform":
res.push([
Path.parse("Events.Inform"),
"Events.Inform",
+v,
{
object: [+v, 0],
@@ -119,7 +114,7 @@ export async function fetchDevice(
break;
case "_lastBoot":
res.push([
Path.parse("Events.1_BOOT"),
"Events.1_BOOT",
+v,
{
object: [+v, 0],
@@ -130,7 +125,7 @@ export async function fetchDevice(
break;
case "_lastBootstrap":
res.push([
Path.parse("Events.0_BOOTSTRAP"),
"Events.0_BOOTSTRAP",
+v,
{
object: [+v, 0],
@@ -142,7 +137,7 @@ export async function fetchDevice(
case "_registered":
// Use current timestamp for registered event attribute timestamps
res.push([
Path.parse("Events.Registered"),
"Events.Registered",
timestamp,
{
object: [timestamp, 0],
@@ -153,7 +148,7 @@ export async function fetchDevice(
break;
case "_id":
res.push([
Path.parse("DeviceID.ID"),
"DeviceID.ID",
timestamp,
{
object: [timestamp, 0],
@@ -165,7 +160,7 @@ export async function fetchDevice(
case "_tags":
if ((v as string[]).length) {
res.push([
Path.parse("Tags"),
"Tags",
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
]);
@@ -173,7 +168,7 @@ export async function fetchDevice(
for (const t of v as string[]) {
res.push([
Path.parse("Tags." + encodeTag(t)),
"Tags." + encodeTag(t),
timestamp,
{
object: [timestamp, 0],
@@ -186,7 +181,7 @@ export async function fetchDevice(
case "_deviceId":
if (v["_Manufacturer"] != null) {
res.push([
Path.parse("DeviceID.Manufacturer"),
"DeviceID.Manufacturer",
timestamp,
{
object: [timestamp, 0],
@@ -198,7 +193,7 @@ export async function fetchDevice(
if (v["_OUI"] != null) {
res.push([
Path.parse("DeviceID.OUI"),
"DeviceID.OUI",
timestamp,
{
object: [timestamp, 0],
@@ -210,7 +205,7 @@ export async function fetchDevice(
if (v["_ProductClass"] != null) {
res.push([
Path.parse("DeviceID.ProductClass"),
"DeviceID.ProductClass",
timestamp,
{
object: [timestamp, 0],
@@ -222,7 +217,7 @@ export async function fetchDevice(
if (v["_SerialNumber"] != null) {
res.push([
Path.parse("DeviceID.SerialNumber"),
"DeviceID.SerialNumber",
timestamp,
{
object: [timestamp, 0],
@@ -257,7 +252,7 @@ export async function saveDevice(
)
continue;
const parent = deviceData.paths.get(diff[0].slice(0, -1));
const parent = deviceData.paths.get(diff[0].slice(0, -1).toString());
// Param timestamps may be greater than session timestamp to track revisions
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
+72 -56
View File
@@ -1,15 +1,12 @@
import * as vm from "node:vm";
import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts";
import { convertOldPrecondition } from "../db/util.ts";
import * as logger from "../logger.ts";
import * as scheduling from "../scheduling.ts";
import * as expression from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import Expression, { Value } from "../common/expression.ts";
import {
Preset,
Expression,
Provisions,
VirtualParameters,
Files,
@@ -93,10 +90,10 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
}
const events = preset["events"] || {};
let precondition = true as Expression;
let precondition: Expression = new Expression.Literal(true);
if (preset["precondition"]) {
try {
precondition = parse(preset["precondition"]);
precondition = Expression.parse(preset["precondition"]);
} catch {
precondition = convertOldPrecondition(
JSON.parse(preset["precondition"]),
@@ -104,7 +101,7 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
}
// Simplify expression
precondition = expression.evaluate(precondition);
precondition = precondition.evaluate((e) => e);
}
const _provisions: Preset["provisions"] = [];
@@ -113,23 +110,42 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
for (const c of preset.configurations) {
switch (c.type) {
case "age":
_provisions.push(["refresh", c.name, +c.age]);
_provisions.push([
"refresh",
new Expression.Literal(c.name),
new Expression.Literal(+c.age),
]);
break;
case "value":
_provisions.push(["value", c.name, c.value]);
_provisions.push([
"value",
new Expression.Literal(c.name),
new Expression.Literal(c.value),
]);
break;
case "add_tag":
_provisions.push(["tag", c.tag, true]);
_provisions.push([
"tag",
new Expression.Literal(c.tag),
new Expression.Literal(true),
]);
break;
case "delete_tag":
_provisions.push(["tag", c.tag, false]);
_provisions.push([
"tag",
new Expression.Literal(c.tag),
new Expression.Literal(false),
]);
break;
case "provision":
_provisions.push([c.name, ...(c.args || [])]);
_provisions.push([
c.name,
...(c.args || []).map((a) => Expression.parse(a)),
]);
break;
case "add_object":
@@ -139,11 +155,19 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 1]);
_provisions.push([
"instances",
new Expression.Literal(p),
new Expression.Literal(1),
]);
for (const k in obj) {
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
_provisions.push(["value", `${p}.${k}`, obj[k]]);
_provisions.push([
"value",
new Expression.Literal(`${p}.${k}`),
new Expression.Literal(obj[k]),
]);
}
}
}
@@ -157,7 +181,11 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 0]);
_provisions.push([
"instances",
new Expression.Literal(p),
new Expression.Literal(0),
]);
}
}
@@ -251,7 +279,7 @@ async function fetchConfig(): Promise<[string, Config]> {
for (const c of conf) {
// Evaluate expressions to simplify them
_config[c._id] = expression.evaluate(parse(c.value));
_config[c._id] = Expression.parse(c.value).evaluate((e) => e);
}
return [h, _config];
@@ -303,49 +331,37 @@ export function getFiles(revision: string): Files {
}
export function getConfig(
snapshotKey: string,
revision: string,
key: string,
context: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null {
const snapshot = localCache.get(snapshotKey);
dflt: string,
fn: (e: Expression) => Expression.Literal,
): string;
export function getConfig(
revision: string,
key: string,
dflt: number,
fn: (e: Expression) => Expression.Literal,
): number;
export function getConfig(
revision: string,
key: string,
dflt: boolean,
fn: (e: Expression) => Expression.Literal,
): boolean;
export function getConfig(
revision: string,
key: string,
dflt: Value,
fn: (e: Expression) => Expression.Literal,
): Value {
const snapshot = localCache.get(revision);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
};
if (!(key in snapshot.config)) {
if (key in oldOpts) {
let id;
if (context?.["id"]) {
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return config.get(oldOpts[key], id);
}
return null;
}
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
const e = snapshot.config[key];
if (!e) return dflt;
const v = e.evaluate(fn).value;
if (typeof v !== typeof dflt) return dflt;
return v;
}
export function getConfigExpression(
+56 -50
View File
@@ -1,7 +1,8 @@
import { Filter } from "mongodb";
import { EJSON } from "bson";
import { complement } from "espresso-iisojs";
import { parseLikePattern } from "../common/expression/parser.ts";
import { Expression } from "../types.ts";
import Expression from "../common/expression.ts";
import { decodeTag } from "../util.ts";
import {
SynthContextBase,
@@ -10,21 +11,14 @@ import {
Clause,
} from "../common/expression/synth.ts";
import normalize from "../common/expression/normalize.ts";
import * as BI from "../common/expression/bigint.ts";
type Minterm = number[];
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");
if (exp[0] !== "PARAM")
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];
const p = exp.path.toString();
if (collection === "devices") {
if (p === "DeviceID.ID") return "_id";
else if (p === "DeviceID") return "_deviceId";
@@ -117,12 +111,12 @@ function getTypes(parameter: string, collection: string): string[] {
function roundOid(oid: string, roundUp: boolean): string {
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;
if (oid.length > match.length) lastChar += oid.charCodeAt(match.length);
if (match.length < 24) lastChar -= 48;
if (lastChar > 0 && roundUp) num++;
num <<= BI.BigInt(4 * (24 - match.length));
num <<= BigInt(4 * (24 - match.length));
if (lastChar < 0 && !roundUp) --num;
const str = num.toString(16);
if (str.length > 24 || str.startsWith("-")) return null;
@@ -259,11 +253,12 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
super();
}
getMinterms(clause: Clause, res: true | false | null): number[][] {
const exp = clause.expression();
if (!Array.isArray(exp)) throw new Error("Invalid query expression");
getMinterms(clause: Clause, res: number): number[][] {
res = res & 0b111;
if (res & (res - 1)) return complement(this.getMinterms(clause, ~res));
if (res === null) {
const exp = clause.expression();
if (res === 0b001) {
const minterms: number[][] = [];
for (const dep of clause.getNullables()) {
const e = dep.operand.expression();
@@ -280,36 +275,42 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
return minterms;
}
if ([">", "<", "="].includes(exp[0])) {
const param = getParam(exp[1], this.collection);
if (!(exp instanceof Expression.Binary))
throw new Error("Invalid query expression");
let rhs = exp[2];
if (typeof rhs === "boolean") rhs = +rhs;
else if (typeof rhs !== "string" && typeof rhs !== "number")
const truthy = res === 0b100;
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`);
let rhs = exp.right.value;
if (typeof rhs === "boolean") rhs = +rhs;
if (
param.startsWith("Tags.") &&
this.collection === "devices" &&
exp[0] === "="
exp.operator === "="
) {
const t = decodeTag(param.slice(5));
const c = new MongoClauseArray("_tags", t);
if (typeof rhs === "string") rhs = 2;
if (exp[0] === "=") {
if ((rhs === 1) !== res) return [];
} else if (exp[0] === ">") {
if ((res && rhs >= 1) || (!res && rhs < 1)) return [];
} else if (exp[0] === "<") {
if ((res && rhs <= 1) || (!res && rhs > 1)) return [];
if (exp.operator === "=") {
if ((rhs === 1) !== truthy) return [];
} else if (exp.operator === ">") {
if ((truthy && rhs >= 1) || (!truthy && rhs < 1)) return [];
} else if (exp.operator === "<") {
if ((truthy && rhs <= 1) || (!truthy && rhs > 1)) return [];
}
return [[(this.getVar(c) << 1) ^ 1]];
}
let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte";
if (exp[0] === "=") op = "$eq";
else if (exp[0] === ">") op = res ? "$gt" : "$lte";
else if (exp[0] === "<") op = res ? "$lt" : "$gte";
if (exp.operator === "=") op = "$eq";
else if (exp.operator === ">") op = truthy ? "$gt" : "$lte";
else if (exp.operator === "<") op = truthy ? "$lt" : "$gte";
const possibleTypes = new Set(getTypes(param, this.collection));
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, ""));
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]);
} else if (exp[0] === "LIKE") {
const pat = exp[2];
if (typeof pat !== "string")
} else if (exp.operator === "LIKE") {
if (
!(
exp.right instanceof Expression.Literal &&
typeof exp.right.value === "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;
if (
Array.isArray(p) &&
p[0] === "FUNC" &&
["UPPER", "LOWER"].includes(p[1])
p instanceof Expression.FunctionCall &&
["UPPER", "LOWER"].includes(p.name)
) {
if (p[1] === "UPPER" && pat !== pat.toUpperCase())
return res ? [] : [[]];
if (p[1] === "LOWER" && pat !== pat.toLowerCase())
return res ? [] : [[]];
if (p.name === "UPPER" && pat !== pat.toUpperCase())
return truthy ? [] : [[]];
if (p.name === "LOWER" && pat !== pat.toLowerCase())
return truthy ? [] : [[]];
caseSensitive = false;
p = p[2];
p = p.args[0];
}
const param = getParam(p, this.collection);
const c = new MongoClauseLike(param, pat, exp[3], caseSensitive);
if (res) return [[(this.getVar(c) << 1) ^ 1]];
const typeClase = new MongoClauseType(param, "string");
const r = [[this.getVar(c) << 1, (this.getVar(typeClase) << 1) ^ 1]];
const c = new MongoClauseLike(param, pat, null, caseSensitive);
if (truthy) return [[(this.getVar(c) << 1) ^ 1]];
const typeClause = new MongoClauseCompare(param, "$gte", "", "string");
const r = [[this.getVar(c) << 1, (this.getVar(typeClause) << 1) ^ 1]];
return r;
} else {
throw new Error("Invalid query expression");
@@ -641,7 +647,7 @@ export function toMongoQuery(
exp = normalize(exp);
const clause = Clause.fromExpression(normalize(exp));
const context = new MongoSynthContext(resource);
const minterms = clause.true(context);
const minterms = clause.getMinterms(context, 0b100);
const minimized = context.minimize(minterms);
if (!minimized.length) return false;
return EJSON.deserialize(context.toQuery(minimized));
@@ -650,5 +656,5 @@ export function toMongoQuery(
export function validQuery(exp: Expression, resource: string): void {
const clause = Clause.fromExpression(normalize(exp));
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 { Expression, FaultStruct } from "../types.ts";
import { FaultStruct } from "../types.ts";
import { Value } from "../common/expression.ts";
export interface Fault {
_id: string;
@@ -71,7 +72,7 @@ interface TaskDeleteObject extends TaskBase {
interface TaskProvisions extends TaskBase {
name: "provisions";
provisions?: [string, ...Expression[]][];
provisions?: [string, ...Value[]][];
}
export type Task =
@@ -122,7 +123,7 @@ type Configuration =
| { type: "delete_tag"; tag: string }
| { type: "add_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 {
_id: string;
+100 -40
View File
@@ -1,4 +1,5 @@
import { Expression } from "../types.ts";
import Expression, { Value } from "../common/expression.ts";
import Path from "../common/path.ts";
import { encodeTag } from "../util.ts";
// Optimize projection by removing overlaps
@@ -26,44 +27,62 @@ export function optimizeProjection(obj: { [path: string]: 1 }): {
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
function recursive(_query): Expression {
const expressions: Expression[] = [];
let res: Expression = new Expression.Literal(true);
for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") {
if (k === "$and") {
const and: Expression = ["AND"];
for (const vv of Object.values(v)) and.push(recursive(vv));
expressions.push(and);
for (const vv of Object.values(v))
res = Expression.and(res, recursive(vv));
} else if (k === "$or") {
const or: Expression = ["OR"];
for (const vv of Object.values(v)) or.push(recursive(vv));
expressions.push(or);
let or: Expression = new Expression.Literal(false);
for (const vv of Object.values(v))
or = Expression.or(or, recursive(vv));
res = Expression.and(res, or);
} else {
throw new Error(`Operator ${k} not supported`);
}
} else if (k === "_tags") {
if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`);
const conjs: Expression[] = [];
for (const [op, val] of Object.entries(v)) {
if (op === "$ne") {
if (typeof v["$ne"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
res = Expression.and(
res,
new Expression.Unary(
"IS NULL",
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(val)}`),
),
),
);
} else if (op === "$eq") {
if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
res = Expression.and(
res,
new Expression.Unary(
"IS NOT NULL",
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(val)}`),
),
),
);
} else {
throw new Error(`Invalid tag query`);
}
}
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else {
expressions.push([
res = Expression.and(
res,
new Expression.Unary(
"IS NOT NULL",
["PARAM", `Tags.${encodeTag(v as string)}`],
]);
new Expression.Parameter(
Path.parse(`Tags.${encodeTag(v as string)}`),
),
),
);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
@@ -73,51 +92,92 @@ export function convertOldPrecondition(q: Record<string, unknown>): Expression {
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`);
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
res = Expression.and(
res,
new Expression.Unary(
exists ? "IS NOT NULL" : "IS NULL",
new Expression.Parameter(Path.parse(k)),
),
);
} else if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`);
const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) {
if (kk === "$eq") {
exps.push(["=", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$ne") {
exps.push([
"OR",
["<>", ["PARAM", k], vv],
["IS NULL", ["PARAM", k]],
]);
const p = new Expression.Parameter(Path.parse(k));
res = Expression.and(
res,
Expression.or(
new Expression.Binary("<>", p, new Expression.Literal(vv)),
new Expression.Unary("IS NULL", p),
),
);
} else if (kk === "$lt") {
exps.push(["<", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
"<",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$lte") {
exps.push(["<=", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
"<=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$gt") {
exps.push([">", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
">",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else if (kk === "$gte") {
exps.push([">=", ["PARAM", k], vv]);
res = Expression.and(
res,
new Expression.Binary(
">=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(vv),
),
);
} else {
throw new Error(`Operator ${kk} not supported`);
}
if (!["string", "number", "boolean"].includes(typeof vv))
throw new Error(`Invalid value for ${kk} operator`);
}
if (exps.length === 1) {
expressions.push(exps[0]);
} else if (exps.length > 1) {
const and: Expression = ["AND"];
expressions.push(and.concat(exps));
}
} else {
expressions.push(["=", ["PARAM", k], v]);
res = Expression.and(
res,
new Expression.Binary(
"=",
new Expression.Parameter(Path.parse(k)),
new Expression.Literal(v as Value),
),
);
}
}
if (expressions.length === 1) return expressions[0];
if (expressions.length === 0) return true;
return ["AND", ...expressions];
return res;
}
// empty filter
if (!Object.keys(q).length) return true;
if (!Object.keys(q).length) return new Expression.Literal(true);
return recursive(q);
}
+42 -13
View File
@@ -1,3 +1,4 @@
import Expression, { Value } from "./common/expression.ts";
import Path from "./common/path.ts";
import {
DeviceData,
@@ -84,9 +85,9 @@ export function getAliasDeclarations(
if (path.alias) {
for (const [i, alias] of path.segments.entries()) {
if (Array.isArray(alias)) {
if (alias instanceof Expression) {
const parent = stripped.slice(0, i + 1);
for (const [p] of alias as [Path, string][]) {
for (const [p] of expressionToAlias(alias)) {
decs = decs.concat(
getAliasDeclarations(parent.concat(p), timestamp, {
value: timestamp,
@@ -100,6 +101,22 @@ export function getAliasDeclarations(
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(
deviceData: DeviceData,
path: Path,
@@ -107,17 +124,19 @@ export function unpack(
): Path[] {
let allMatches = [] as Path[];
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);
} else {
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);
for (let i = path.length - 1; i >= 0; --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 unpacked = unpack(deviceData, p, revision);
const filtered: Path[] = [];
@@ -189,7 +208,7 @@ export function clear(
attributes.value = attributes.object;
}
for (const p of deviceData.paths.find(
for (const p of deviceData.paths.findCompat(
path,
true,
true,
@@ -252,12 +271,12 @@ function compareEquality(a, b): boolean {
export function set(
deviceData: DeviceData,
path: Path,
pathStr: string,
timestamp: number,
attributes: Attributes,
toClear?: Clear[],
): Clear[] {
path = deviceData.paths.add(path);
const path = deviceData.paths.add(pathStr);
const currentTimestamp = deviceData.timestamps.get(path);
@@ -312,7 +331,7 @@ export function set(
if (path.length > 1) {
toClear = set(
deviceData,
path.slice(0, path.length - 1),
path.slice(0, path.length - 1).toString(),
timestamp,
{ object: [timestamp, 1] },
toClear,
@@ -326,7 +345,12 @@ export function set(
deviceData.attributes.delete(path);
changeFlags |= 1;
} 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)) {
toClear = toClear || [];
toClear.push([p, timestamp]);
@@ -343,7 +367,12 @@ export function set(
toClear = toClear || [];
toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]);
} 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);
for (const k in tracker)
if (tracker[k] & changeFlags) deviceData.changes.add(k);
@@ -356,11 +385,11 @@ export function set(
export function track(
deviceData: DeviceData,
path: Path,
pathStr: string,
marker: string,
attributes?: string[],
): void {
path = deviceData.paths.add(path);
const path = deviceData.paths.add(pathStr);
let f = 1;
if (attributes)
+14 -5
View File
@@ -79,14 +79,23 @@ export async function getStatus(): Promise<Status> {
const users = getUsers(configSnapshot);
const ui = getUiConfig(configSnapshot);
return {
const status = {
users: !Object.keys(users).length,
presets: !presetCount,
filters: !Object.keys(ui["filters"]).length,
device: !Object.keys(ui["device"]).length,
index: !Object.keys(ui["index"]).length,
overview: !Object.keys(ui["overview"]).length,
filters: true,
device: true,
index: true,
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> {
+22 -20
View File
@@ -14,6 +14,7 @@ import { flattenDevice } from "./ui/db.ts";
import { getRequestOrigin } from "./forwarded.ts";
import { acquireLock, releaseLock } from "./lock.ts";
import { ResourceLockedError } from "./common/errors.ts";
import Expression from "./common/expression.ts";
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
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 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;
if (url.searchParams.has("timeout")) {
onlineThreshold = parseInt(url.searchParams.get("timeout"));
@@ -357,26 +376,9 @@ async function handler(
onlineThreshold = getConfig(
revision,
"cwmp.deviceOnlineThreshold",
{},
Date.now(),
(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;
4000,
configCallback,
);
}
let status = await apiFunctions.connectionRequest(deviceId, device);
+147 -184
View File
@@ -25,7 +25,6 @@ import {
TransferCompleteRequest,
Operation,
ScriptResult,
Expression,
GetParameterValues,
GetParameterAttributes,
GetParameterNames,
@@ -42,6 +41,7 @@ import {
import { getRequestOrigin } from "./forwarded.ts";
import * as logger from "./logger.ts";
import { encodeTag } from "./util.ts";
import Expression, { Value } from "./common/expression.ts";
const VALID_PARAM_TYPES = new Set([
"xsd:int",
@@ -102,29 +102,25 @@ function generateRpcId(sessionContext: SessionContext): string {
export function configContextCallback(
sessionContext: SessionContext,
exp: Expression,
): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
let name = exp[1];
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress")
return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
): Expression.Literal {
if (exp instanceof Expression.Literal) return exp;
else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW")
return new Expression.Literal(sessionContext.timestamp);
if (exp.name === "REMOTE_ADDRESS")
return new Expression.Literal(
getRequestOrigin(sessionContext.httpRequest).remoteAddress,
);
} else if (exp instanceof Expression.Parameter) {
const deviceData = sessionContext.deviceData;
const paths = deviceData.paths;
const path = paths.get(Path.parse(name));
const path = paths.get(exp.path.toString());
if (path) {
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(
@@ -132,9 +128,9 @@ export async function inform(
rpcReq: InformRequest,
): Promise<AcsResponse> {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
const params: [Path, number, Attributes][] = [
const params: [string, number, Attributes][] = [
[
Path.parse("DeviceID.Manufacturer"),
"DeviceID.Manufacturer",
timestamp,
{
object: [timestamp, 0],
@@ -144,7 +140,7 @@ export async function inform(
],
[
Path.parse("DeviceID.OUI"),
"DeviceID.OUI",
timestamp,
{
object: [timestamp, 0],
@@ -154,7 +150,7 @@ export async function inform(
],
[
Path.parse("DeviceID.ProductClass"),
"DeviceID.ProductClass",
timestamp,
{
object: [timestamp, 0],
@@ -164,7 +160,7 @@ export async function inform(
],
[
Path.parse("DeviceID.SerialNumber"),
"DeviceID.SerialNumber",
timestamp,
{
object: [timestamp, 0],
@@ -177,7 +173,7 @@ export async function inform(
for (const p of rpcReq.parameterList) {
const path = p[0];
params.push([
path,
path.toString(),
timestamp,
{
object: [timestamp, 0],
@@ -187,7 +183,7 @@ export async function inform(
}
params.push([
Path.parse("Events.Inform"),
"Events.Inform",
timestamp,
{
object: [timestamp, 0],
@@ -198,7 +194,7 @@ export async function inform(
for (const e of rpcReq.event) {
params.push([
Path.parse(`Events.${encodeTag(e.replace(/\s+/g, "_"))}`),
`Events.${encodeTag(e.replace(/\s+/g, "_"))}`,
timestamp,
{
object: [timestamp, 0],
@@ -210,7 +206,7 @@ export async function inform(
if (sessionContext.new) {
params.push([
Path.parse("DeviceID.ID"),
"DeviceID.ID",
timestamp,
{
object: [timestamp, 0],
@@ -219,7 +215,7 @@ export async function inform(
},
]);
params.push([
Path.parse("Events.Registered"),
"Events.Registered",
timestamp,
{
object: [timestamp, 0],
@@ -235,7 +231,7 @@ export async function inform(
let toClear = null;
for (const p of params) {
// 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]);
} else {
toClear = device.set(
@@ -301,69 +297,49 @@ export async function transferComplete(
let toClear = null;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastDownload`),
);
toClear = device.set(
sessionContext.deviceData,
p,
`Downloads.${instance}.LastDownload`,
timestamp,
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
toClear,
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastFileType`),
);
toClear = device.set(
sessionContext.deviceData,
p,
`Downloads.${instance}.LastFileType`,
timestamp,
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] },
toClear,
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastFileName`),
);
toClear = device.set(
sessionContext.deviceData,
p,
`Downloads.${instance}.LastFileName`,
timestamp,
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] },
toClear,
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastTargetFileName`),
);
toClear = device.set(
sessionContext.deviceData,
p,
`Downloads.${instance}.LastTargetFileName`,
timestamp,
{ value: [timestamp, [operation.args.targetFileName, "xsd:string"]] },
toClear,
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.StartTime`),
);
toClear = device.set(
sessionContext.deviceData,
p,
`Downloads.${instance}.StartTime`,
timestamp,
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
toClear,
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.CompleteTime`),
);
toClear = device.set(
sessionContext.deviceData,
p,
`Downloads.${instance}.CompleteTime`,
timestamp,
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
toClear,
@@ -387,21 +363,21 @@ function revertDownloadParameters(
): void {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.LastDownload`),
const lastDownloadPath = sessionContext.deviceData.paths.add(
`Downloads.${instance}.LastDownload`,
);
const lastDownload = sessionContext.deviceData.attributes.get(p);
const lastDownload =
sessionContext.deviceData.attributes.get(lastDownloadPath);
p = sessionContext.deviceData.paths.add(
Path.parse(`Downloads.${instance}.Download`),
);
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
const toClear = device.set(
sessionContext.deviceData,
`Downloads.${instance}.Download`,
timestamp,
{
value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
});
},
);
if (toClear) {
for (const c of toClear)
@@ -426,11 +402,10 @@ export async function timeoutOperations(
throw new Error(`Unknown operation name ${operation.name}`);
const DOWNLOAD_TIMEOUT =
+localCache.getConfig(
localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadTimeout",
{},
sessionContext.timestamp,
3600,
(e) => configContextCallback(sessionContext, e),
) * 1000;
@@ -443,11 +418,10 @@ export async function timeoutOperations(
commandKey: commandKey,
});
const SUCCESS_ON_TIMEOUT = +localCache.getConfig(
const SUCCESS_ON_TIMEOUT = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadSuccessOnTimeout",
{},
sessionContext.timestamp,
false,
(e) => configContextCallback(sessionContext, e),
);
@@ -486,16 +460,15 @@ export async function timeoutOperations(
export function addProvisions(
sessionContext: SessionContext,
channel: string,
provisions: [string, ...Expression[]][],
provisions: [string, ...Value[]][],
): void {
// Multiply by two because every iteration is two
// phases: read and update
const MAX_ITERATIONS =
+localCache.getConfig(
localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
{},
sessionContext.timestamp,
32,
(e) => configContextCallback(sessionContext, e),
) * 2;
@@ -549,8 +522,7 @@ export function clearProvisions(sessionContext: SessionContext): void {
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
{},
sessionContext.timestamp,
32,
(e) => configContextCallback(sessionContext, e),
) * 2;
@@ -848,22 +820,20 @@ function runDeclarations(
(path.alias | path.wildcard) & 1 ||
path.segments[0] === "VirtualParameters"
) {
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters"));
sessionContext.deviceData.paths.add("VirtualParameters");
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)) {
sessionContext.deviceData.paths.add(
Path.parse(`VirtualParameters.${k}`),
);
sessionContext.deviceData.paths.add(`VirtualParameters.${k}`);
}
}
}
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")
sessionContext.deviceData.paths.add(Path.parse("FactoryReset"));
sessionContext.deviceData.paths.add("FactoryReset");
if (path.alias) {
const aliasDecs = device.getAliasDeclarations(
@@ -871,12 +841,12 @@ function runDeclarations(
declaration.pathGet || 1,
);
for (const ad of aliasDecs) {
const p = sessionContext.deviceData.paths.add(ad.path);
const p = sessionContext.deviceData.paths.add(ad.path.toString());
allDeclareTimestamps.set(
p,
Math.max(ad.pathGet || 1, allDeclareTimestamps.get(p) || 0),
);
let attrTrackers;
let attrTrackers: string[];
if (ad.attrGet) {
attrTrackers = Object.keys(ad.attrGet);
mergeAttributeTimestamps(p, ad.attrGet);
@@ -884,7 +854,7 @@ function runDeclarations(
device.track(
sessionContext.deviceData,
p,
p.toString(),
"prerequisite",
attrTrackers,
);
@@ -900,14 +870,14 @@ function runDeclarations(
mergeAttributeTimestamps(u, declaration.attrGet);
}
} else {
path = sessionContext.deviceData.paths.add(path);
path = sessionContext.deviceData.paths.add(path.toString());
allDeclareTimestamps.set(
path,
Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(path) || 0),
);
if (declaration.attrGet)
mergeAttributeTimestamps(path, declaration.attrGet);
device.track(sessionContext.deviceData, path, "prerequisite");
device.track(sessionContext.deviceData, path.toString(), "prerequisite");
}
if (declaration.attrSet) {
@@ -928,7 +898,7 @@ function runDeclarations(
}
if (declaration.pathSet != null) {
let minInstances, maxInstances;
let minInstances: number, maxInstances: number;
if (Array.isArray(declaration.pathSet)) {
minInstances = declaration.pathSet[0];
maxInstances = declaration.pathSet[1];
@@ -938,17 +908,19 @@ function runDeclarations(
let parent = path.slice(0, -1);
let keys;
if (Array.isArray(path.segments[path.length - 1])) {
let keys: Record<string, string>;
if (path.segments[path.length - 1] instanceof Expression) {
keys = {};
for (const [p, v] of path.segments[path.length - 1])
keys[p.toString()] = v;
for (const [p, v] of device.expressionToAlias(
path.segments[path.length - 1] as Expression,
))
keys[p.toString()] = v as string;
} else if (path.segments[path.length - 1] === "*") {
keys = {};
}
if (!parent.wildcard && !parent.alias) {
parent = sessionContext.deviceData.paths.add(parent);
parent = sessionContext.deviceData.paths.add(parent.toString());
if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path);
@@ -1143,8 +1115,7 @@ export async function rpcRequest(
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations",
{},
sessionContext.timestamp,
32,
(e) => configContextCallback(sessionContext, e),
) * 2;
@@ -1184,7 +1155,7 @@ export async function rpcRequest(
);
const timestamp = sessionContext.timestamp + sessionContext.iteration;
let toClear;
let toClear: Clear[];
const allVirtualParameters = localCache.getVirtualParameters(
sessionContext.cacheSnapshot,
@@ -1198,7 +1169,7 @@ export async function rpcRequest(
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set(
sessionContext.deviceData,
declaration[0],
declaration[0].toString(),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] },
toClear,
@@ -1211,7 +1182,7 @@ export async function rpcRequest(
for (const k of Object.keys(allVirtualParameters)) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`VirtualParameters.${k}`),
`VirtualParameters.${k}`,
timestamp,
{
object: [timestamp, 0],
@@ -1221,7 +1192,7 @@ export async function rpcRequest(
}
toClear = device.set(
sessionContext.deviceData,
declaration[0],
declaration[0].toString(),
timestamp,
null,
toClear,
@@ -1235,7 +1206,7 @@ export async function rpcRequest(
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
toClear = device.set(
sessionContext.deviceData,
declaration[0],
declaration[0].toString(),
timestamp,
{ object: [timestamp, 0] },
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],
false,
true,
@@ -1290,7 +1261,7 @@ export async function rpcRequest(
return rpcRequest(sessionContext, null);
}
let toClear;
let toClear: Clear[];
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
// Update tags
@@ -1299,7 +1270,7 @@ export async function rpcRequest(
if (v && !c) {
toClear = device.set(
sessionContext.deviceData,
p,
p.toString(),
timestamp,
{
object: [timestamp, 0],
@@ -1311,7 +1282,7 @@ export async function rpcRequest(
} else if (c && !v) {
toClear = device.set(
sessionContext.deviceData,
p,
p.toString(),
timestamp,
null,
toClear,
@@ -1320,11 +1291,11 @@ export async function rpcRequest(
}
// Downloads
let index;
let index: number;
for (const instance of sessionContext.syncState.downloadsToCreate) {
if (index == null) {
index = 0;
for (const p of sessionContext.deviceData.paths.find(
for (const p of sessionContext.deviceData.paths.findCompat(
Path.parse("Downloads.*"),
false,
true,
@@ -1341,7 +1312,7 @@ export async function rpcRequest(
toClear = device.set(
sessionContext.deviceData,
Path.parse("Downloads"),
"Downloads",
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear,
@@ -1349,7 +1320,7 @@ export async function rpcRequest(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${index}`),
`Downloads.${index}`,
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear,
@@ -1383,7 +1354,7 @@ export async function rpcRequest(
for (const [k, v] of Object.entries(params)) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${index}.${k}`),
`Downloads.${index}.${k}`,
timestamp,
{
object: [timestamp, 0],
@@ -1399,7 +1370,7 @@ export async function rpcRequest(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${index}.*`),
`Downloads.${index}.*`,
timestamp,
null,
toClear,
@@ -1411,7 +1382,7 @@ export async function rpcRequest(
for (const instance of sessionContext.syncState.downloadsToDelete) {
toClear = device.set(
sessionContext.deviceData,
instance,
instance.toString(),
timestamp,
null,
toClear,
@@ -1432,7 +1403,7 @@ export async function rpcRequest(
if (val[0] !== attrs.value[1][0]) {
toClear = device.set(
sessionContext.deviceData,
p,
p.toString(),
timestamp,
{ value: [timestamp, val] },
toClear,
@@ -1504,7 +1475,7 @@ export async function rpcRequest(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`VirtualParameters.${vparams[i][0]}`),
`VirtualParameters.${vparams[i][0]}`,
timestamp,
vpu,
toClear,
@@ -1527,7 +1498,7 @@ function generateGetRpcRequest(
for (const path of syncState.refreshAttributes.exist) {
let found = false;
for (const p of sessionContext.deviceData.paths.find(
for (const p of sessionContext.deviceData.paths.findCompat(
path,
false,
true,
@@ -1547,7 +1518,9 @@ function generateGetRpcRequest(
}
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);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1557,7 +1530,7 @@ function generateGetRpcRequest(
for (const path of syncState.refreshAttributes.object) {
let found = false;
for (const p of sessionContext.deviceData.paths.find(
for (const p of sessionContext.deviceData.paths.findCompat(
path,
false,
true,
@@ -1577,7 +1550,9 @@ function generateGetRpcRequest(
}
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);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1586,7 +1561,7 @@ function generateGetRpcRequest(
syncState.refreshAttributes.object.clear();
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);
const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1597,8 +1572,7 @@ function generateGetRpcRequest(
const GPN_NEXT_LEVEL = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpnNextLevel",
{},
sessionContext.timestamp,
0,
(e) => configContextCallback(sessionContext, e),
) as number;
@@ -1609,11 +1583,10 @@ function generateGetRpcRequest(
// Skip root GPN workaround
if (path && !path.length) {
const SKIP_ROOT_GPN = !!localCache.getConfig(
const SKIP_ROOT_GPN = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.skipRootGpn",
{},
sessionContext.timestamp,
false,
(e) => configContextCallback(sessionContext, e),
);
@@ -1634,7 +1607,7 @@ function generateGetRpcRequest(
let est = 0;
if (path.length >= GPN_NEXT_LEVEL) {
const patterns: [Path, number][] = [[path, 0]];
for (const p of sessionContext.deviceData.paths.find(
for (const p of sessionContext.deviceData.paths.findCompat(
path,
true,
false,
@@ -1651,7 +1624,7 @@ function generateGetRpcRequest(
syncState.gpn.delete(path);
} else {
nextLevel = false;
for (const p of sessionContext.deviceData.paths.find(
for (const p of sessionContext.deviceData.paths.findCompat(
path,
false,
true,
@@ -1672,8 +1645,7 @@ function generateGetRpcRequest(
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
{},
sessionContext.timestamp,
32,
(e) => configContextCallback(sessionContext, e),
) as number;
@@ -1703,8 +1675,7 @@ function generateGetRpcRequest(
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
{},
sessionContext.timestamp,
32,
(e) => configContextCallback(sessionContext, e),
) as number;
@@ -1764,11 +1735,10 @@ function generateSetRpcRequest(
const deviceData = sessionContext.deviceData;
const SKIP_WRITABLE_CHECK = !!localCache.getConfig(
const SKIP_WRITABLE_CHECK = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.skipWritableCheck",
{},
sessionContext.timestamp,
false,
(e) => configContextCallback(sessionContext, e),
);
@@ -1810,24 +1780,21 @@ function generateSetRpcRequest(
const GPV_BATCH_SIZE = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.gpvBatchSize",
{},
sessionContext.timestamp,
32,
(e) => configContextCallback(sessionContext, e),
) as number;
const DATETIME_MILLISECONDS = !!localCache.getConfig(
const DATETIME_MILLISECONDS = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds",
{},
sessionContext.timestamp,
true,
(e) => configContextCallback(sessionContext, e),
);
const BOOLEAN_LITERAL = !!localCache.getConfig(
const BOOLEAN_LITERAL = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.booleanLiteral",
{},
sessionContext.timestamp,
true,
(e) => configContextCallback(sessionContext, e),
);
@@ -1904,15 +1871,13 @@ function generateSetRpcRequest(
const t2 = attrs?.value?.[1]?.[0] as number;
if (!(t <= t2)) {
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(
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(
deviceData.paths.get(
p.slice(0, -1).concat(Path.parse("TargetFileName")),
),
deviceData.paths.get(p.slice(0, -1).toString() + ".TargetFileName"),
);
return {
@@ -1928,7 +1893,7 @@ function generateSetRpcRequest(
// Reboot
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 t = attrs?.value?.[1][0] as number;
if (!(t >= syncState.reboot)) {
@@ -1942,7 +1907,7 @@ function generateSetRpcRequest(
syncState.factoryReset > 0 &&
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 t = attrs?.value?.[1][0] as number;
if (!(t >= syncState.factoryReset)) {
@@ -2064,8 +2029,7 @@ function processDeclarations(
const deviceData = sessionContext.deviceData;
const syncState = sessionContext.syncState;
const root = sessionContext.deviceData.paths.add(Path.parse(""));
const paths = deviceData.paths.find(root, false, true, 99);
const paths = deviceData.paths.findCompat(Path.root, false, true, 99);
paths.sort((a, b): number =>
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard,
);
@@ -2358,7 +2322,7 @@ function processDeclarations(
allDeclareAttributeTimestamps.size ||
allDeclareAttributeValues.size
)
func(root, 1, 0, paths);
func(Path.root, 1, 0, [Path.root, ...paths]);
return virtualParameterDeclarations;
}
@@ -2372,7 +2336,7 @@ function processInstances(
maxInstances: number,
defer: boolean,
): void {
parent = sessionContext.deviceData.paths.add(parent);
parent = sessionContext.deviceData.paths.add(parent.toString());
let instancesToCreate: InstanceSet, instancesToDelete: Set<Path>;
if (parent.segments[0] === "Downloads") {
if (parent.length !== 1) return;
@@ -2491,19 +2455,17 @@ export async function rpcResponse(
if (!parameterList.length) {
sessionContext.rpcRequest = null;
} else {
const DATETIME_MILLISECONDS = !!localCache.getConfig(
const DATETIME_MILLISECONDS = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds",
{},
sessionContext.timestamp,
true,
(e) => configContextCallback(sessionContext, e),
);
const BOOLEAN_LITERAL = !!localCache.getConfig(
const BOOLEAN_LITERAL = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.booleanLiteral",
{},
sessionContext.timestamp,
true,
(e) => configContextCallback(sessionContext, e),
);
@@ -2543,7 +2505,7 @@ export async function rpcResponse(
}
toClear = device.set(
sessionContext.deviceData,
path,
path.toString(),
timestamp,
{
object: [timestamp, 0],
@@ -2562,7 +2524,7 @@ export async function rpcResponse(
});
toClear = device.set(
sessionContext.deviceData,
Path.parse(p),
p,
timestamp,
{
object: [timestamp, 0],
@@ -2589,7 +2551,7 @@ export async function rpcResponse(
}
toClear = device.set(
sessionContext.deviceData,
path,
path.toString(),
timestamp,
{
notification: [timestamp, notification],
@@ -2608,7 +2570,7 @@ export async function rpcResponse(
});
toClear = device.set(
sessionContext.deviceData,
Path.parse(p),
p,
timestamp,
{
notification: [timestamp, 0],
@@ -2623,7 +2585,8 @@ export async function rpcResponse(
return invalidResponse("Response name does not match request name");
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));
else root = Path.parse(rpcReq.parameterPath);
@@ -2667,7 +2630,7 @@ export async function rpcResponse(
"VirtualParameters",
"Downloads",
]) {
const p = sessionContext.deviceData.paths.get(Path.parse(n));
const p = sessionContext.deviceData.paths.get(n);
if (p && sessionContext.deviceData.attributes.has(p))
sessionContext.deviceData.timestamps.set(p, timestamp);
}
@@ -2693,7 +2656,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
path,
path.toString(),
timestamp,
{
object: [timestamp, object ? 1 : 0],
@@ -2706,7 +2669,7 @@ export async function rpcResponse(
for (const path of wildcardParams) {
toClear = device.set(
sessionContext.deviceData,
path,
path.toString(),
timestamp,
null,
toClear,
@@ -2719,7 +2682,7 @@ export async function rpcResponse(
for (const p of rpcReq.parameterList) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(p[0]),
p[0],
timestamp + 1,
{
object: [timestamp + 1, 0],
@@ -2755,7 +2718,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(p[0]),
p[0],
timestamp + 1,
attrs,
toClear,
@@ -2767,7 +2730,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(rpcReq.objectName + rpcRes.instanceNumber),
rpcReq.objectName + rpcRes.instanceNumber,
timestamp + 1,
{ object: [timestamp + 1, 1] },
toClear,
@@ -2778,7 +2741,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(rpcReq.objectName.slice(0, -1)),
rpcReq.objectName.slice(0, -1),
timestamp + 1,
null,
toClear,
@@ -2789,7 +2752,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse("Reboot"),
"Reboot",
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear,
@@ -2800,7 +2763,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse("FactoryReset"),
"FactoryReset",
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear,
@@ -2811,7 +2774,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.Download`),
`Downloads.${rpcReq.instance}.Download`,
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear,
@@ -2820,7 +2783,7 @@ export async function rpcResponse(
if (rpcRes.status === 0) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastDownload`),
`Downloads.${rpcReq.instance}.LastDownload`,
timestamp + 1,
{
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]],
@@ -2830,7 +2793,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastFileType`),
`Downloads.${rpcReq.instance}.LastFileType`,
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear,
@@ -2838,7 +2801,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastFileName`),
`Downloads.${rpcReq.instance}.LastFileName`,
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear,
@@ -2846,7 +2809,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.LastTargetFileName`),
`Downloads.${rpcReq.instance}.LastTargetFileName`,
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear,
@@ -2854,7 +2817,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.StartTime`),
`Downloads.${rpcReq.instance}.StartTime`,
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
toClear,
@@ -2862,7 +2825,7 @@ export async function rpcResponse(
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Downloads.${rpcReq.instance}.CompleteTime`),
`Downloads.${rpcReq.instance}.CompleteTime`,
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
toClear,
@@ -2977,7 +2940,7 @@ export async function deserialize(
const deviceData = initDeviceData();
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]);
@@ -2999,8 +2962,8 @@ export async function serialize(
): Promise<string> {
const deviceData = [];
for (const path of sessionContext.deviceData.paths.find(
Path.parse(""),
for (const path of sessionContext.deviceData.paths.findCompat(
Path.root,
false,
false,
99,
+5 -15
View File
@@ -4,8 +4,7 @@ import Path from "./common/path.ts";
import PathSet from "./common/path-set.ts";
import VersionedMap from "./versioned-map.ts";
import InstanceSet from "./instance-set.ts";
export type Expression = string | number | boolean | null | any[];
import Expression, { Value } from "./common/expression.ts";
export interface Fault {
code: string;
@@ -160,7 +159,7 @@ export interface Task {
fileName?: string;
targetFileName?: string;
expiry?: number;
provisions?: [string, ...Expression[]][];
provisions?: [string, ...Value[]][];
}
export interface Operation {
@@ -470,7 +469,7 @@ export interface Permissions {
[resource: string]: {
access: number;
filter: Expression;
validate?: Expression;
validate: Expression;
};
};
};
@@ -479,7 +478,7 @@ export interface Permissions {
export type PermissionSet = {
[resource: string]: {
access: number;
validate?: Expression;
validate: Expression;
filter: Expression;
};
}[];
@@ -488,16 +487,7 @@ export interface Config {
[name: string]: Expression;
}
export interface UiConfig {
filters: Record<string, unknown>;
device: Record<string, unknown>;
index: Record<string, unknown>;
overview: {
charts?: Record<string, unknown>;
groups?: Record<string, unknown>;
};
pageSize?: Expression;
}
export type UiConfig = Record<string, string>;
export interface SoapMessage {
id: string;
+12 -5
View File
@@ -214,8 +214,17 @@ router.get("/", async (ctx) => {
// be problematic when using relatives asset paths in HTML
if (ctx.path.endsWith("//")) return;
const permissionSets: PermissionSet[] =
ctx.state.authorizer.getPermissionSets();
const ps: PermissionSet[] = 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 = "";
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
@@ -231,9 +240,7 @@ router.get("/", async (ctx) => {
<body class="h-full bg-stone-100">
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
<script>
window.clientConfig = ${JSON.stringify({
ui: localCache.getUiConfig(ctx.state.configSnapshot),
})};
window.clientConfig = ${JSON.stringify(localCache.getUiConfig(ctx.state.configSnapshot))};
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
window.genieacsVersion = ${JSON.stringify(VERSION)};
window.username = ${JSON.stringify(
+197 -125
View File
@@ -3,11 +3,11 @@ import Router from "@koa/router";
import { ObjectId } from "mongodb";
import * as db from "./db.ts";
import * as apiFunctions from "../api-functions.ts";
import { evaluate, and, extractParams } from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import Expression, { extractPaths } from "../common/expression.ts";
import Path from "../common/path.ts";
import * as logger from "../logger.ts";
import { getConfig } from "../ui/local-cache.ts";
import { Expression, Task } from "../types.ts";
import { Task } from "../types.ts";
import { generateSalt, hashPassword } from "../auth.ts";
import { del } from "../cache.ts";
import Authorizer from "../common/authorizer.ts";
@@ -67,11 +67,14 @@ router.get(`/devices/:id.csv`, async (ctx) => {
id: ctx.params.id,
};
const filter = and(authorizer.getFilter("devices", 2), [
const filter = Expression.and(
authorizer.getFilter("devices", 2),
new Expression.Binary(
"=",
["PARAM", RESOURCE_IDS.devices],
ctx.params.id,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS.devices)),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log);
@@ -89,36 +92,51 @@ router.get(`/devices/:id.csv`, async (ctx) => {
);
const lines: string[] = [
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp",
"Parameter,Object,Writable,Value,Value type,Timestamp,Value timestamp,Notification,Access list,Attributes timestamp",
];
for (const k of Object.keys(device).sort()) {
const p = device[k];
let value = "";
let type = "";
if (p.value) {
value = p.value[0];
type = p.value[1];
if (type === "xsd:dateTime" && typeof value === "number")
const keys = Object.keys(device).sort();
let prevParam = "";
let attrs: Record<string, string | number | boolean | null> = {};
function flushRow(): void {
if (!prevParam) return;
let value: string | number | boolean | null = attrs["value"] ?? "";
if (attrs["type"] === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toJSON();
}
const row = [
k,
p.object,
new Date(p.objectTimestamp).toJSON(),
p.writable,
new Date(p.writableTimestamp).toJSON(),
prevParam,
attrs["object"] ?? "",
attrs["writable"] ?? "",
`"${String(value).replace(/"/g, '""')}"`,
type,
new Date(p.valueTimestamp).toJSON(),
p.notification,
new Date(p.notificationTimestamp).toJSON(),
p.accessList ? p.accessList.join(", ") : "",
new Date(p.accessListTimestamp).toJSON(),
attrs["type"] ?? "",
attrs["timestamp"] != null ? new Date(+attrs["timestamp"]).toJSON() : "",
attrs["valueTimestamp"] != null
? new Date(+attrs["valueTimestamp"]).toJSON()
: "",
attrs["notification"] ?? "",
attrs["accessList"] ?? "",
attrs["attributesTimestamp"] != null
? new Date(+attrs["attributesTimestamp"]).toJSON()
: "",
];
lines.push(row.map((r) => (r != null ? r : "")).join(","));
}
for (const k of keys) {
const colonIdx = k.lastIndexOf(":");
const param = colonIdx === -1 ? k : k.slice(0, colonIdx);
const attr = colonIdx === -1 ? "value" : k.slice(colonIdx + 1);
if (param !== prevParam) {
flushRow();
prevParam = param;
attrs = {};
}
attrs[attr] = device[k];
}
flushRow();
ctx.body = lines.join("\n");
logger.accessInfo(log);
});
@@ -128,7 +146,10 @@ for (const [resource, flags] of Object.entries(resources)) {
const authorizer: Authorizer = ctx.state.authorizer;
let filter: Expression = authorizer.getFilter(resource, 1);
if (ctx.request.query.filter)
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
filter = Expression.and(
filter,
Expression.parse(singleParam(ctx.request.query.filter)),
);
const log = {
message: `Count ${resource}`,
@@ -144,11 +165,18 @@ for (const [resource, flags] of Object.entries(resources)) {
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
filter = and(filter, [
"OR",
[">=", ["PARAM", "expiry"], Date.now() + 60000],
["IS NULL", ["PARAM", "expiry"]],
]);
const p = new Expression.Parameter(Path.parse("expiry"));
filter = Expression.and(
filter,
Expression.or(
new Expression.Binary(
">=",
p,
new Expression.Literal(Date.now() + 60000),
),
new Expression.Unary("IS NULL", p),
),
);
}
const count = await db.count(resource, filter);
@@ -165,7 +193,10 @@ for (const [resource, flags] of Object.entries(resources)) {
const options: Parameters<typeof db.query>[2] = {};
let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter)
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
filter = Expression.and(
filter,
Expression.parse(singleParam(ctx.request.query.filter)),
);
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort)
@@ -193,11 +224,18 @@ for (const [resource, flags] of Object.entries(resources)) {
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
filter = and(filter, [
"OR",
[">=", ["PARAM", "expiry"], Date.now() + 60000],
["IS NULL", ["PARAM", "expiry"]],
]);
const p = new Expression.Parameter(Path.parse("expiry"));
filter = Expression.and(
filter,
Expression.or(
new Expression.Binary(
">=",
p,
new Expression.Literal(Date.now() + 60000),
),
new Expression.Unary("IS NULL", p),
),
);
}
logger.accessInfo(log);
@@ -219,7 +257,10 @@ for (const [resource, flags] of Object.entries(resources)) {
const options: Parameters<typeof db.query>[2] = { projection: {} };
let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter)
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
filter = Expression.and(
filter,
Expression.parse(singleParam(ctx.request.query.filter)),
);
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort)
@@ -239,25 +280,38 @@ for (const [resource, flags] of Object.entries(resources)) {
return void (ctx.status = 403);
}
const columns: Record<string, Expression> = JSON.parse(
const columnsStr: Record<string, string> = JSON.parse(
singleParam(ctx.request.query.columns),
);
const now = Date.now();
for (const [k, v] of Object.entries(columns)) {
const e = evaluate(parse(v as string), null, now);
columns[k] = e;
for (const p of extractParams(e))
if (typeof p === "string") options.projection[p] = 1;
}
const now = Date.now();
const columns: Record<string, Expression> = Object.fromEntries(
Object.entries(columnsStr).map(([k, v]) => {
let exp = Expression.parse(v);
exp = exp.evaluate((e) => {
if (e instanceof Expression.FunctionCall && e.name === "NOW")
return new Expression.Literal(now);
return e;
});
for (const p of extractPaths(exp)) options.projection[p.toString()] = 1;
return [k, exp];
}),
);
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
filter = and(filter, [
"OR",
[">=", ["PARAM", "expiry"], Date.now() + 60000],
["IS NULL", ["PARAM", "expiry"]],
]);
const p = new Expression.Parameter(Path.parse("expiry"));
filter = Expression.and(
filter,
Expression.or(
new Expression.Binary(
">=",
p,
new Expression.Literal(Date.now() + 60000),
),
new Expression.Unary("IS NULL", p),
),
);
}
logger.accessInfo(log);
@@ -272,45 +326,40 @@ for (const [resource, flags] of Object.entries(resources)) {
"\n";
for await (const obj of db.query(resource, filter, options)) {
const arr = Object.values(columns).map((exp) => {
const v = evaluate(exp, obj, null, (e) => {
if (Array.isArray(e)) {
if (e[0] === "PARAM") {
return exp.evaluate((e) => {
if (e instanceof Expression.Literal) return e;
else if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(now);
if (e.name === "DATE_STRING") {
if (e.args[0] instanceof Expression.Literal)
return new Expression.Literal(
new Date(e.args[0].value as number).toJSON(),
);
}
} else if (e instanceof Expression.Parameter) {
let v = obj[e.path.toString()];
if (resource === "devices") {
if (e[1] === "Tags") {
if (e.path.toString() === "Tags") {
const tags = [];
for (const p in obj)
if (p.startsWith("Tags."))
if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
tags.push(decodeTag(p.slice(5)));
return tags.join(", ");
v = 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();
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[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();
}
}
if (e.path.toString() === "detail") v = yamlStringify(v);
}
return e;
});
if (Array.isArray(v) || v == null) return "";
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
return v;
if (typeof v === "string") v = `"${v.replace(/"/g, '""')}"`;
if (v != null) return new Expression.Literal(v);
}
return new Expression.Literal(null);
}).value;
});
yield arr.join(",") + "\n";
}
@@ -326,11 +375,15 @@ for (const [resource, flags] of Object.entries(resources)) {
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
};
const filter = and(authorizer.getFilter(resource, 2), [
const filter = Expression.and(
authorizer.getFilter(resource, 2),
new Expression.Binary(
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
@@ -352,11 +405,14 @@ for (const [resource, flags] of Object.entries(resources)) {
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
};
const filter = and(authorizer.getFilter(resource, 2), [
const filter = Expression.and(
authorizer.getFilter(resource, 2),
new Expression.Binary(
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
@@ -379,11 +435,14 @@ for (const [resource, flags] of Object.entries(resources)) {
id: ctx.params.id,
};
const filter = and(authorizer.getFilter(resource, 3), [
const filter = Expression.and(
authorizer.getFilter(resource, 3),
new Expression.Binary(
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
@@ -467,11 +526,14 @@ router.get("/blob/files/:id", async (ctx) => {
id: id,
};
const filter = and(authorizer.getFilter(resource, 2), [
const filter = Expression.and(
authorizer.getFilter(resource, 2),
new Expression.Binary(
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
@@ -566,11 +628,14 @@ router.post("/devices/:id/tasks", async (ctx) => {
let statuses: { _id: string; status: string }[];
try {
const filter = and(authorizer.getFilter("devices", 3), [
const filter = Expression.and(
authorizer.getFilter("devices", 3),
new Expression.Binary(
"=",
["PARAM", "DeviceID.ID"],
deviceId,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
new Expression.Literal(ctx.params.id),
),
);
device = (await db.query("devices", filter).next()).value;
if (!device) return void (ctx.status = 404);
@@ -596,30 +661,31 @@ router.post("/devices/:id/tasks", async (ctx) => {
await releaseLock(`cwmp_session_${deviceId}`, token);
}
const now = Date.now();
const onlineThreshold = getConfig(
ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold",
{},
Date.now(),
4000,
(exp) => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") {
const p = device[exp[1]];
if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") {
if (exp instanceof Expression.Literal) return exp;
else if (exp instanceof Expression.Parameter) {
const p = device[exp.path.toString()];
if (p != null) return new Expression.Literal(p);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return new Expression.Literal(now);
if (exp.name === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p?.value) return new URL(p.value[0]).hostname;
}
return null;
if (p != null) return new Expression.Literal(new URL(p).hostname);
}
}
return exp;
}
return new Expression.Literal(null);
},
) as number;
);
const lastInform = device["Events.Inform"].value[0] as number;
const lastInform = device["Events.Inform"] as number;
let status = await apiFunctions.connectionRequest(deviceId, device);
if (!status) {
@@ -667,11 +733,14 @@ router.post("/devices/:id/tags", async (ctx) => {
tags: ctx.request.body,
};
const filter = and(authorizer.getFilter("devices", 3), [
const filter = Expression.and(
authorizer.getFilter("devices", 3),
new Expression.Binary(
"=",
["PARAM", "DeviceID.ID"],
ctx.params.id,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
new Expression.Literal(ctx.params.id),
),
);
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
@@ -745,11 +814,14 @@ router.put("/users/:id/password", async (ctx) => {
const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) {
const filter = and(authorizer.getFilter("users", 3), [
const filter = Expression.and(
authorizer.getFilter("users", 3),
new Expression.Binary(
"=",
["PARAM", RESOURCE_IDS.users],
username,
]);
new Expression.Parameter(Path.parse(RESOURCE_IDS["users"])),
new Expression.Literal(username),
),
);
const { value: res } = await db.query("users", filter).next();
if (!res) return void (ctx.status = 404);
const validate = authorizer.getValidator("users", res);
+65 -135
View File
@@ -2,12 +2,11 @@ import { Script } from "node:vm";
import { Readable } from "node:stream";
import { Collection, ObjectId, WithoutId } from "mongodb";
import { encodeTag } from "../util.ts";
import { evaluate } from "../common/expression/util.ts";
import { Expression, Fault, Task } from "../types.ts";
import { Fault, Task } from "../types.ts";
import { collections, filesBucket } from "../db/db.ts";
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
import * as MongoTypes from "../db/types.ts";
import { parse, parseList, stringify } from "../common/expression/parser.ts";
import Expression, { parseList, Value } from "../common/expression.ts";
import { toMongoQuery } from "../db/synth.ts";
function processDeviceProjection(
@@ -61,21 +60,8 @@ function parseDate(d: Date): number | string {
return isNaN(n) ? "" + d : n;
}
interface FlatAttributes {
object?: boolean;
objectTimestamp?: number;
writable?: boolean;
writableTimestamp?: number;
value?: [string | number | boolean, string];
valueTimestamp?: number;
notification?: number;
notificationTimestamp?: number;
accessList?: string[];
accessListTimestamp?: number;
}
export interface FlatDevice {
[param: string]: FlatAttributes;
[param: string]: Value;
}
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
@@ -88,100 +74,38 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
for (const [name, tree] of Object.entries(input)) {
if (!root) {
if (name === "_lastInform") {
output["Events.Inform"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["Events.Inform"] = parseDate(tree as Date);
output["Events.Inform:type"] = "xsd:dateTime";
} else if (name === "_registered") {
output["Events.Registered"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["Events.Registered"] = parseDate(tree as Date);
output["Events.Registered:type"] = "xsd:dateTime";
} else if (name === "_lastBoot") {
output["Events.1_BOOT"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["Events.1_BOOT"] = parseDate(tree as Date);
output["Events.1_BOOT:type"] = "xsd:dateTime";
} else if (name === "_lastBootstrap") {
output["Events.0_BOOTSTRAP"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["Events.0_BOOTSTRAP"] = parseDate(tree as Date);
output["Events.0_BOOTSTRAP:type"] = "xsd:dateTime";
} else if (name === "_id") {
output["DeviceID.ID"] = {
value: [tree as string, "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.ID"] = tree as string;
output["DeviceID.ID:type"] = "xsd:dateTime";
} else if (name === "_deviceId") {
output["DeviceID.Manufacturer"] = {
value: [tree["_Manufacturer"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.OUI"] = {
value: [tree["_OUI"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.ProductClass"] = {
value: [tree["_ProductClass"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.SerialNumber"] = {
value: [tree["_SerialNumber"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.Manufacturer"] = tree["_Manufacturer"];
output["DeviceID.Manufacturer:type"] = "xsd:string";
output["DeviceID.OUI"] = tree["_OUI"];
output["DeviceID.OUI:type"] = "xsd:string";
output["DeviceID.ProductClass"] = tree["_ProductClass"];
output["DeviceID.ProductClass:type"] = "xsd:string";
output["DeviceID.SerialNumber"] = tree["_SerialNumber"];
output["DeviceID.SerialNumber:type"] = "xsd:string";
} else if (name === "_tags") {
output["Tags"] = {
writable: false,
writableTimestamp: timestamp,
object: true,
objectTimestamp: timestamp,
};
output["Tags:object"] = true;
output["Tags:writable"] = true;
for (const t of tree as string[]) {
output[`Tags.${encodeTag(t)}`] = {
value: [true, "xsd:boolean"],
valueTimestamp: timestamp,
writable: true,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
const et = encodeTag(t);
output[`Tags.${et}`] = true;
output[`Tags.${et}:type`] = "xsd:boolean";
output[`Tags.${et}:writable`] = true;
}
}
}
@@ -194,39 +118,36 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
else if (+input["_timestamp"] > timestamp)
childrenTimestamp = +input["_timestamp"];
const attrs: FlatAttributes = {};
const r = root ? `${root}.${name}` : name;
if (tree["_value"] != null) {
attrs.value = [
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
tree["_type"],
];
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
attrs.object = false;
attrs.objectTimestamp = childrenTimestamp;
output[r] =
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"];
output[`${r}:type`] = tree["_type"];
output[`${r}:timestamp`] = childrenTimestamp;
output[`${r}:valueTimestamp`] = +(
tree["_timestamp"] || childrenTimestamp
);
} else if (tree["_object"] != null) {
attrs.object = tree["_object"];
attrs.objectTimestamp = childrenTimestamp;
output[`${r}:object`] = tree["_object"];
output[`${r}:timestamp`] = childrenTimestamp;
}
if (tree["_writable"] != null) {
attrs.writable = tree["_writable"];
attrs.writableTimestamp = childrenTimestamp;
output[`${r}:writable`] = tree["_writable"];
output[`${r}:timestamp`] = childrenTimestamp;
}
if (tree["_notification"] != null) {
attrs.notification = tree["_notification"];
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
output[`${r}:notification`] = tree["_notification"];
output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
}
if (tree["_accessList"] != null) {
attrs.accessList = tree["_accessList"];
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
output[`${r}:accessList`] = tree["_accessList"].join(",");
output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
}
const r = root ? `${root}.${name}` : name;
output[r] = attrs;
if (attrs.object || tree["object"] == null)
recursive(tree, r, output, childrenTimestamp);
}
}
@@ -259,14 +180,12 @@ function flattenPreset(
if (p.precondition) {
try {
// Try parse to check expression validity
parse(p.precondition as string);
Expression.parse(p.precondition as string);
} catch {
p.precondition = convertOldPrecondition(
JSON.parse(p.precondition as string),
);
p.precondition = (p.precondition as string).length
? stringify(p.precondition as Expression)
: "";
const e = convertOldPrecondition(JSON.parse(p.precondition as string));
if (e instanceof Expression.Literal && e.value)
p.precondition = e.toString();
else p.precondition = "";
}
}
@@ -285,7 +204,7 @@ function flattenPreset(
) {
p.provision = provision.name;
p.provisionArgs = provision.args
? provision.args.map((a) => stringify(a)).join(", ")
? provision.args.map((a) => a.toString()).join(", ")
: "";
}
@@ -309,8 +228,7 @@ function preProcessPreset(data: Record<string, unknown>): MongoTypes.Preset {
const preset = Object.assign({}, data);
if (!preset.precondition) preset.precondition = "";
// Try parse to check expression validity
parse(preset.precondition as string);
else Expression.parse(preset.precondition as string); // Try parse to check validity
preset.weight = parseInt(preset.weight as string) || 0;
@@ -361,7 +279,13 @@ export async function* query(
options?: QueryOptions,
): AsyncGenerator<any, void, undefined> {
options = options || {};
filter = evaluate(filter, null, Date.now());
const now = Date.now();
filter = filter.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(now);
}
return e;
});
const q = toMongoQuery(filter, resource);
if (!q) return;
@@ -408,7 +332,13 @@ export async function* query(
export function count(resource: string, filter: Expression): Promise<number> {
const collection = collections[resource] as Collection<any>;
filter = evaluate(filter, null, Date.now());
const now = Date.now();
filter = filter.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(now);
}
return e;
});
const q = toMongoQuery(filter, resource);
if (!q) return Promise.resolve(0);
return collection.countDocuments(q);
+39 -66
View File
@@ -1,9 +1,7 @@
import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts";
import * as expression from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import { Expression, Users, Permissions, Config, UiConfig } from "../types.ts";
import Expression, { Value } from "../common/expression.ts";
import { Users, Permissions, Config, UiConfig } from "../types.ts";
import { LocalCache } from "../local-cache.ts";
interface Snapshot {
@@ -26,12 +24,14 @@ async function fetchPermissions(): Promise<[string, Permissions]> {
if (!permissions[p.role]) permissions[p.role] = {};
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
let validate: Expression;
if (p.validate) validate = Expression.parse(p.validate);
else validate = new Expression.Literal(true);
permissions[p.role][p.access][p.resource] = {
access: p.access,
filter: parse(p.filter || "true"),
filter: Expression.parse(p.filter || "true"),
validate,
};
if (p.validate)
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
}
return [h, permissions];
@@ -62,33 +62,18 @@ async function fetchConfig(): Promise<[string, Config, UiConfig]> {
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
const ui = {
filters: {},
device: {},
index: {},
overview: {},
pageSize: null,
};
const ui: UiConfig = {};
const _config = {};
for (const c of conf) {
// Evaluate expressions to simplify them
const val = expression.evaluate(parse(c.value));
_config[c._id] = val;
if (c._id.startsWith("ui.")) {
const keys = c._id.split(".");
if (!(keys[1] in ui)) continue;
// remove the first key(ui)
keys.shift();
let ref = ui;
while (keys.length > 1) {
const k = keys.shift();
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
ref = ref[k];
}
ref[keys[0]] = val;
ui[c._id.slice(3)] = c.value;
continue;
}
// Evaluate expressions to simplify them
const val = Expression.parse(c.value).evaluate((e) => e);
_config[c._id] = val;
}
return [h, _config, ui];
@@ -123,47 +108,35 @@ export async function getRevision(): Promise<string> {
export function getConfig(
revision: string,
key: string,
context: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null {
dflt: string,
fn: (e: Expression) => Expression.Literal,
): string;
export function getConfig(
revision: string,
key: string,
dflt: number,
fn: (e: Expression) => Expression.Literal,
): number;
export function getConfig(
revision: string,
key: string,
dflt: boolean,
fn: (e: Expression) => Expression.Literal,
): boolean;
export function getConfig(
revision: string,
key: string,
dflt: Value,
fn: (e: Expression) => Expression.Literal,
): Value {
const snapshot = localCache.get(revision);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
};
if (!(key in snapshot.config)) {
if (key in oldOpts) {
let id;
if (context?.["id"]) {
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return config.get(oldOpts[key], id);
}
return null;
}
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
const e = snapshot.config[key];
if (!e) return dflt;
const v = e.evaluate(fn).value;
if (typeof v !== typeof dflt) return dflt;
return v;
}
export function getConfigExpression(revision: string, key: string): Expression {
+8
View File
@@ -183,4 +183,12 @@ export default class VersionedMap<K, V> {
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-send": "^5.0.1",
"mongodb": "^4.16.0",
"parsimmon": "^1.18.1",
"seedrandom": "^3.0.5"
},
"bin": {
@@ -46,7 +45,6 @@
"@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.7",
"@types/node": "^25.3.5",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8",
"esbuild": "^0.27.4",
"eslint": "^10.0.3",
@@ -3028,12 +3026,6 @@
"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": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
@@ -5323,11 +5315,6 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8317,12 +8304,6 @@
"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": {
"version": "6.15.0",
"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",
"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": {
"version": "4.0.0",
"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-send": "^5.0.1",
"mongodb": "^4.16.0",
"parsimmon": "^1.18.1",
"seedrandom": "^3.0.5"
},
"devDependencies": {
@@ -56,7 +55,6 @@
"@types/koa-compress": "^4.0.7",
"@types/mithril": "^2.2.7",
"@types/node": "^25.3.5",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8",
"esbuild": "^0.27.4",
"eslint": "^10.0.3",
+6 -9
View File
@@ -2,13 +2,13 @@ import test from "node:test";
import assert from "node:assert";
import { EJSON } from "bson";
import { Filter } from "mongodb";
import { stringify, parse } from "../lib/common/expression/parser.ts";
import Expression from "../lib/common/expression.ts";
import { convertOldPrecondition } from "../lib/db/util.ts";
import { toMongoQuery } from "../lib/db/synth.ts";
void test("convertOldPrecondition", () => {
const tests = [
[{}, "true"],
[{}, "TRUE"],
[{ test: "test" }, 'test = "test"'],
[{ test: { $eq: "test" } }, 'test = "test"'],
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
@@ -31,7 +31,7 @@ void test("convertOldPrecondition", () => {
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
[
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
'test = "test" OR (test <> "test" OR test IS NULL)',
'test = "test" OR test <> "test" OR test IS NULL',
],
[
{ test: { $gte: "test1", $ne: "test2" } },
@@ -54,7 +54,7 @@ void test("convertOldPrecondition", () => {
for (const t of tests) {
assert.strictEqual(
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)),
convertOldPrecondition(t[0] as Record<string, unknown>).toString(),
t[1],
);
}
@@ -86,7 +86,6 @@ void test("toMongoQuery", async () => {
"Param1 <> 1657844103524",
{
"Param1._value": { $ne: 1657844103524 },
$and: [
{
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
@@ -114,7 +113,6 @@ void test("toMongoQuery", async () => {
},
},
],
[
"LOWER(Param1) LIKE 'value'",
{
@@ -146,7 +144,7 @@ void test("toMongoQuery", async () => {
];
for (const [expStr, expect] of queries) {
const exp = parse(expStr);
const exp = Expression.parse(expStr);
let query = toMongoQuery(exp, "devices");
if (query) query = EJSON.serialize(query);
assert.deepStrictEqual(query, expect);
@@ -156,11 +154,10 @@ void test("toMongoQuery", async () => {
["Param1 = Param2", "Right-hand operand must be a literal value"],
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
["NOW() = 1", "Left-hand operand must be a parameter"],
["param{param2} = 1", "Left-hand operand must be a parameter"],
];
for (const [expStr, err] of failQueries) {
const exp = parse(expStr);
const exp = Expression.parse(expStr);
assert.throws(() => toMongoQuery(exp, "devices"), {
message: err,
});
+22 -19
View File
@@ -7,7 +7,7 @@ import VersionedMap from "../lib/versioned-map.ts";
import { Attributes } from "../lib/types.ts";
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 expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
@@ -32,66 +32,69 @@ void test("unpack", () => {
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"]],
});
device.set(deviceData, Path.parse("a.1.c"), now, {
device.set(deviceData, "a.1.c", now, {
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"]],
});
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"]],
});
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"]],
});
device.set(deviceData, Path.parse("a.1.a.2.a"), now, {
device.set(deviceData, "a.1.a.2.a", now, {
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"]],
});
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"]],
});
device.set(deviceData, Path.parse("a.2.b"), now, {
device.set(deviceData, "a.2.b", now, {
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"]],
});
device.set(deviceData, Path.parse("a.2.a.1.a"), now, {
device.set(deviceData, "a.2.a.1.a", now, {
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"]],
});
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"]],
});
device.set(deviceData, Path.parse("a.2.a.2.a"), now, {
device.set(deviceData, "a.2.a.2.a", now, {
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"]],
});
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"]],
});
let unpacked: Path[];
unpacked = device.unpack(
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[0].toString(), "a.1.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[0].toString(), "a.2.a.2.a");
});
+10 -10
View File
@@ -1,13 +1,12 @@
import test from "node:test";
import assert from "node:assert";
import initSqlJs from "sql.js/dist/sql-asm.js";
import { parse, stringify } from "../lib/common/expression/parser.ts";
import {
bookmarkToExpression,
paginate,
toBookmark,
} 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";
const VALUES = [null, -1, false, "a"];
@@ -79,15 +78,15 @@ async function testPaginate(
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
.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);
assert.ok(covers(q1, fulfilled));
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]);
if (allMatches.length === fulfilledMatches.length) return;
@@ -98,14 +97,14 @@ async function testPaginate(
);
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
const capped = ["AND", q2, bookmarkFilter];
const capped = Expression.and(q2, bookmarkFilter);
assert.ok(!covers(q1, capped));
const cappedMatches = await query(
`(${stringify(capped)}) ORDER BY ${orderBy}`,
`(${capped.toString()}) ORDER BY ${orderBy}`,
);
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
const min = minimize(["OR", q1, capped]);
const min = minimize(Expression.or(q1, capped));
await testPaginate(min, q2, sort);
}
@@ -115,6 +114,7 @@ void test("paginate", async () => {
const sortOrders = getAllSortOrders(params);
for (const [q1, q2] of cases)
for (const sort of sortOrders)
await testPaginate(parse(q1), parse(q2), sort);
for (const sort of sortOrders) {
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 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", () => {
const pathSet = new PathSet();
pathSet.add(Path.parse("a"));
pathSet.add(Path.parse("a"));
assert.strictEqual(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
pathSet.add("a");
pathSet.add("a");
assert.strictEqual(
pathSet.findCompat(Path.parse("a"), true, true, 99).length,
1,
);
});
void test("get", () => {
const pathSet = new PathSet();
pathSet.add(Path.parse("a.*"));
pathSet.add(Path.parse("a.a"));
pathSet.add(Path.parse("*.*"));
pathSet.add("a.*");
pathSet.add("a.a");
pathSet.add("*.*");
assert.strictEqual(pathSet.get(Path.parse("a.*")).toString(), "a.*");
assert.strictEqual(pathSet.get(Path.parse("*.a")), null);
assert.strictEqual(pathSet.get("a.*").toString(), "a.*");
assert.equal(pathSet.get("*.a"), null);
});
void test("find", () => {
const pathSet = new PathSet();
pathSet.add(Path.parse("a"));
pathSet.add(Path.parse("a.*"));
pathSet.add(Path.parse("a.a"));
pathSet.add(Path.parse("*.a"));
pathSet.add(Path.parse("*.*"));
pathSet.add("a");
pathSet.add("a.*");
pathSet.add("a.a");
pathSet.add("*.a");
pathSet.add("*.*");
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"],
);
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", "*.*"],
);
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.*"],
);
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"],
);
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.*", "*.*"],
);
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", "*.*"],
);
});
+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..b"));
assert.throws(() => Path.parse("b*"));
assert.throws(() => Path.parse("*b"));
assert.doesNotThrow(() => Path.parse("b*"));
assert.doesNotThrow(() => Path.parse("*b"));
assert.throws(() => Path.parse("a.b c.d"));
assert.throws(() => Path.parse("a["));
assert.throws(() => Path.parse("a[b"));
assert.throws(() => Path.parse("a[b:"));
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.throws(() => Path.parse(""));
});
void test("toString", () => {
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
const path1 = Path.parse('abc.[ abc=123 and def=" abc " ].123');
const path2 = Path.parse('abc.[abc = 123 AND def = " abc "].123');
assert.strictEqual(path1.toString(), path2.toString());
});
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);
assert.strictEqual(sliced.toString(), '*.b.[x:"y"]');
assert.strictEqual(sliced.toString(), '*.b.[x = "y"]');
assert.strictEqual(sliced.alias, 0b100);
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", () => {
const path1 = Path.parse("a");
const path2 = Path.parse("*.[a:b]");
const concat = path1.concat(path2);
assert.strictEqual(concat.toString(), 'a.*.[a:"b"]');
assert.strictEqual(concat.alias, 0b100);
assert.strictEqual(concat.wildcard, 0b10);
// Alias and wildcard propagation
const c0 = Path.parse("a").concat(Path.parse('*.[a = "b"]'));
assert.strictEqual(c0.toString(), 'a.*.[a = "b"]');
assert.strictEqual(c0.alias, 0b100);
assert.strictEqual(c0.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 initSqlJs from "sql.js/dist/sql-asm.js";
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 DECIMAL_VALUES = [null, 0, -10, 10];
@@ -86,7 +86,7 @@ void test("minimize", async () => {
for (const c of cases) {
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);
assert.strictEqual(setsEqual(res1, res2), true);
}
@@ -104,9 +104,9 @@ void test("unionDiff", async () => {
for (const [c1, c2] of getPermutations(cases, cases)) {
const res1 = await query(c1);
const res2 = await query(c2);
const [union, diff] = unionDiff(parse(c1), parse(c2));
const res3 = await query(stringify(union));
const res4 = await query(stringify(diff));
const [union, diff] = unionDiff(Expression.parse(c1), Expression.parse(c2));
const res3 = await query(union.toString());
const res4 = await query(diff.toString());
const unionSet = new Set([...res1, ...res2]);
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
@@ -117,15 +117,35 @@ void test("unionDiff", 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(
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,
);
@@ -139,7 +159,7 @@ void test("covers", async () => {
for (const [c1, c2] of cases) {
const res1 = await query(c1);
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));
assert.strictEqual(
+21 -3
View File
@@ -20,19 +20,37 @@ import { contextifyComponent } from "./components.ts";
import { PermissionSet, UiConfig } from "../lib/types.ts";
import drawerComponent from "./drawer-component.ts";
import { render as renderOverlay } from "./overlay.ts";
import Expression from "../lib/common/expression.ts";
declare global {
interface Window {
authorizer: Authorizer;
permissionSets: PermissionSet[];
permissionSets: {
[resource: string]: { access: number; validate: string; filter: string };
}[][];
username: string;
clientConfig: { ui: UiConfig };
clientConfig: UiConfig;
configSnapshot: 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;
+57 -35
View File
@@ -1,26 +1,41 @@
import { ClosureComponent } from "mithril";
import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts";
import { parse } from "../../lib/common/expression/parser.ts";
import memoize from "../../lib/common/memoize.ts";
import { icon } from "../tailwind-utility-components.ts";
import { QueryResponse, evaluateExpression } from "../store.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";
const memoizedParse = memoize(parse);
import Path from "../../lib/common/path.ts";
function escapeRegExp(str): string {
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[][] {
if (keysByDepth.has(device)) return keysByDepth.get(device);
const res: string[][] = [];
for (const key of Object.keys(device)) {
const prepareParams = memoize((device: FlatDevice): Parameter[][] => {
const map = new Map<string, Parameter>();
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;
for (
let i = key.lastIndexOf(".", key.length - 2);
@@ -29,11 +44,10 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
)
++count;
while (res.length <= count) res.push([]);
res[count].push(key);
res[count].push(attrs);
}
keysByDepth.set(device, res);
return res;
}
});
interface Attrs {
device: FlatDevice;
@@ -51,9 +65,13 @@ const component: ClosureComponent<Attrs> = () => {
return {
view: (vnode) => {
const device = vnode.attrs.device;
const allParams = prepareParams(device);
const limit =
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
let limit = 100;
if (vnode.attrs.limit) {
const l = evaluateExpression(vnode.attrs.limit, device);
if (typeof l.value === "number") limit = l.value;
}
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",
@@ -75,35 +93,36 @@ const component: ClosureComponent<Attrs> = () => {
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
}
const filteredKeys: string[] = [];
const allKeys = orderKeysByDepth(device);
const filteredParams: Parameter[] = [];
let count = 0;
for (const keys of allKeys) {
for (const keys of allParams) {
let c = 0;
for (const k of keys) {
const p = device[k];
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
const str = k.value ? `${k.path.toString()} ${k.value}` : k;
if (re && !re.test(str)) continue;
++c;
if (count < limit) filteredKeys.push(k);
if (count < limit) filteredParams.push(k);
}
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 p = device[k];
const rows = filteredParams.map((p) => {
const val = [];
if (p.object === false) {
if (p.value) {
val.push(
m(
"parameter",
Object.assign({ device: device, parameter: memoizedParse(k) }),
Object.assign({ device: device, parameter: p.path }),
),
);
} else if (p.object && p.writable) {
if (instanceRegex.test(k)) {
if (instanceRegex.test(p.path.toString())) {
val.push(
m(
"button",
@@ -112,8 +131,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => {
taskQueue.queueTask({
name: "deleteObject",
device: device["DeviceID.ID"].value[0] as string,
objectName: k,
device: device["DeviceID.ID"] as string,
objectName: p.path.toString(),
});
},
},
@@ -133,8 +152,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => {
taskQueue.queueTask({
name: "addObject",
device: device["DeviceID.ID"].value[0] as string,
objectName: k,
device: device["DeviceID.ID"] as string,
objectName: p.path.toString(),
});
},
},
@@ -156,8 +175,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => {
taskQueue.queueTask({
name: "getParameterValues",
device: device["DeviceID.ID"].value[0] as string,
parameterNames: [k],
device: device["DeviceID.ID"] as string,
parameterNames: [p.path.toString()],
});
},
},
@@ -170,7 +189,10 @@ const component: ClosureComponent<Attrs> = () => {
return m(
"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),
);
});
@@ -195,7 +217,7 @@ const component: ClosureComponent<Attrs> = () => {
m(
"span.text-xs",
"Displaying ",
m("span.font-medium", "" + filteredKeys.length),
m("span.font-medium", "" + filteredParams.length),
" out of ",
m("span.font-medium", "" + count),
" parameters",
@@ -204,7 +226,7 @@ const component: ClosureComponent<Attrs> = () => {
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
{
href: `api/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0],
device["DeviceID.ID"],
)}.csv`,
download: "",
},
+16 -15
View File
@@ -1,7 +1,6 @@
import { Attributes, ClosureComponent } from "mithril";
import { map } from "../../lib/common/expression/parser.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 { evaluateExpression, getTimestamp } from "../store.ts";
import { FlatDevice } from "../../lib/ui/db.ts";
@@ -14,15 +13,16 @@ const evaluateAttributes = memoize(
): Attributes => {
const res: Attributes = {};
for (const [k, v] of Object.entries(attrs)) {
const vv = map(v, (e) => {
if (
Array.isArray(e) &&
e[0] === "FUNC" &&
e[1] === "ENCODEURICOMPONENT"
) {
const a = evaluateExpression(e[2], obj);
if (a == null) return null;
return encodeURIComponent(a as string);
const vv = v.evaluate((e) => {
if (e instanceof Expression.Literal) return e;
else if (e instanceof Expression.FunctionCall) {
if (e.name === "ENCODEURICOMPONENT") {
const a = evaluateExpression(e.args[0], obj);
if (a instanceof Expression.Literal) {
if (a.value == null) return new Expression.Literal(null);
return new Expression.Literal(encodeURIComponent(a.value));
}
}
}
return e;
});
@@ -47,9 +47,10 @@ const component: ClosureComponent<Attrs> = () => {
}
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}`;
const type = evaluateExpression(c["type"], device || {});
const type = evaluateExpression(c["type"], device || {}).value;
if (!type) return null;
return m(type as string, c);
});
@@ -59,8 +60,8 @@ const component: ClosureComponent<Attrs> = () => {
if (el == null) return children;
let attrs: Attributes;
if (Array.isArray(el)) {
el = evaluateExpression(el, device || {});
if (el instanceof Expression) {
el = evaluateExpression(el, device || {}).value;
} else if (typeof el === "object") {
if (el["attributes"] != null) {
attrs = evaluateAttributes(
+4 -4
View File
@@ -19,7 +19,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.queueTask({
name: "reboot",
device: device["DeviceID.ID"].value[0],
device: device["DeviceID.ID"],
});
},
},
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.queueTask({
name: "factoryReset",
device: device["DeviceID.ID"].value[0],
device: device["DeviceID.ID"],
});
},
},
@@ -51,7 +51,7 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.stageDownload({
name: "download",
devices: [device["DeviceID.ID"].value[0]],
devices: [device["DeviceID.ID"]],
});
},
},
@@ -66,7 +66,7 @@ const component: ClosureComponent = (): Component => {
title: "Delete device",
onclick: () => {
if (!confirm("Deleting this device. Are you sure?")) return;
const deviceId = device["DeviceID.ID"].value[0];
const deviceId = device["DeviceID.ID"];
store
.deleteResource("devices", deviceId)
+13 -6
View File
@@ -3,17 +3,24 @@ import { m } from "../components.ts";
import * as store from "../store.ts";
import * as notifications from "../notifications.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 => {
return {
view: (vnode) => {
const device = vnode.attrs["device"];
const deviceId = device["DeviceID.ID"].value[0];
const faults = store.fetch("faults", [
"AND",
[">", ["PARAM", "_id"], `${deviceId}:`],
["<", ["PARAM", "_id"], `${deviceId}:zzzz`],
]);
const deviceId = device["DeviceID.ID"];
const p = new Expression.Parameter(Path.parse("_id"));
const exp = Expression.and(
new Expression.Binary(">", p, new Expression.Literal(`${deviceId}:`)),
new Expression.Binary(
"<",
p,
new Expression.Literal(`${deviceId}:zzzz`),
),
);
const faults = store.fetch("faults", exp);
const headers = [
"Channel",
+5 -3
View File
@@ -1,18 +1,20 @@
import { ClosureComponent, Component } from "mithril";
import { m } from "../components.ts";
import { evaluateExpression } from "../store.ts";
import Expression from "../../lib/common/expression.ts";
const component: ClosureComponent = (): Component => {
return {
view: (vnode) => {
let deviceId;
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) => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (c instanceof Expression)
c = evaluateExpression(c, device ?? {}).value;
if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {});
const type = evaluateExpression(c["type"], device ?? {}).value;
if (!type) return null;
const attrs = Object.assign({}, vnode.attrs, c);
return m(type as string, attrs);
+12 -9
View File
@@ -1,20 +1,23 @@
import { ClosureComponent, Component } from "mithril";
import { m } from "../components.ts";
import config from "../config.ts";
import { overview } from "../config.ts";
import { evaluateExpression } from "../store.ts";
const CHARTS = config.ui.overview.charts;
const CHARTS = overview.charts;
const component: ClosureComponent = (): Component => {
return {
view: (vnode) => {
const device = vnode.attrs["device"];
const chartName = evaluateExpression(vnode.attrs["chart"], device || {});
const chart = CHARTS[chartName as string] as Record<string, unknown>;
const chartName = evaluateExpression(
vnode.attrs["chart"],
device ?? {},
).value;
const chart = CHARTS[chartName as string];
if (!chart) return null;
for (const slice of Object.values(chart.slices)) {
const filter = slice["filter"];
if (evaluateExpression(filter, device || {})) {
for (const slice of chart.slices) {
const filter = slice.filter;
if (evaluateExpression(filter, device ?? {}).value) {
const dot = m(
"svg.inline",
{
@@ -28,10 +31,10 @@ const component: ClosureComponent = (): Component => {
cx: "0.5em",
cy: "0.5em",
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;
+7 -3
View File
@@ -2,7 +2,7 @@ import { ClosureComponent, VnodeDOM } from "mithril";
import { m } from "../components.ts";
import { QueryResponse, evaluateExpression } from "../store.ts";
import { FlatDevice } from "../../lib/ui/db.ts";
import { Expression } from "../../lib/types.ts";
import Expression from "../../lib/common/expression.ts";
interface Attrs {
device: FlatDevice;
@@ -19,7 +19,11 @@ const component: ClosureComponent<Attrs> = () => {
const device = vnode.attrs.device;
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(
{
device: device,
@@ -45,7 +49,7 @@ const component: ClosureComponent<Attrs> = () => {
},
m(
"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),
);
+37 -30
View File
@@ -2,47 +2,45 @@ import { ClosureComponent } from "mithril";
import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts";
import { QueryResponse, evaluateExpression } from "../store.ts";
import * as expressionParser from "../../lib/common/expression/parser.ts";
import { icon } from "../tailwind-utility-components.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 {
device: FlatDevice;
parameter: Expression;
label: Expression;
childParameters: Record<string, { label: string; parameter: Expression }>;
childParameters: Record<string, { label: Expression; parameter: Expression }>;
filter?: Expression;
deviceQuery: QueryResponse;
}
const component: ClosureComponent<Attrs> = () => {
let object: Path;
let parameters: { label: Expression; parameter: Expression }[];
return {
oninit: (vnode) => {
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");
vnode.state["object"] = obj[1];
vnode.state["parameters"] = Object.values(vnode.attrs.childParameters);
object = obj.path;
parameters = Object.values(vnode.attrs.childParameters);
},
view: (vnode) => {
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 prefix = `${object}.`;
const prefix = `${object.toString()}.`;
for (const p in device) {
if (p.startsWith(prefix)) {
if (!p.startsWith(prefix)) continue;
if (p.lastIndexOf(":") !== -1) continue;
const i = p.indexOf(".", prefix.length);
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";
return m(
@@ -53,7 +51,7 @@ const component: ClosureComponent<Attrs> = () => {
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
padding,
},
evaluateExpression(p["label"], device),
evaluateExpression(p.label, device).value,
);
});
@@ -63,24 +61,33 @@ const component: ClosureComponent<Attrs> = () => {
const rows = [];
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) => {
if (Array.isArray(e) && e[0] === "PARAM")
return ["PARAM", ["||", i, ".", e[1]]];
const root = Path.parse(i);
filter = filter.evaluate((e) => {
if (e instanceof Expression.Parameter)
return new Expression.Parameter(root.concat(e.path));
return e;
});
if (!evaluateExpression(filter, device)) continue;
if (!evaluateExpression(filter, device).value) continue;
const row = parameters.map((p, j) => {
const padding = j ? "px-3" : "pl-6 pr-3";
const param = expressionParser.map(p.parameter, (e) => {
if (Array.isArray(e) && e[0] === "PARAM")
return ["PARAM", ["||", i, ".", e[1]]];
const param = p.parameter.evaluate((e) => {
if (e instanceof Expression.Parameter)
return new Expression.Parameter(root.concat(e.path));
return e;
});
let type = "parameter";
if (p["type"] instanceof Expression)
type = evaluateExpression(p["type"], device).value + "";
return m(
"td",
{
@@ -91,7 +98,7 @@ const component: ClosureComponent<Attrs> = () => {
device: device,
parameter: param,
},
p.type || "parameter",
type,
Object.assign({}, p, {
device: device,
parameter: param,
@@ -101,7 +108,7 @@ const component: ClosureComponent<Attrs> = () => {
);
});
if (device[i].writable === true) {
if (device[i + ":writable"]) {
row.push(
m(
"td",
@@ -116,7 +123,7 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => {
taskQueue.queueTask({
name: "deleteObject",
device: device["DeviceID.ID"].value[0] as string,
device: device["DeviceID.ID"] as string,
objectName: i,
});
},
@@ -147,7 +154,7 @@ const component: ClosureComponent<Attrs> = () => {
);
}
if (device[object].writable === true) {
if (device[object.toString() + ":writable"]) {
rows.push(
m(
"tr",
@@ -161,8 +168,8 @@ const component: ClosureComponent<Attrs> = () => {
onclick: () => {
taskQueue.queueTask({
name: "addObject",
device: device["DeviceID.ID"].value[0] as string,
objectName: object,
device: device["DeviceID.ID"] as string,
objectName: object.toString(),
});
},
},
+39 -45
View File
@@ -2,54 +2,52 @@ import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components.ts";
import * as taskQueue from "../task-queue.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 timeAgo from "../timeago.ts";
import { icon } from "../tailwind-utility-components.ts";
const evaluateParam = memoize((exp, obj, now: number) => {
const evaluateParam = memoize(
(
exp: Expression,
obj: any,
now: number,
): { value: Value; timestamp: number; parameter: string } => {
let timestamp = now;
exp = evaluate(exp, null, now, (e) => {
if (!Array.isArray(e)) return e;
for (let i = 1; i < e.length; ++i) {
if (
Array.isArray(e[i]) &&
e[i][0] === "PARAM" &&
!Array.isArray(e[i][1])
) {
let v = null;
const p = obj[e[i][1]];
if (p?.value) {
v = p.value[0];
timestamp = Math.min(timestamp, p.valueTimestamp);
const valueMap: Map<Expression.Literal, string> = new Map();
const lit = exp.evaluate((e): Expression.Literal => {
if (e instanceof Expression.Literal) return e;
if (e instanceof Expression.Parameter) {
let v = obj[e.path.toString()];
if (v) {
timestamp = Math.min(
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;
}
e = e.slice();
e[i] = v;
} 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(),
);
}
}
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2]))
return new Date(e[2]).toLocaleString();
return e;
}
return new Expression.Literal(null);
});
let parameter = null;
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 };
});
return { value: lit.value, timestamp, parameter: valueMap.get(lit) };
},
);
const component: ClosureComponent = (): Component => {
return {
@@ -65,7 +63,7 @@ const component: ClosureComponent = (): Component => {
if (value == null) return null;
let edit;
if (device[parameter]?.writable) {
if (device[parameter + ":writable"]) {
edit = m(
"button",
{
@@ -73,13 +71,9 @@ const component: ClosureComponent = (): Component => {
onclick: () => {
taskQueue.stageSpv({
name: "setParameterValues",
devices: [device["DeviceID.ID"].value[0]],
devices: [device["DeviceID.ID"]],
parameterValues: [
[
parameter,
device[parameter].value[0],
device[parameter].value[1],
],
[parameter, device[parameter], device[parameter + ":type"]],
],
});
},
+14 -6
View File
@@ -1,13 +1,20 @@
import { ClosureComponent, Component } from "mithril";
import { ClosureComponent } from "mithril";
import { m } from "../components.ts";
import * as taskQueue from "../task-queue.ts";
import * as store from "../store.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 {
view: (vnode) => {
const device = vnode.attrs["device"];
const device = vnode.attrs.device;
return m(
"button",
@@ -19,15 +26,16 @@ const component: ClosureComponent = (): Component => {
e.target.disabled = true;
const params = Object.values(vnode.attrs["parameters"])
.map((exp) => {
if (!Array.isArray(exp) || exp[0] !== "PARAM") return null;
return store.evaluateExpression(exp[1], device) as string;
if (exp instanceof Expression.Parameter)
return exp.path.toString();
return null;
})
.filter((exp) => !!exp);
const task = {
name: "getParameterValues",
parameterNames: params,
device: device["DeviceID.ID"].value[0],
device: device["DeviceID.ID"] as string,
};
taskQueue
+5 -4
View File
@@ -4,7 +4,7 @@ import * as notifications from "../notifications.ts";
import * as store from "../store.ts";
import { icon } from "../tailwind-utility-components.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";
interface Attrs {
@@ -22,7 +22,8 @@ const component: ClosureComponent<Attrs> = () => {
const tags = [];
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();
@@ -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",
onclick: (e) => {
e.target.disabled = true;
const deviceId = device["DeviceID.ID"].value[0] as string;
const deviceId = device["DeviceID.ID"] as string;
store
.updateTags(deviceId, { [tag]: false })
.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",
onclick: (e) => {
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:`);
if (!tag) {
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 uiConfigComponent from "./ui-config-component.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 { 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" });
try {
object.value = stringify(parse(object.value || ""));
object.value = Expression.parse(object.value || "").toString();
} catch {
return void resolve({
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;
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 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 config from "./config.ts";
import { device as deviceConfig } from "./config.ts";
import * as store from "./store.ts";
import Expression from "../lib/common/expression.ts";
import Path from "../lib/common/path.ts";
export function init(
args: Record<string, unknown>,
@@ -14,16 +16,25 @@ export function init(
return Promise.resolve({
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 {
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.fulfilling) {
return m(
@@ -38,15 +49,15 @@ export const component: ClosureComponent = (): Component => {
);
}
const conf = config.ui.device;
const conf = deviceConfig;
const cmps = [];
for (const c of Object.values(conf)) {
cmps.push(
m.context(
{ device: dev.value[0], deviceQuery: dev },
store.evaluateExpression(c["type"], null) as string,
c,
store.evaluateExpression(c["type"], {}).value as string,
c as any,
),
);
}
+64 -52
View File
@@ -1,63 +1,72 @@
import { ClosureComponent, Component, Children } from "mithril";
import { ClosureComponent, Children } from "mithril";
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 filterComponent from "./filter-component.ts";
import * as store from "./store.ts";
import { queueTask, stageDownload } from "./task-queue.ts";
import * as notifications from "./notifications.ts";
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
import { evaluate, extractParams } from "../lib/common/expression/util.ts";
import Expression, { extractPaths } from "../lib/common/expression.ts";
import memoize from "../lib/common/memoize.ts";
import * as smartQuery from "./smart-query.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
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;
}
const memoizedGetSortable = memoize((p: Expression) => {
const expressionParams = extractPaths(p);
if (expressionParams.length === 1) return expressionParams[0];
return null;
});
const getDownloadUrl = memoize((filter, indexParameters) => {
const getDownloadUrl = memoize(
(
filter: Expression,
indexParameters: { label: string; parameter: Expression }[],
) => {
const columns = {};
for (const p of indexParameters)
columns[store.evaluateExpression(p.label, null) as string] = stringify(
p.parameter,
);
for (const p of indexParameters) columns[p.label] = p.parameter.toString();
return `api/devices.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(columns),
})}`;
});
},
);
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("devices", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
export function init(
args: Record<string, unknown>,
): Promise<Record<string, unknown>> {
export function init(args: Record<string, unknown>): Promise<Attrs> {
return new Promise((resolve, reject) => {
if (!window.authorizer.hasAccess("devices", 2))
return void reject(new Error("You are not authorized to view this page"));
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const indexParameters = Object.values(config.ui.index);
let filter: Expression = null;
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);
const indexParameters = indexConfig;
if (!indexParameters.length) {
indexParameters.push({
label: "ID",
parameter: ["PARAM", "DeviceID.ID"],
parameter: Expression.parse("DeviceID.ID"),
unsortable: false,
raw: {},
});
}
resolve({ filter, indexParameters, sort });
@@ -220,11 +229,17 @@ function renderActions(selected: Set<string>): Children {
return buttons;
}
export const component: ClosureComponent = (): Component => {
interface Attrs {
indexParameters: typeof indexConfig;
filter?: Expression;
sort?: Record<string, number>;
}
export const component: ClosureComponent<Attrs> = () => {
return {
view: (vnode) => {
document.title = "Devices - GenieACS";
const attributes = vnode.attrs["indexParameters"];
const attributes = vnode.attrs.indexParameters;
function showMore(): void {
vnode.state["showCount"] =
@@ -232,22 +247,22 @@ export const component: ClosureComponent = (): Component => {
m.redraw();
}
function onFilterChanged(filter): void {
const ops = { filter };
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
function onFilterChanged(filter: Expression): void {
const ops = {};
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);
}
const sort = vnode.attrs["sort"]
? memoizedJsonParse(vnode.attrs["sort"])
: {};
const sort = vnode.attrs.sort || {};
const sortAttributes = {};
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i];
if (attr.unsortable) continue;
const param = memoizedGetSortable(attr.parameter);
if (param) sortAttributes[i] = sort[param] || 0;
if (param) sortAttributes[i] = sort[param.toString()] || 0;
}
function onSortChange(sortedAttrs): void {
@@ -256,17 +271,16 @@ export const component: ClosureComponent = (): Component => {
const param = memoizedGetSortable(
attributes[Math.abs(index) - 1].parameter,
);
_sort[param] = Math.sign(index);
_sort[param.toString()] = Math.sign(index);
}
const ops = { sort: JSON.stringify(_sort) };
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/devices", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const devs = store.fetch("devices", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,
@@ -280,15 +294,15 @@ export const component: ClosureComponent = (): Component => {
return m.context(
{ device: device, parameter: attr.parameter },
attr.type || "parameter",
attr,
attr.raw,
);
};
const attrs = {};
attrs["attributes"] = attributes.map((a) => ({
...a,
label: store.evaluateExpression(a.label, null),
type: store.evaluateExpression(a.type, null),
label: a.label,
type: a.type,
}));
attrs["data"] = devs.value;
attrs["total"] = count.value;
@@ -301,9 +315,7 @@ export const component: ClosureComponent = (): Component => {
return m(
"a.text-cyan-700 hover:text-cyan-900",
{
href: `#!/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0],
)}`,
href: `#!/devices/${encodeURIComponent(device["DeviceID.ID"])}`,
},
"Show",
);
+2 -1
View File
@@ -18,6 +18,7 @@ import {
queueTask,
StageTask,
} from "./task-queue.ts";
import Expression from "../lib/common/expression.ts";
const invalid: WeakSet<StageTask> = new WeakSet();
@@ -95,7 +96,7 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
function renderStagingDownload(task: StageTask): Children {
if (!task.fileName || !task.fileType) invalid.add(task);
else invalid.delete(task);
const files = store.fetch("files", true);
const files = store.fetch("files", new Expression.Literal(true));
let oui = "";
let productClass = "";
for (const d of task.devices) {
+30 -18
View File
@@ -1,18 +1,15 @@
import { ClosureComponent, Component, Children } from "mithril";
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 filterComponent from "./filter-component.ts";
import * as store from "./store.ts";
import * as notifications from "./notifications.ts";
import memoize from "../lib/common/memoize.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 Expression from "../lib/common/expression.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse);
const attributes = [
@@ -33,15 +30,27 @@ const getDownloadUrl = memoize((filter) => {
}
return `api/faults.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("faults", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -66,9 +75,11 @@ export function init(
new Error("You are not authorized to view this page"),
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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 });
}
@@ -84,7 +95,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/faults", ops);
}
@@ -108,10 +121,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/faults", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const faults = store.fetch("faults", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,
+30 -17
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children } from "mithril";
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 * as store from "./store.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 * as overlay from "./overlay.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 attributes: {
@@ -46,10 +43,22 @@ const formData = {
.concat([{ id: "file", label: "File", type: "file" }]),
};
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("files", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -87,7 +96,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id;
return `api/files.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
@@ -101,8 +110,11 @@ export function init(
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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 });
}
@@ -118,7 +130,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/files", ops);
}
@@ -140,10 +154,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/files", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const files = store.fetch("files", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,
+51 -33
View File
@@ -1,10 +1,9 @@
import m, { ClosureComponent } from "mithril";
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
import memoize from "../lib/common/memoize.ts";
import Autocomplete from "./autocomplete-compnent.ts";
import * as smartQuery from "./smart-query.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 labels = smartQuery.getLabels(resource);
@@ -22,20 +21,35 @@ const getAutocomplete = memoize((resource) => {
return autocomplete;
});
function parseFilter(resource, f): Expression {
function parseFilter(resource: string, f: string): Expression {
let exp;
if (/^[\s0-9a-zA-Z]+:/.test(f)) {
const k = f.split(":", 1)[0];
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 {
exp = parse(f);
exp = Expression.parse(f);
}
const unpacked = map(exp, (e) => {
if (Array.isArray(e) && e[0] === "FUNC") {
if (e[1] === "Q") return smartQuery.unpack(resource, e[2], e[3]);
else if (e[1] === "NOW") return Date.now();
const unpacked = exp.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
if (e.name === "NOW") return new Expression.Literal(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;
});
@@ -46,28 +60,33 @@ function parseFilter(resource, f): Expression {
return exp;
}
function stringifyFilter(f: 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[] {
function splitFilter(filter: Expression): string[] {
if (!filter) return [""];
const list: string[] = [];
const f = parse(filter);
if (Array.isArray(f) && f[0] === "AND")
for (const ff of f.slice(1)) list.push(stringifyFilter(ff));
else list.push(stringifyFilter(f));
if (filter instanceof Expression.Literal && filter.value) return [""];
const list: Expression[] = [filter];
const res: string[] = [];
while (list.length) {
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("");
return list;
res.push("");
return res;
}
interface Attrs {
resource: string;
filter: string;
onChange: (filter: string) => void;
filter?: Expression;
onChange: (filter: Expression) => void;
}
const component: ClosureComponent<Attrs> = (initialVnode) => {
@@ -80,23 +99,22 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
filterTouched = false;
filterInvalid = 0;
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 {
return parseFilter(attrs.resource, f);
filter = Expression.and(filter, parseFilter(attrs.resource, f));
} catch {
filterInvalid |= 1 << idx;
}
return null;
});
}
filterList.push("");
if (filterInvalid) {
m.redraw();
return;
}
if (list.length === 0) attrs.onChange("");
else if (list.length > 1) attrs.onChange(stringify(["AND", ...list]));
else attrs.onChange(stringify(list[0]));
if (!filterList.length) attrs.onChange(null);
else attrs.onChange(filter);
}
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";
if (idx === 0) classNames += " rounded-t-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`, {
type: "text",
+4 -4
View File
@@ -63,7 +63,7 @@ function renderTable(
checked: records.length && selected.size === records.length,
onchange: (e) => {
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);
else selected.delete(id);
}
@@ -148,7 +148,7 @@ function renderTable(
// Table rows
const rows = [];
for (const record of records) {
const id = record["_id"] || record["DeviceID.ID"].value[0];
const id = record["_id"] ?? record["DeviceID.ID"];
const tds = [];
const isSelected = selected.has(id);
if (buttons.length) {
@@ -224,7 +224,7 @@ function renderTable(
{
class: isSelected ? "bg-stone-50" : "",
onclick: (e) => {
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
if (e.target.closest("input, button, a")) {
e.redraw = false;
return;
}
@@ -339,7 +339,7 @@ const component: ClosureComponent = (): Component => {
const _selected = new Set<string>();
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);
}
+25 -28
View File
@@ -1,31 +1,29 @@
import { ClosureComponent, Component } from "mithril";
import { ClosureComponent } from "mithril";
import { m } from "./components.ts";
import config from "./config.ts";
import { overview } from "./config.ts";
import * as store from "./store.ts";
import pieChartComponent from "./pie-chart-component.ts";
const GROUPS = config.ui.overview.groups || {};
const CHARTS = {};
for (const group of Object.values(GROUPS)) {
for (const chartName of Object.values(group["charts"]) as string[])
CHARTS[chartName] = config.ui.overview.charts[chartName];
const GROUPS = overview.groups;
const CHARTS: typeof overview.charts = {};
for (const group of GROUPS) {
for (const chartName of group.charts)
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);
for (let [chartName, chart] of Object.entries(charts)) {
charts[chartName] = chart = Object.assign({}, chart);
chart["slices"] = Object.assign({}, chart["slices"]);
for (let [sliceName, slice] of Object.entries(chart["slices"])) {
const filter = slice["filter"];
chart["slices"][sliceName] = slice = Object.assign({}, slice);
slice["count"] = store.count("devices", filter);
charts[chartName] = chart = { ...chart };
chart.slices = chart.slices.map((s) => ({ ...s }));
for (const slice of chart.slices) {
slice["count"] = store.count("devices", slice.filter);
}
}
return charts;
}
export function init(): Promise<Record<string, unknown>> {
export function init(): Promise<{ charts: typeof overview.charts }> {
if (!window.authorizer.hasAccess("devices", 1)) {
return Promise.reject(
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) });
}
export const component: ClosureComponent = (): Component => {
interface Attrs {
charts: typeof overview.charts;
}
export const component: ClosureComponent<Attrs> = () => {
return {
view: (vnode) => {
document.title = "Overview - GenieACS";
const children = [];
for (const group of Object.values(GROUPS)) {
if (group["label"]) {
for (const group of GROUPS) {
if (group.label) {
children.push(
m(
"h1.text-xl font-medium text-stone-900 mb-5",
store.evaluateExpression(group["label"], null),
),
m("h1.text-xl font-medium text-stone-900 mb-5", group["label"]),
);
}
const groupChildren = [];
for (const chartName of Object.values(group["charts"]) as string[]) {
const chart = vnode.attrs["charts"][chartName];
for (const chartName of group.charts) {
const chart = vnode.attrs.charts[chartName];
const chartChildren = [];
if (chart.label) {
chartChildren.push(
m(
"h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center",
store.evaluateExpression(chart.label, null),
chart.label,
),
);
}
const attrs = {};
attrs["chart"] = chart;
chartChildren.push(m(pieChartComponent, attrs));
chartChildren.push(m(pieChartComponent, { chart }));
groupChildren.push(
m(
+33 -19
View File
@@ -1,6 +1,6 @@
import { Children, ClosureComponent, Component } from "mithril";
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 notifications from "./notifications.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 * as overlay from "./overlay.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";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedParse = memoize((str) => Expression.parse(str));
const memoizedJsonParse = memoize(JSON.parse);
const attributes = [
@@ -60,10 +58,22 @@ function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
return lines;
}
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("permissions", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -89,7 +99,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
if (object.filter) {
try {
object.filter = stringify(memoizedParse(object.filter));
object.filter = memoizedParse(object.filter).toString();
} catch {
return void resolve({
filter: "Filter must be valid expression",
@@ -99,7 +109,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
if (object.validate) {
try {
object.validate = stringify(memoizedParse(object.validate));
object.validate = memoizedParse(object.validate).toString();
} catch {
return void resolve({
validate: "Validate must be valid expression",
@@ -163,7 +173,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id;
return `api/permissions.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
@@ -176,8 +186,11 @@ export function init(
new Error("You are not authorized to view this page"),
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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 });
}
@@ -193,7 +206,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/permissions", ops);
}
@@ -218,10 +233,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/permissions", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const permissions = store.fetch("permissions", filter, {
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 { stringify } from "../lib/common/expression/parser.ts";
import memoize from "../lib/common/memoize.ts";
import * as store from "./store.ts";
import Expression from "../lib/common/expression.ts";
const memoizedStringify = memoize(stringify);
function drawChart(chartData): Children {
function drawChart(chartData: Attrs["chart"]): Children {
const slices = chartData.slices;
const total: number = Array.from(Object.values(chartData.slices)).reduce(
(a: number, s) => a + (s["count"]["value"] || 0),
@@ -27,10 +23,10 @@ function drawChart(chartData): Children {
m(
"td",
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(
"td.text-stone-500 text-right tabular-nums",
`${Math.round(percent * 100)}%`,
@@ -41,7 +37,7 @@ function drawChart(chartData): Children {
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
{
href: `#!/devices/?${m.buildQueryString({
filter: memoizedStringify(slice["filter"]),
filter: slice.filter.toString(),
})}`,
},
slice["count"]["value"] || 0,
@@ -67,7 +63,7 @@ function drawChart(chartData): Children {
paths.push(
m("path.stroke-white stroke-1", {
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",
{
"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 {
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 { m } from "./components.ts";
import config from "./config.ts";
import { pageSize as PAGE_SIZE } from "./config.ts";
import filterComponent from "./filter-component.ts";
import * as overlay from "./overlay.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 memoize from "../lib/common/memoize.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 memoizedParse = memoize((str) => Expression.parse(str));
const memoizedJsonParse = memoize(JSON.parse);
const attributes = [
@@ -27,10 +25,22 @@ const attributes = [
{ id: "provisionArgs", label: "Arguments", type: "textarea" },
];
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("presets", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -71,7 +81,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
if (object.precondition) {
try {
object.precondition = stringify(memoizedParse(object.precondition));
object.precondition = memoizedParse(object.precondition).toString();
} catch {
return void resolve({
precondition: "Precondition must be valid expression",
@@ -134,7 +144,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id;
return `api/presets.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
@@ -148,8 +158,11 @@ export function init(
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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 });
}
@@ -165,7 +178,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/presets", ops);
}
@@ -197,10 +212,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/presets", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const presets = store.fetch("presets", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,
@@ -220,7 +234,10 @@ export const component: ClosureComponent = (): Component => {
"instances",
]);
const provisions = store.fetch("provisions", true);
const provisions = store.fetch(
"provisions",
new Expression.Literal(true),
);
if (provisions.fulfilled) {
for (const p of provisions.value) {
userDefinedProvisions.add(p["_id"]);
+30 -17
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children } from "mithril";
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 * as store from "./store.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 * as overlay from "./overlay.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";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse);
const attributes = [
@@ -22,10 +19,22 @@ const attributes = [
{ id: "script", label: "Script", type: "code" },
];
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("provisions", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id;
return `api/provisions.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
@@ -118,8 +127,11 @@ export function init(
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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) => {
loadCodeMirror()
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/provisions", ops);
}
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/provisions", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const provisions = store.fetch("provisions", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,
+150 -72
View File
@@ -1,51 +1,103 @@
import config from "./config.ts";
import { Expression } from "../lib/types.ts";
import { filters } from "./config.ts";
import Expression from "../lib/common/expression.ts";
import { encodeTag } from "../lib/util.ts";
import Path from "../lib/common/path.ts";
const resources = {
devices: {},
faults: {
Device: { parameter: ["PARAM", "device"], type: "string" },
Channel: { parameter: ["PARAM", "channel"], type: "string" },
Code: { parameter: ["PARAM", "code"], type: "string" },
Retries: { parameter: ["PARAM", "retries"], type: "number" },
Timestamp: { parameter: ["PARAM", "timestamp"], type: "timestamp" },
Device: {
parameter: new Expression.Parameter(Path.parse("device")),
type: "string",
},
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: {
ID: { parameter: ["PARAM", "_id"], type: "string" },
Channel: { parameter: ["PARAM", "channel"], type: "string" },
Weight: { parameter: ["PARAM", "weight"], type: "number" },
ID: {
parameter: new Expression.Parameter(Path.parse("_id")),
type: "string",
},
Channel: {
parameter: new Expression.Parameter(Path.parse("channel")),
type: "string",
},
Weight: {
parameter: new Expression.Parameter(Path.parse("weight")),
type: "number",
},
},
provisions: {
ID: { parameter: ["PARAM", "_id"], type: "string" },
ID: {
parameter: new Expression.Parameter(Path.parse("_id")),
type: "string",
},
},
virtualParameters: {
ID: { parameter: ["PARAM", "_id"], type: "string" },
ID: {
parameter: new Expression.Parameter(Path.parse("_id")),
type: "string",
},
},
files: {
ID: { parameter: ["PARAM", "_id"], type: "string" },
Type: { parameter: ["PARAM", "metadata.fileType"], type: "string" },
OUI: { parameter: ["PARAM", "metadata.oui"], type: "string" },
ID: {
parameter: new Expression.Parameter(Path.parse("_id")),
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": {
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",
},
Version: { parameter: ["PARAM", "metadata.version"], type: "string" },
},
permissions: {
Role: { parameter: ["PARAM", "role"], type: "string" },
Resource: { parameter: ["PARAM", "resource"], type: "string" },
Access: { parameter: ["PARAM", "access"], type: "number" },
Role: {
parameter: new Expression.Parameter(Path.parse("role")),
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(
config.ui.filters as Record<
string,
{ label: string; parameter: string; type: string }
>,
)) {
for (const v of filters) {
resources.devices[v.label] = {
parameter: v.parameter,
type: (v.type || "").split(",").map((s) => s.trim()),
@@ -70,7 +122,7 @@ function queryNumber(param: Expression, value: string): Expression {
const v = parseInt(value);
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 {
@@ -86,52 +138,71 @@ function queryTimestamp(param: Expression, value: string): Expression {
let v = parseInt(value);
if (v !== +value) v = Date.parse(value);
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 {
return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()];
return new Expression.Binary(
"LIKE",
new Expression.FunctionCall("LOWER", [param]),
new Expression.Literal(value.toLowerCase()),
);
}
function queryStringCaseSensitive(
param: Expression,
value: string,
): Expression {
return ["LIKE", param, value];
return new Expression.Binary("LIKE", param, new Expression.Literal(value));
}
function queryStringMonoCase(param: Expression, value: string): Expression {
return [
"OR",
["LIKE", param, value.toLowerCase()],
["LIKE", param, value.toUpperCase()],
];
return Expression.or(
new Expression.Binary(
"LIKE",
param,
new Expression.Literal(value.toLowerCase()),
),
new Expression.Binary(
"LIKE",
param,
new Expression.Literal(value.toUpperCase()),
),
);
}
function queryMac(param: Expression, value: string): Expression {
value = value.replace(/[^a-f0-9]/gi, "").toLowerCase();
if (!value) return null;
if (value.length === 12) {
return [
"OR",
["=", param, value.replace(/(..)(?!$)/g, "$1:").toLowerCase()],
["=", param, value.replace(/(..)(?!$)/g, "$1:").toUpperCase()],
];
value = value.replace(/(..)(?!$)/g, "$1:");
return Expression.or(
new Expression.Binary(
"=",
param,
new Expression.Literal(value.toLowerCase()),
),
new Expression.Binary(
"=",
param,
new Expression.Literal(value.toUpperCase()),
),
);
}
return [
"OR",
[
param = new Expression.FunctionCall("LOWER", [param]);
return Expression.or(
new Expression.Binary(
"LIKE",
["FUNC", "LOWER", param],
`%${value.replace(/(..)(?!$)/g, "$1:")}%`,
],
[
param,
new Expression.Literal(`%${value.replace(/(..)(?!$)/g, "$1:")}%`),
),
new Expression.Binary(
"LIKE",
["FUNC", "LOWER", param],
`%${value.replace(/(.)(.)/g, "$1:$2")}%`,
],
];
param,
new Expression.Literal(`%${value.replace(/(.)(.)/g, "$1:$2")}%`),
),
);
}
function queryMacWildcard(param: Expression, value: string): Expression {
@@ -143,23 +214,32 @@ function queryMacWildcard(param: Expression, value: string): Expression {
p.replace(/(.)(.)/gi, "$1:$2"),
]);
const res = new Set();
const set = new Set<string>();
for (let i = 0; i < 2 ** groups.length; ++i) {
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;
res.add(r.toLocaleLowerCase());
res.add(r.toUpperCase());
set.add(r.toLocaleLowerCase());
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]);
if (clauses.length === 1) return clauses[0];
return ["OR", ...clauses];
let res: Expression = new Expression.Literal(false);
for (const s of set) {
res = Expression.or(
res,
new Expression.Binary("LIKE", param, new Expression.Literal(s)),
);
}
return res;
}
function queryTag(tag: string): Expression {
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 {
@@ -214,21 +294,21 @@ export function unpack(
if (!resources[resource]) return null;
const type = resources[resource][label].type;
value = value.trim();
const res: Expression = ["OR"];
let res: Expression = new Expression.Literal(false);
if (type.length === 0 || type.includes("number")) {
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")) {
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")) {
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")) {
@@ -236,30 +316,28 @@ export function unpack(
resources[resource][label].parameter,
value,
);
if (q) res.push(q);
if (q) res = Expression.or(res, q);
}
if (type.includes("string-monocase")) {
const q = queryStringMonoCase(resources[resource][label].parameter, value);
if (q) res.push(q);
if (q) res = Expression.or(res, q);
}
if (type.includes("mac")) {
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")) {
const q = queryMacWildcard(resources[resource][label].parameter, value);
if (q) res.push(q);
if (q) res = Expression.or(res, q);
}
if (type.includes("tag")) {
const q = queryTag(value);
if (q) res.push(q);
if (q) res = Expression.or(res, q);
}
if (res.length <= 1) return null;
else if (res.length === 2) return res[1];
else return res;
return res;
}
+85 -41
View File
@@ -1,8 +1,8 @@
import m from "mithril";
import { stringify } from "../lib/common/expression/parser.ts";
import { or, and, evaluate } from "../lib/common/expression/util.ts";
import Expression from "../lib/common/expression.ts";
import Path from "../lib/common/path.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 { configSnapshot, genieacsVersion } from "./config.ts";
import { QueueTask } from "./task-queue.ts";
@@ -14,7 +14,30 @@ import {
toBookmark,
} 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);
let fulfillTimestamp = 0;
@@ -59,7 +82,7 @@ for (const r of [
objects: new Map(),
count: 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 {
if (!Array.isArray(exp)) return exp;
const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
return e;
return memoizedEvaluate(exp, fulfillTimestamp + clockSkew);
}
export function count(resourceType: string, filter: Expression): QueryResponse {
const filterStr = memoizedStringify(filter);
const filterStr = filter.toString();
let queryResponse = resources[resourceType].count.get(filterStr);
if (queryResponse) return queryResponse;
@@ -271,7 +292,8 @@ function compareFunction(sort: {
function findMatches(resourceType, filter, sort, limit): any[] {
let value = [];
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));
if (limit) value = value.slice(0, limit);
@@ -284,7 +306,6 @@ export function fetch(
filter: Expression,
options: { limit?: number; sort?: { [param: string]: number } } = {},
): QueryResponse {
const filterStr = memoizedStringify(filter);
const sort = Object.assign({}, options.sort);
const limit = options.limit || 0;
@@ -292,7 +313,7 @@ export function fetch(
sort["DeviceID.ID"] = sort["DeviceID.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);
if (queryResponse) return queryResponse;
@@ -308,7 +329,10 @@ export function fetch(
);
const matches = findMatches(resourceType, satisfied, sort, limit);
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);
else queries.unsatisfied.set(queryResponse, diff);
return queryResponse;
@@ -336,7 +360,7 @@ export function fulfill(accessTimestamp: number): void {
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
filter: filter.toString(),
}),
extract: (xhr) => {
if (xhr.status === 403) throw new Error("Not authorized");
@@ -393,7 +417,7 @@ export function fulfill(accessTimestamp: number): void {
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
filter: filter.toString(),
limit: 1,
skip: limit - 1,
sort: JSON.stringify(sort),
@@ -419,16 +443,19 @@ export function fulfill(accessTimestamp: number): void {
let updated = false;
const allPromises2 = [];
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
let combinedFilter = null;
let combinedFilter = new Expression.Literal(false) as Expression;
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark)
filter = and(filter, bookmarkToExpression(bookmark, sort));
combinedFilter = or(combinedFilter, filter);
filter = Expression.and(
filter,
bookmarkToExpression(bookmark, sort),
);
combinedFilter = Expression.or(combinedFilter, filter);
}
const [union, diff] = unionDiff(
@@ -436,19 +463,18 @@ export function fulfill(accessTimestamp: number): void {
combinedFilter,
);
if (!diff) {
if (diff instanceof Expression.Literal && !diff.value) {
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(
filter,
null,
fulfillTimestamp + clockSkew,
);
filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark)
filter = and(filter, bookmarkToExpression(bookmark, sort));
filter = Expression.and(
filter,
bookmarkToExpression(bookmark, sort),
);
queries.value.set(
queryResponse,
@@ -462,7 +488,11 @@ export function fulfill(accessTimestamp: number): void {
}
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());
const combinedFilterDiff = diff;
@@ -474,15 +504,12 @@ export function fulfill(accessTimestamp: number): void {
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(combinedFilterDiff),
filter: combinedFilterDiff.toString(),
}),
background: false,
}).then((res) => {
for (const r of res as any[]) {
const id =
resourceType === "devices"
? r["DeviceID.ID"].value[0]
: r["_id"];
const id = r["DeviceID.ID"] ?? r["_id"];
resources[resourceType].objects.set(id, r);
deleted.delete(id);
}
@@ -490,7 +517,8 @@ export function fulfill(accessTimestamp: number): void {
for (const d of deleted) {
const obj = resources[resourceType].objects.get(d);
if (
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew)
evaluate(combinedFilterDiff, fulfillTimestamp + clockSkew, obj)
.value
)
resources[resourceType].objects.delete(d);
}
@@ -502,7 +530,10 @@ export function fulfill(accessTimestamp: number): void {
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark)
filter = and(filter, bookmarkToExpression(bookmark, sort));
filter = Expression.and(
filter,
bookmarkToExpression(bookmark, sort),
);
queries.value.set(
queryResponse,
@@ -530,7 +561,7 @@ export function setTimestamp(t: number): void {
if (t > fulfillTimestamp) {
fulfillTimestamp = t;
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[]> {
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({
method: "GET",
url: `api/config/?${m.buildQueryString({ filter: filter })}`,
url: `api/config/?${m.buildQueryString({ filter: filter.toString() })}`,
background: true,
});
}
export function resourceExists(resource: string, id: string): Promise<number> {
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({
method: "HEAD",
url:
`api/${resource}/?` +
m.buildQueryString({
filter: memoizedStringify(filter),
filter: filter.toString(),
}),
extract: (xhr) => {
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(
exp: Expression,
obj: Record<string, unknown>,
): Expression.Literal;
export function evaluateExpression(
exp: Expression,
obj?: Record<string, unknown>,
): Expression {
if (!Array.isArray(exp)) return exp;
return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
return memoizedEvaluate(exp, fulfillTimestamp + clockSkew, obj);
}
export function changePassword(
+2 -2
View File
@@ -4,7 +4,7 @@ import * as store from "./store.ts";
import { yaml } from "./dynamic-loader.ts";
import * as configFunctions from "./config-functions.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> {
return new Promise((resolve, reject) => {
@@ -27,7 +27,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
}
// 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
.queryConfig(`${prefix.join(".")}.%`)
+34 -18
View File
@@ -1,6 +1,6 @@
import { Children, ClosureComponent, Component } from "mithril";
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 notifications from "./notifications.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 * as overlay from "./overlay.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 changePasswordComponent from "./change-password-component.ts";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse);
const attributes = [
@@ -22,10 +19,22 @@ const attributes = [
{ id: "roles", label: "Roles", type: "multi", options: [] },
];
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("users", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -120,7 +129,7 @@ const getDownloadUrl = memoize((filter) => {
for (const attr of attributes) cols[attr.label] = attr.id;
return `api/users.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
@@ -134,8 +143,11 @@ export function init(
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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 });
}
@@ -151,7 +163,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/users", ops);
}
@@ -176,10 +190,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/users", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const users = store.fetch("users", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,
@@ -189,7 +202,10 @@ export const component: ClosureComponent = (): Component => {
const count = store.count("users", filter);
// Getting the roles
const permissions = store.fetch("permissions", true);
const permissions = store.fetch(
"permissions",
new Expression.Literal(true),
);
if (permissions.fulfilled) {
for (const attr of attributes) {
if (attr.id === "roles")
+30 -17
View File
@@ -1,6 +1,6 @@
import { ClosureComponent, Component, Children } from "mithril";
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 * as store from "./store.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 * as overlay from "./overlay.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";
const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse);
const attributes = [
@@ -22,10 +19,22 @@ const attributes = [
{ id: "script", label: "Script", type: "code" },
];
const unpackSmartQuery = memoize((query) => {
return map(query, (e) => {
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
return smartQuery.unpack("virtualParameters", e[2], e[3]);
const unpackSmartQuery = memoize((query: Expression) => {
return query.evaluate((e) => {
if (e instanceof Expression.FunctionCall) {
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;
});
});
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
const cols = {};
for (const attr of attributes) cols[attr.label] = attr.id;
return `api/virtualParameters.csv?${m.buildQueryString({
filter: stringify(filter),
filter: filter.toString(),
columns: JSON.stringify(cols),
})}`;
});
@@ -118,8 +127,11 @@ export function init(
);
}
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
let filter: Expression = null;
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) => {
loadCodeMirror()
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
}
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"];
m.route.set("/virtualParameters", ops);
}
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
m.route.set("/virtualParameters", ops);
}
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true;
filter = unpackSmartQuery(filter);
const filter = unpackSmartQuery(
vnode.attrs["filter"] ?? new Expression.Literal(true),
);
const virtualParameters = store.fetch("virtualParameters", filter, {
limit: vnode.state["showCount"] || PAGE_SIZE,