refactor: replace array-based expressions with typed class hierarchy
Replace the old Lisp-like array expression representation (["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in an abstract Expression class (Literal, Parameter, Binary, Unary, FunctionCall, Conditional). This gives compile-time type safety to expression construction, evaluation, and pattern matching. Key changes: - New lib/common/expression.ts defines the class hierarchy with map/evaluate/toString methods - Rewrite parser as hand-rolled recursive descent, removing the parsimmon dependency - Extend Path with a colon separator to unify parameter identity and attributes into a single address space (e.g. "Device.WiFi.SSID.1.Name:writable"). The colon field tracks how many trailing segments are attribute qualifiers. PathSet rewritten with separate param/attr segment indexes - Flatten device attribute representation in the UI to colon-delimited keys mirroring the new path syntax
This commit is contained in:
1 parent
75908e624f
commit
6f3ce6f7c8
71 files changed
+4045
-3449
No files matched your search
+47
-29
@@ -46,6 +46,7 @@ connect to MongoDB and start an HTTP(S) server.
|
|||||||
bin/ Service entry points (5 executables)
|
bin/ Service entry points (5 executables)
|
||||||
lib/ All backend logic
|
lib/ All backend logic
|
||||||
common/ Shared utilities (expression engine, Path, errors)
|
common/ Shared utilities (expression engine, Path, errors)
|
||||||
|
expression.ts Expression class hierarchy (base + subclasses)
|
||||||
expression/ Expression parser, evaluator, normalizer, minimizer
|
expression/ Expression parser, evaluator, normalizer, minimizer
|
||||||
cwmp/ CWMP-service-specific DB and caching
|
cwmp/ CWMP-service-specific DB and caching
|
||||||
db/ Database layer (MongoDB collections, query synthesis)
|
db/ Database layer (MongoDB collections, query synthesis)
|
||||||
@@ -78,15 +79,18 @@ MongoDB and start the HTTP server in each worker.
|
|||||||
by the extensions subsystem. It communicates with its parent via IPC,
|
by the extensions subsystem. It communicates with its parent via IPC,
|
||||||
executing user-defined extension scripts in isolation.
|
executing user-defined extension scripts in isolation.
|
||||||
|
|
||||||
### The Expression System (`lib/common/expression/`)
|
### The Expression System (`lib/common/expression.ts`, `lib/common/expression/`)
|
||||||
|
|
||||||
The `Expression` type (`string | number | boolean | null | any[]`) is the most
|
The `Expression` class (defined in `lib/common/expression.ts`) is the most
|
||||||
important abstraction in the codebase. It is a recursive, Lisp-like S-expression
|
important abstraction in the codebase. It uses a typed class hierarchy:
|
||||||
where scalars are literal values and arrays are operations (the operator at
|
`Expression.Literal` (wraps `string | number | boolean | null`),
|
||||||
index 0, operands following). For example:
|
`Expression.Parameter` (wraps a `Path`), `Expression.Binary` (operator +
|
||||||
|
left/right), `Expression.Unary` (operator + operand), `Expression.FunctionCall`
|
||||||
|
(name + args), and `Expression.Conditional` (condition/then/otherwise). For
|
||||||
|
example, in the SQL-like text syntax:
|
||||||
|
|
||||||
```
|
```
|
||||||
["AND", ["=", ["PARAM", "Device.ModelName"], "BrandX"], [">", ["PARAM", "Events.Inform"], 1000]]
|
Device.ModelName = "BrandX" AND Events.Inform > 1000
|
||||||
```
|
```
|
||||||
|
|
||||||
Expressions are used pervasively:
|
Expressions are used pervasively:
|
||||||
@@ -101,23 +105,28 @@ Expressions are used pervasively:
|
|||||||
|
|
||||||
The expression pipeline flows through four modules:
|
The expression pipeline flows through four modules:
|
||||||
|
|
||||||
1. `parser.ts` -- Parses a SQL-like text syntax into the AST (uses Parsimmon).
|
1. `parser.ts` -- Parses a SQL-like text syntax into the AST using a hand-rolled
|
||||||
Also provides `map()` / `mapAsync()` tree-walking primitives and
|
recursive descent parser with a `Cursor`-based scanner. Also provides
|
||||||
`stringify()` for serialization.
|
`stringifyExpression()` for serialization. The `map()` / `mapAsync()`
|
||||||
|
tree-walking primitives are abstract methods on the `Expression` class, and
|
||||||
|
`stringify()` is now `Expression.toString()`.
|
||||||
2. `normalize.ts` -- Algebraic normalization using exact rational polynomial
|
2. `normalize.ts` -- Algebraic normalization using exact rational polynomial
|
||||||
arithmetic over bigints. Ensures equivalent expressions have the same
|
arithmetic over native `bigint` values. The `Polynomial` class extends
|
||||||
canonical form (e.g., `a + 2 > b` and `a > b - 2` normalize identically).
|
`Expression` as an intermediate representation. Ensures equivalent
|
||||||
|
expressions have the same canonical form (e.g., `a + 2 > b` and `a > b - 2`
|
||||||
|
normalize identically).
|
||||||
3. `synth.ts` -- Boolean logic minimization via the Espresso algorithm
|
3. `synth.ts` -- Boolean logic minimization via the Espresso algorithm
|
||||||
(espresso-iisojs). Converts expressions to minimal sum-of-products form with
|
(espresso-iisojs). Converts expressions to minimal sum-of-products form with
|
||||||
three-valued logic (true/false/null). Handles domain-specific constraints
|
three-valued logic (true/false/null). Handles domain-specific constraints
|
||||||
like comparison ordering and LIKE pattern relationships.
|
like comparison ordering and LIKE pattern relationships.
|
||||||
4. `util.ts` -- Runtime evaluation. Resolves `PARAM` references against concrete
|
4. `evaluate.ts` -- Runtime evaluation. The `reduce()` function evaluates
|
||||||
data, evaluates operators, and supports partial evaluation (returns a reduced
|
operators on literal values and supports partial evaluation (returns a
|
||||||
expression if some values are unknown).
|
reduced expression if some values are unknown). Parameter resolution is
|
||||||
|
handled by the `Expression.evaluate()` method (in `lib/common/expression.ts`)
|
||||||
|
which calls `reduce()` after mapping children via a user-supplied callback.
|
||||||
|
|
||||||
`bigint.ts` provides a named-function wrapper around native bigint operations
|
`pagination.ts` implements cursor-based pagination by generating filter
|
||||||
for readability. `pagination.ts` implements cursor-based pagination by
|
expressions from sort-key bookmarks.
|
||||||
generating filter expressions from sort-key bookmarks.
|
|
||||||
|
|
||||||
### The Path System (`lib/common/path.ts`, `lib/common/path-set.ts`)
|
### The Path System (`lib/common/path.ts`, `lib/common/path-set.ts`)
|
||||||
|
|
||||||
@@ -127,17 +136,22 @@ query-based addressing.
|
|||||||
|
|
||||||
Key design decisions:
|
Key design decisions:
|
||||||
|
|
||||||
- **Interned/flyweight** -- All `Path` instances are cached. `Path.parse()` is
|
- **Cached** -- `Path.parse()` caches instances in a two-generation LRU cache
|
||||||
the only way to create them; the constructor is protected. Two-generation LRU
|
rotated every 120 seconds. The constructor is public (used directly by the
|
||||||
cache rotated every 120 seconds.
|
parser and by methods like `slice()`, `concat()`, and `stripAlias()`).
|
||||||
- **Bitmask encoding** -- The `wildcard` and `alias` fields are bitfields for
|
- **Bitmask encoding** -- The `wildcard` and `alias` fields are bitfields for
|
||||||
O(1) segment-type checking. This limits paths to 32 segments.
|
O(1) segment-type checking. This limits paths to 32 segments. A `colon` field
|
||||||
|
tracks the number of attribute path segments (after a `:` separator), enabling
|
||||||
|
`paramLength` and `attrLength` accessors.
|
||||||
- **Immutable** -- Segment arrays are `Object.freeze()`-d.
|
- **Immutable** -- Segment arrays are `Object.freeze()`-d.
|
||||||
|
|
||||||
`PathSet` is a multi-indexed collection of paths supporting pattern-matching
|
`PathSet` is a multi-indexed collection of paths supporting pattern-matching
|
||||||
queries. It indexes paths by length, by segment-at-position, and by string
|
queries. It maintains separate `paramSegmentIndex` and `attrSegmentIndex` arrays
|
||||||
representation. The `find()` method uses set intersection to efficiently match
|
(one `Map<string, Set<Path>>` per position), plus a `stringIndex` map. The
|
||||||
wildcard patterns.
|
`find()` method takes bitmasks to control which segments require exact matches
|
||||||
|
vs. wildcard compatibility, then uses set intersection across the smallest index
|
||||||
|
sets. A higher-level `findCompat()` method computes the appropriate bitmasks for
|
||||||
|
superset/subset matching.
|
||||||
|
|
||||||
### CWMP Protocol Layer (`lib/cwmp.ts`, `lib/soap.ts`, `lib/xml-parser.ts`)
|
### CWMP Protocol Layer (`lib/cwmp.ts`, `lib/soap.ts`, `lib/xml-parser.ts`)
|
||||||
|
|
||||||
@@ -272,13 +286,17 @@ and CSV export.
|
|||||||
`lib/ui/db.ts` translates between the UI's flat parameter representation and
|
`lib/ui/db.ts` translates between the UI's flat parameter representation and
|
||||||
MongoDB's nested document structure. The `flattenDevice()` function is the key
|
MongoDB's nested document structure. The `flattenDevice()` function is the key
|
||||||
transformation: it recursively walks the nested device document and produces a
|
transformation: it recursively walks the nested device document and produces a
|
||||||
flat key-value map (e.g.,
|
flat key-value map with colon-delimited attribute keys (e.g.,
|
||||||
`Device.WiFi.SSID.1.Name -> {value: [...], writable: true}`).
|
`"Device.WiFi.SSID.1.Name" -> value`,
|
||||||
|
`"Device.WiFi.SSID.1.Name:type" -> "xsd:string"`,
|
||||||
|
`"Device.WiFi.SSID.1.Name:writable" -> true`). The `FlatDevice` type is
|
||||||
|
`Record<string, Value>` where `Value = string | number | boolean | null`.
|
||||||
|
|
||||||
`lib/ui/local-cache.ts` caches permissions, users, and config in-process with
|
`lib/ui/local-cache.ts` caches permissions, users, and config in-process with
|
||||||
hash-based revision tracking. The `getConfig()` function resolves config
|
hash-based revision tracking. The `getConfig()` function uses typed overloads --
|
||||||
expressions with dynamic context and backward compatibility for legacy config
|
it takes a typed default value (`string`, `number`, or `boolean`) and an
|
||||||
keys.
|
expression evaluation callback, returning a value of the same type as the
|
||||||
|
default.
|
||||||
|
|
||||||
### Database Layer (`lib/db/`)
|
### Database Layer (`lib/db/`)
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -107,13 +107,7 @@ const packageDotJsonPlugin = {
|
|||||||
const inlineDepsPlugin = {
|
const inlineDepsPlugin = {
|
||||||
name: "inlineDeps",
|
name: "inlineDeps",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
const deps = [
|
const deps = ["espresso-iisojs", "@codemirror", "mithril", "yaml"];
|
||||||
"parsimmon",
|
|
||||||
"espresso-iisojs",
|
|
||||||
"@codemirror",
|
|
||||||
"mithril",
|
|
||||||
"yaml",
|
|
||||||
];
|
|
||||||
const depFiles = new Set();
|
const depFiles = new Set();
|
||||||
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
||||||
if (args.pluginData === "inlineDeps") return undefined;
|
if (args.pluginData === "inlineDeps") return undefined;
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ export default defineConfig(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-shadow": ["error", { allow: ["err"] }],
|
"@typescript-eslint/no-shadow": ["error", { allow: ["err"] }],
|
||||||
"handle-callback-err": "error",
|
"handle-callback-err": "error",
|
||||||
"prefer-arrow-callback": "error",
|
"prefer-arrow-callback": "error",
|
||||||
"no-buffer-constructor": "error",
|
"no-buffer-constructor": "error",
|
||||||
|
|||||||
+65
-74
@@ -31,8 +31,8 @@ import {
|
|||||||
udpConnectionRequest,
|
udpConnectionRequest,
|
||||||
xmppConnectionRequest,
|
xmppConnectionRequest,
|
||||||
} from "./connection-request.ts";
|
} from "./connection-request.ts";
|
||||||
import { Expression, Task } from "./types.ts";
|
import { Task } from "./types.ts";
|
||||||
import { evaluate } from "./common/expression/util.ts";
|
import Expression, { Value } from "./common/expression.ts";
|
||||||
import { hashPassword } from "./auth.ts";
|
import { hashPassword } from "./auth.ts";
|
||||||
import { flattenDevice } from "./ui/db.ts";
|
import { flattenDevice } from "./ui/db.ts";
|
||||||
import { ResourceLockedError } from "./common/errors.ts";
|
import { ResourceLockedError } from "./common/errors.ts";
|
||||||
@@ -42,7 +42,7 @@ const XMPP_CONFIGURED = !!config.get("XMPP_JID");
|
|||||||
|
|
||||||
export async function connectionRequest(
|
export async function connectionRequest(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
device?: Record<string, { value?: [boolean | number | string, string] }>,
|
device?: Record<string, Value>,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!device) {
|
if (!device) {
|
||||||
const res = await collections.devices.findOne({ _id: deviceId });
|
const res = await collections.devices.findOne({ _id: deviceId });
|
||||||
@@ -58,47 +58,36 @@ export async function connectionRequest(
|
|||||||
password;
|
password;
|
||||||
|
|
||||||
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
|
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
|
||||||
connectionRequestUrl = (device[
|
connectionRequestUrl =
|
||||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
|
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"] ||
|
||||||
].value || [""])[0];
|
"";
|
||||||
udpConnectionRequestAddress = ((
|
udpConnectionRequestAddress =
|
||||||
device[
|
device[
|
||||||
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
|
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
|
||||||
] || {}
|
] || "";
|
||||||
).value || [""])[0];
|
stunEnable =
|
||||||
stunEnable = ((
|
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || "";
|
||||||
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || {}
|
connReqJabberId =
|
||||||
).value || [""])[0];
|
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || "";
|
||||||
connReqJabberId = ((
|
username =
|
||||||
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
username = ((
|
|
||||||
device[
|
device[
|
||||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
|
||||||
] || {}
|
] || "";
|
||||||
).value || [""])[0];
|
password =
|
||||||
password = ((
|
|
||||||
device[
|
device[
|
||||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
|
||||||
] || {}
|
] || "";
|
||||||
).value || [""])[0];
|
|
||||||
} else {
|
} else {
|
||||||
connectionRequestUrl = (device[
|
connectionRequestUrl =
|
||||||
"Device.ManagementServer.ConnectionRequestURL"
|
device["Device.ManagementServer.ConnectionRequestURL"] || "";
|
||||||
].value || [""])[0];
|
udpConnectionRequestAddress =
|
||||||
udpConnectionRequestAddress = ((
|
device["Device.ManagementServer.UDPConnectionRequestAddress"] || "";
|
||||||
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
|
stunEnable = device["Device.ManagementServer.STUNEnable"] || "";
|
||||||
).value || [""])[0];
|
connReqJabberId = device["Device.ManagementServer.ConnReqJabberID"] || "";
|
||||||
stunEnable = ((device["Device.ManagementServer.STUNEnable"] || {})
|
username =
|
||||||
.value || [""])[0];
|
device["Device.ManagementServer.ConnectionRequestUsername"] || "";
|
||||||
connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {})
|
password =
|
||||||
.value || [""])[0];
|
device["Device.ManagementServer.ConnectionRequestPassword"] || "";
|
||||||
username = ((
|
|
||||||
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
password = ((
|
|
||||||
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
}
|
}
|
||||||
let remoteAddress;
|
let remoteAddress;
|
||||||
try {
|
try {
|
||||||
@@ -107,68 +96,70 @@ export async function connectionRequest(
|
|||||||
return "Invalid connection request URL";
|
return "Invalid connection request URL";
|
||||||
}
|
}
|
||||||
|
|
||||||
const evalCallback = (exp): Expression => {
|
const snapshot = await getRevision();
|
||||||
if (!Array.isArray(exp)) return exp;
|
const now = Date.now();
|
||||||
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
|
|
||||||
let name = exp[1];
|
const evalCallback = (exp: Expression): Expression => {
|
||||||
|
if (exp instanceof Expression.Parameter) {
|
||||||
|
let name = exp.path.toString();
|
||||||
if (name === "id") name = "DeviceID.ID";
|
if (name === "id") name = "DeviceID.ID";
|
||||||
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
|
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
|
||||||
else if (name === "productClass") name = "DeviceID.ProductClass";
|
else if (name === "productClass") name = "DeviceID.ProductClass";
|
||||||
else if (name === "oui") name = "DeviceID.OUI";
|
else if (name === "oui") name = "DeviceID.OUI";
|
||||||
else if (name === "remoteAddress") return remoteAddress;
|
else if (name === "remoteAddress")
|
||||||
else if (name === "username") return username;
|
return new Expression.Literal(remoteAddress);
|
||||||
else if (name === "password") return password;
|
else if (name === "username") return new Expression.Literal(username);
|
||||||
|
else if (name === "password") return new Expression.Literal(password);
|
||||||
const p = device[name];
|
return new Expression.Literal(device[name] ?? null);
|
||||||
if (p?.value) return p.value[0];
|
} else if (exp instanceof Expression.FunctionCall) {
|
||||||
} else if (exp[0] === "FUNC") {
|
if (exp.name === "NOW") return new Expression.Literal(now);
|
||||||
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
|
else if (exp.name === "REMOTE_ADDRESS")
|
||||||
else if (exp[1] === "USERNAME") return username;
|
return new Expression.Literal(remoteAddress);
|
||||||
else if (exp[1] === "PASSWORD") return password;
|
else if (exp.name === "USERNAME") return new Expression.Literal(username);
|
||||||
|
else if (exp.name === "PASSWORD") return new Expression.Literal(password);
|
||||||
}
|
}
|
||||||
return exp;
|
return exp;
|
||||||
};
|
};
|
||||||
|
|
||||||
const snapshot = await getRevision();
|
const configCallback = (exp: Expression): Expression.Literal => {
|
||||||
const now = Date.now();
|
const e = evalCallback(exp);
|
||||||
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
|
if (e instanceof Expression.Literal) return e;
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UDP_CONNECTION_REQUEST_PORT = getConfig(
|
||||||
snapshot,
|
snapshot,
|
||||||
"cwmp.udpConnectionRequestPort",
|
"cwmp.udpConnectionRequestPort",
|
||||||
{},
|
0,
|
||||||
now,
|
configCallback,
|
||||||
evalCallback,
|
|
||||||
);
|
);
|
||||||
const CONNECTION_REQUEST_TIMEOUT = +getConfig(
|
const CONNECTION_REQUEST_TIMEOUT = getConfig(
|
||||||
snapshot,
|
snapshot,
|
||||||
"cwmp.connectionRequestTimeout",
|
"cwmp.connectionRequestTimeout",
|
||||||
{},
|
2000,
|
||||||
now,
|
configCallback,
|
||||||
evalCallback,
|
|
||||||
);
|
);
|
||||||
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
|
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = getConfig(
|
||||||
snapshot,
|
snapshot,
|
||||||
"cwmp.connectionRequestAllowBasicAuth",
|
"cwmp.connectionRequestAllowBasicAuth",
|
||||||
{},
|
false,
|
||||||
now,
|
configCallback,
|
||||||
evalCallback,
|
|
||||||
);
|
);
|
||||||
let authExp: Expression = getConfigExpression(
|
let authExp: Expression = getConfigExpression(
|
||||||
snapshot,
|
snapshot,
|
||||||
"cwmp.connectionRequestAuth",
|
"cwmp.connectionRequestAuth",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (authExp === undefined) {
|
if (!authExp) {
|
||||||
authExp = [
|
authExp = new Expression.FunctionCall("AUTH", [
|
||||||
"FUNC",
|
new Expression.FunctionCall("USERNAME", []),
|
||||||
"AUTH",
|
new Expression.FunctionCall("PASSWORD", []),
|
||||||
["PARAM", "username"],
|
]);
|
||||||
["PARAM", "password"],
|
|
||||||
] as Expression;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authExp = evaluate(authExp, {}, now, evalCallback);
|
authExp = authExp.evaluate(evalCallback);
|
||||||
|
|
||||||
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
const debug = getConfig(snapshot, "cwmp.debug", false, configCallback);
|
||||||
|
|
||||||
let udpProm = Promise.resolve(false);
|
let udpProm = Promise.resolve(false);
|
||||||
if (udpConnectionRequestAddress && +stunEnable) {
|
if (udpConnectionRequestAddress && +stunEnable) {
|
||||||
|
|||||||
+30
-30
@@ -1,5 +1,5 @@
|
|||||||
import { PermissionSet, Expression } from "../types.ts";
|
import { PermissionSet } from "../types.ts";
|
||||||
import { evaluate, or } from "./expression/util.ts";
|
import Expression from "./expression.ts";
|
||||||
|
|
||||||
export default class Authorizer {
|
export default class Authorizer {
|
||||||
declare private permissionSets: PermissionSet[];
|
declare private permissionSets: PermissionSet[];
|
||||||
@@ -43,12 +43,12 @@ export default class Authorizer {
|
|||||||
if (this.getFilterCache.has(cacheKey))
|
if (this.getFilterCache.has(cacheKey))
|
||||||
return this.getFilterCache.get(cacheKey);
|
return this.getFilterCache.get(cacheKey);
|
||||||
|
|
||||||
let filter: Expression = null;
|
let filter: Expression = new Expression.Literal(false);
|
||||||
for (const permissionSet of this.permissionSets) {
|
for (const permissionSet of this.permissionSets) {
|
||||||
for (const perm of permissionSet) {
|
for (const perm of permissionSet) {
|
||||||
if (perm[resourceType]) {
|
if (perm[resourceType]) {
|
||||||
if (perm[resourceType].access >= access)
|
if (perm[resourceType].access >= access)
|
||||||
filter = or(filter, perm[resourceType].filter);
|
filter = Expression.or(filter, perm[resourceType].filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export default class Authorizer {
|
|||||||
if (this.validatorCache.has(resource))
|
if (this.validatorCache.has(resource))
|
||||||
return this.validatorCache.get(resource);
|
return this.validatorCache.get(resource);
|
||||||
|
|
||||||
const validators: Expression[] = [];
|
let validators: Expression = new Expression.Literal(false);
|
||||||
|
|
||||||
for (const permissionSet of this.permissionSets) {
|
for (const permissionSet of this.permissionSets) {
|
||||||
for (const perm of permissionSet) {
|
for (const perm of permissionSet) {
|
||||||
@@ -73,7 +73,7 @@ export default class Authorizer {
|
|||||||
perm[resourceType].access >= 3 &&
|
perm[resourceType].access >= 3 &&
|
||||||
perm[resourceType].validate
|
perm[resourceType].validate
|
||||||
)
|
)
|
||||||
validators.push(perm[resourceType].validate);
|
validators = Expression.or(validators, perm[resourceType].validate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +82,6 @@ export default class Authorizer {
|
|||||||
mutation: any,
|
mutation: any,
|
||||||
any: any,
|
any: any,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!validators.length) return false;
|
|
||||||
|
|
||||||
const object = {
|
const object = {
|
||||||
mutationType,
|
mutationType,
|
||||||
mutation,
|
mutation,
|
||||||
@@ -92,31 +90,33 @@ export default class Authorizer {
|
|||||||
options: any,
|
options: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const valueFunction = (paramName): any => {
|
const now = Date.now();
|
||||||
const entry = paramName.split(".", 1)[0];
|
const res = validators.evaluate((exp) => {
|
||||||
paramName = paramName.slice(entry.length + 1);
|
if (exp instanceof Expression.Literal) return exp;
|
||||||
let value = null;
|
if (exp instanceof Expression.Parameter) {
|
||||||
if (["mutation", "options"].includes(entry)) {
|
if (exp.path.colon) return new Expression.Literal(null);
|
||||||
value = object[entry];
|
const entry = exp.path.segments[0] as string;
|
||||||
for (const seg of paramName.split(".")) {
|
const paramName = exp.path.slice(1);
|
||||||
if (value == null) break;
|
let value = null;
|
||||||
if (typeof value !== "object") value = null;
|
if (["mutation", "options"].includes(entry)) {
|
||||||
else value = value[seg];
|
value = object[entry];
|
||||||
|
for (const seg of paramName.segments) {
|
||||||
|
if (value == null) break;
|
||||||
|
if (typeof value !== "object") value = null;
|
||||||
|
else value = value[seg as string];
|
||||||
|
}
|
||||||
|
} else if (object[entry]) {
|
||||||
|
if (paramName.length) value = object[entry][paramName.toString()];
|
||||||
|
else value = object[entry];
|
||||||
}
|
}
|
||||||
} else if (object[entry]) {
|
return new Expression.Literal(value);
|
||||||
if (paramName) value = object[entry][paramName];
|
} else if (exp instanceof Expression.FunctionCall) {
|
||||||
else value = object[entry];
|
if (exp.name === "NOW") return new Expression.Literal(now);
|
||||||
}
|
}
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
}).value;
|
||||||
|
|
||||||
return value;
|
return !!res;
|
||||||
};
|
|
||||||
|
|
||||||
const res = evaluate(
|
|
||||||
validators.length > 1 ? ["OR", validators] : validators[0],
|
|
||||||
valueFunction,
|
|
||||||
Date.now(),
|
|
||||||
);
|
|
||||||
return !Array.isArray(res) && !!res;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.validatorCache.set(resource, validator);
|
this.validatorCache.set(resource, validator);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 };
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import Expression from "../expression.ts";
|
||||||
|
import { likePatternToRegExp } from "./parser.ts";
|
||||||
|
|
||||||
|
function compare(
|
||||||
|
a: boolean | number | string,
|
||||||
|
b: boolean | number | string,
|
||||||
|
): number {
|
||||||
|
if (typeof a === "boolean") a = +a;
|
||||||
|
if (typeof b === "boolean") b = +b;
|
||||||
|
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
|
||||||
|
return a > b ? 1 : a < b ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(a: boolean | number | string): number {
|
||||||
|
switch (typeof a) {
|
||||||
|
case "number":
|
||||||
|
return a;
|
||||||
|
case "boolean":
|
||||||
|
return +a;
|
||||||
|
case "string":
|
||||||
|
return parseFloat(a) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(a: boolean | number | string): string {
|
||||||
|
switch (typeof a) {
|
||||||
|
case "string":
|
||||||
|
return a;
|
||||||
|
case "number":
|
||||||
|
return a.toString();
|
||||||
|
case "boolean":
|
||||||
|
return (+a).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const regExpCache: WeakMap<Expression.Literal, RegExp> = new WeakMap();
|
||||||
|
|
||||||
|
export function reduce(exp: Expression): Expression {
|
||||||
|
if (exp instanceof Expression.Literal) return exp;
|
||||||
|
|
||||||
|
if (exp instanceof Expression.Unary) {
|
||||||
|
if (exp.operator === "NOT") {
|
||||||
|
if (exp.operand instanceof Expression.Literal) {
|
||||||
|
if (exp.operand.value == null) return exp.operand;
|
||||||
|
return new Expression.Literal(!exp.operand.value);
|
||||||
|
} else if (exp.operand instanceof Expression.Unary) {
|
||||||
|
if (exp.operand.operator === "NOT") return exp.operand.operand;
|
||||||
|
}
|
||||||
|
} else if (exp.operator === "IS NULL") {
|
||||||
|
if (exp.operand instanceof Expression.Literal) {
|
||||||
|
return new Expression.Literal(exp.operand.value == null);
|
||||||
|
}
|
||||||
|
} else if (exp.operator === "IS NOT NULL") {
|
||||||
|
if (exp.operand instanceof Expression.Literal) {
|
||||||
|
return new Expression.Literal(exp.operand.value != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (exp instanceof Expression.Binary) {
|
||||||
|
if (exp.operator === "AND") {
|
||||||
|
return Expression.and(exp.left, exp.right);
|
||||||
|
} else if (exp.operator === "OR") {
|
||||||
|
return Expression.or(exp.left, exp.right);
|
||||||
|
} else if (["=", ">", "<", "<>", ">=", "<="].includes(exp.operator)) {
|
||||||
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
||||||
|
return exp.left;
|
||||||
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
||||||
|
return exp.right;
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Literal &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const c = compare(exp.left.value, exp.right.value);
|
||||||
|
switch (exp.operator) {
|
||||||
|
case "=":
|
||||||
|
return new Expression.Literal(c === 0);
|
||||||
|
case ">":
|
||||||
|
return new Expression.Literal(c > 0);
|
||||||
|
case "<":
|
||||||
|
return new Expression.Literal(c < 0);
|
||||||
|
case "<>":
|
||||||
|
return new Expression.Literal(c !== 0);
|
||||||
|
case ">=":
|
||||||
|
return new Expression.Literal(c >= 0);
|
||||||
|
case "<=":
|
||||||
|
return new Expression.Literal(c <= 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (["+", "-", "*", "/"].includes(exp.operator)) {
|
||||||
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
||||||
|
return exp.left;
|
||||||
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
||||||
|
return exp.right;
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Literal &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const a = toNumber(exp.left.value);
|
||||||
|
const b = toNumber(exp.right.value);
|
||||||
|
switch (exp.operator) {
|
||||||
|
case "+":
|
||||||
|
return new Expression.Literal(a + b);
|
||||||
|
case "-":
|
||||||
|
return new Expression.Literal(a - b);
|
||||||
|
case "*":
|
||||||
|
return new Expression.Literal(a * b);
|
||||||
|
case "/":
|
||||||
|
return new Expression.Literal(a / b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (exp.operator === "%") {
|
||||||
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
||||||
|
return exp.left;
|
||||||
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
||||||
|
return exp.right;
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Literal &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const a = toNumber(exp.left.value);
|
||||||
|
const b = Math.trunc(toNumber(exp.right.value));
|
||||||
|
if (b === 0) return new Expression.Literal(null);
|
||||||
|
return new Expression.Literal(a % b);
|
||||||
|
}
|
||||||
|
} else if (exp.operator === "||") {
|
||||||
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
||||||
|
return exp.left;
|
||||||
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
||||||
|
return exp.right;
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Literal &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const a = toString(exp.left.value);
|
||||||
|
const b = toString(exp.right.value);
|
||||||
|
return new Expression.Literal(a + b);
|
||||||
|
}
|
||||||
|
} else if (exp.operator === "LIKE") {
|
||||||
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
||||||
|
return exp.left;
|
||||||
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
||||||
|
return exp.right;
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Literal &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const s = toString(exp.left.value);
|
||||||
|
let r = regExpCache.get(exp.right);
|
||||||
|
if (!r) {
|
||||||
|
r = likePatternToRegExp(toString(exp.right.value));
|
||||||
|
regExpCache.set(exp.right, r);
|
||||||
|
}
|
||||||
|
return new Expression.Literal(r.test(s));
|
||||||
|
}
|
||||||
|
} else if (exp.operator === "NOT LIKE") {
|
||||||
|
if (exp.left instanceof Expression.Literal && exp.left.value == null)
|
||||||
|
return exp.left;
|
||||||
|
if (exp.right instanceof Expression.Literal && exp.right.value == null)
|
||||||
|
return exp.right;
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Literal &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const s = toString(exp.left.value);
|
||||||
|
let r = regExpCache.get(exp.right);
|
||||||
|
if (!r) {
|
||||||
|
r = likePatternToRegExp(toString(exp.right.value));
|
||||||
|
regExpCache.set(exp.right, r);
|
||||||
|
}
|
||||||
|
return new Expression.Literal(!r.test(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (exp instanceof Expression.FunctionCall) {
|
||||||
|
if (exp.name === "COALESCE") {
|
||||||
|
const args = exp.args.filter(
|
||||||
|
(arg) => !(arg instanceof Expression.Literal && arg.value == null),
|
||||||
|
);
|
||||||
|
if (!args.length) return new Expression.Literal(null);
|
||||||
|
if (args.length === 1) return args[0];
|
||||||
|
if (args[0] instanceof Expression.Literal) return args[0];
|
||||||
|
if (args.length !== exp.args.length)
|
||||||
|
return new Expression.FunctionCall("COALESCE", args);
|
||||||
|
} else if (exp.name === "UPPER") {
|
||||||
|
if (exp.args[0] instanceof Expression.Literal) {
|
||||||
|
if (exp.args[0].value == null) return exp.args[0];
|
||||||
|
const a = toString(exp.args[0].value);
|
||||||
|
return new Expression.Literal(a.toUpperCase());
|
||||||
|
}
|
||||||
|
} else if (exp.name === "LOWER") {
|
||||||
|
if (exp.args[0] instanceof Expression.Literal) {
|
||||||
|
if (exp.args[0].value == null) return exp.args[0];
|
||||||
|
const a = toString(exp.args[0].value);
|
||||||
|
return new Expression.Literal(a.toLowerCase());
|
||||||
|
}
|
||||||
|
} else if (exp.name === "ROUND") {
|
||||||
|
let p = 0;
|
||||||
|
if (exp.args.length > 1) {
|
||||||
|
if (exp.args[1] instanceof Expression.Literal) {
|
||||||
|
if (exp.args[1].value == null) return exp.args[1];
|
||||||
|
p = Math.trunc(toNumber(exp.args[1].value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exp.args[0] instanceof Expression.Literal) {
|
||||||
|
if (exp.args[0].value == null) return exp.args[0];
|
||||||
|
const n = toNumber(exp.args[0].value);
|
||||||
|
const d = 10 ** p;
|
||||||
|
const m = n * d * (1 + Number.EPSILON);
|
||||||
|
return new Expression.Literal(Math.round(m) / d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (exp instanceof Expression.Conditional) {
|
||||||
|
if (exp.condition instanceof Expression.Literal) {
|
||||||
|
if (exp.condition.value) return exp.then;
|
||||||
|
return exp.otherwise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exp;
|
||||||
|
}
|
||||||
+229
-231
@@ -1,22 +1,15 @@
|
|||||||
import * as BI from "./bigint.ts";
|
import Expression from "../expression.ts";
|
||||||
import { Expression } from "../../types.ts";
|
import { reduce } from "./evaluate.ts";
|
||||||
import { map } from "./parser.ts";
|
|
||||||
import { and, evaluateCallback } from "./util.ts";
|
|
||||||
|
|
||||||
const ZERO = BI.BigInt(0);
|
|
||||||
const ONE = BI.BigInt(1);
|
|
||||||
const TWO = BI.BigInt(2);
|
|
||||||
const NEGATIVE_ONE = BI.BigInt(-1);
|
|
||||||
|
|
||||||
class Indeterminates {
|
class Indeterminates {
|
||||||
declare public map: Map<string, number>;
|
declare public map: Map<Expression, number>;
|
||||||
declare public sortedKeys: string[];
|
declare public sortedKeys: Expression[];
|
||||||
|
|
||||||
public constructor(str?: string) {
|
public constructor(exp?: Expression) {
|
||||||
this.map = new Map();
|
this.map = new Map();
|
||||||
if (str) {
|
if (exp) {
|
||||||
this.map.set(str, 1);
|
this.map.set(exp, 1);
|
||||||
this.sortedKeys = [str];
|
this.sortedKeys = [exp];
|
||||||
} else {
|
} else {
|
||||||
this.sortedKeys = [];
|
this.sortedKeys = [];
|
||||||
}
|
}
|
||||||
@@ -37,27 +30,31 @@ class Indeterminates {
|
|||||||
const res = new Indeterminates();
|
const res = new Indeterminates();
|
||||||
res.sortedKeys = indeterminates1.sortedKeys.slice();
|
res.sortedKeys = indeterminates1.sortedKeys.slice();
|
||||||
res.map = new Map(indeterminates1.map);
|
res.map = new Map(indeterminates1.map);
|
||||||
|
const strMap: Map<string, Expression> = new Map();
|
||||||
|
for (const k of res.map.keys()) strMap.set(k.toString(), k);
|
||||||
|
|
||||||
for (const [key, val] of indeterminates2.map) {
|
for (const [key, val] of indeterminates2.map) {
|
||||||
const v = res.map.get(key);
|
const k = strMap.get(key.toString());
|
||||||
if (!v) {
|
if (!k) {
|
||||||
res.map.set(key, val);
|
res.map.set(key, val);
|
||||||
res.sortedKeys.push(key);
|
res.sortedKeys.push(key);
|
||||||
} else {
|
} else {
|
||||||
const v2 = val + v;
|
const v2 = val + res.map.get(k);
|
||||||
if (!v2) {
|
if (!v2) {
|
||||||
res.map.delete(key);
|
res.map.delete(k);
|
||||||
res.sortedKeys = res.sortedKeys.filter((s) => s !== key);
|
res.sortedKeys = res.sortedKeys.filter((s) => s !== k);
|
||||||
} else {
|
} else {
|
||||||
res.map.set(key, v2);
|
res.map.set(k, v2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sortedKeys.sort((a, b) => {
|
res.sortedKeys.sort((a, b) => {
|
||||||
if (a.length !== b.length) return b.length - a.length;
|
const str1 = a.toString();
|
||||||
else if (a > b) return 1;
|
const str2 = b.toString();
|
||||||
else if (a < b) return -1;
|
if (str1.length !== str2.length) return str2.length - str1.length;
|
||||||
|
else if (str1 > str2) return 1;
|
||||||
|
else if (str1 < str2) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,10 +70,10 @@ class Indeterminates {
|
|||||||
const k2 = b.sortedKeys[i];
|
const k2 = b.sortedKeys[i];
|
||||||
const w2 = b.map.get(k2);
|
const w2 = b.map.get(k2);
|
||||||
if (w1 !== w2) return w2 - w1;
|
if (w1 !== w2) return w2 - w1;
|
||||||
if (k1.length > k2.length) return -1;
|
if (k1.toString().length > k2.toString().length) return -1;
|
||||||
else if (k1.length < k2.length) return 1;
|
else if (k1.toString().length < k2.toString().length) return 1;
|
||||||
else if (k1 > k2) return 1;
|
else if (k1.toString() > k2.toString()) return 1;
|
||||||
else if (k1 < k2) return -1;
|
else if (k1.toString() < k2.toString()) return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -84,26 +81,35 @@ class Indeterminates {
|
|||||||
|
|
||||||
interface Term {
|
interface Term {
|
||||||
indeterminates: Indeterminates;
|
indeterminates: Indeterminates;
|
||||||
coefficientNumerator: BI.bigint;
|
coefficientNumerator: bigint;
|
||||||
coefficientDenominator: BI.bigint;
|
coefficientDenominator: bigint;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
|
function findGcd(a: bigint, b: bigint): bigint {
|
||||||
while (BI.ne(b, ZERO)) {
|
while (b !== 0n) {
|
||||||
const t = b;
|
const t = b;
|
||||||
b = BI.rem(a, b);
|
b = a % b;
|
||||||
a = t;
|
a = t;
|
||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Polynomial {
|
class Polynomial extends Expression {
|
||||||
declare public terms: Term[];
|
declare public terms: Term[];
|
||||||
|
|
||||||
public constructor(terms: Term[]) {
|
public constructor(terms: Term[]) {
|
||||||
|
super();
|
||||||
this.terms = terms;
|
this.terms = terms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map(): Polynomial {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async mapAsync(): Promise<Polynomial> {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public static simplifyTerms(terms: Term[]): Term[] {
|
public static simplifyTerms(terms: Term[]): Term[] {
|
||||||
const ts = terms
|
const ts = terms
|
||||||
.slice()
|
.slice()
|
||||||
@@ -115,40 +121,37 @@ class Polynomial {
|
|||||||
const t1 = ts[i - 1];
|
const t1 = ts[i - 1];
|
||||||
const t2 = ts[i];
|
const t2 = ts[i];
|
||||||
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
|
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
|
||||||
const numerator = BI.add(
|
const numerator =
|
||||||
BI.mul(t1.coefficientNumerator, t2.coefficientDenominator),
|
t1.coefficientNumerator * t2.coefficientDenominator +
|
||||||
BI.mul(t2.coefficientNumerator, t1.coefficientDenominator),
|
t2.coefficientNumerator * t1.coefficientDenominator;
|
||||||
);
|
|
||||||
|
|
||||||
const denominator = BI.mul(
|
const denominator =
|
||||||
t1.coefficientDenominator,
|
t1.coefficientDenominator * t2.coefficientDenominator;
|
||||||
t2.coefficientDenominator,
|
|
||||||
);
|
|
||||||
|
|
||||||
const gcd = findGcd(numerator, denominator);
|
const gcd = findGcd(numerator, denominator);
|
||||||
|
|
||||||
ts[i] = {
|
ts[i] = {
|
||||||
indeterminates: t2.indeterminates,
|
indeterminates: t2.indeterminates,
|
||||||
coefficientNumerator: BI.div(numerator, gcd),
|
coefficientNumerator: numerator / gcd,
|
||||||
coefficientDenominator: BI.div(denominator, gcd),
|
coefficientDenominator: denominator / gcd,
|
||||||
};
|
};
|
||||||
ts[i - 1] = {
|
ts[i - 1] = {
|
||||||
indeterminates: t1.indeterminates,
|
indeterminates: t1.indeterminates,
|
||||||
coefficientNumerator: ZERO,
|
coefficientNumerator: 0n,
|
||||||
coefficientDenominator: t1.coefficientDenominator,
|
coefficientDenominator: t1.coefficientDenominator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ts.filter((v) => BI.ne(v.coefficientNumerator, ZERO));
|
return ts.filter((v) => v.coefficientNumerator !== 0n);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromIndeterminate(indeterminate: Expression): Polynomial {
|
public static fromIndeterminate(indeterminate: Expression): Polynomial {
|
||||||
const indeterminates = new Indeterminates(JSON.stringify(indeterminate));
|
const indeterminates = new Indeterminates(indeterminate);
|
||||||
const terms = [
|
const terms = [
|
||||||
{
|
{
|
||||||
indeterminates: indeterminates,
|
indeterminates: indeterminates,
|
||||||
coefficientNumerator: ONE,
|
coefficientNumerator: 1n,
|
||||||
coefficientDenominator: ONE,
|
coefficientDenominator: 1n,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return new Polynomial(terms);
|
return new Polynomial(terms);
|
||||||
@@ -156,15 +159,12 @@ class Polynomial {
|
|||||||
|
|
||||||
public static fromConstant(constant: number): Polynomial {
|
public static fromConstant(constant: number): Polynomial {
|
||||||
const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
|
const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
|
||||||
let numerator = BI.BigInt("0b" + int);
|
let numerator = BigInt("0b" + int);
|
||||||
if (constant < 0) numerator = BI.mul(numerator, NEGATIVE_ONE);
|
if (constant < 0) numerator = numerator / -1n;
|
||||||
let denominator = ONE;
|
let denominator = 1n;
|
||||||
if (frac) {
|
if (frac) {
|
||||||
denominator = BI.exp(TWO, BI.BigInt(frac.length));
|
denominator = 2n ** BigInt(frac.length);
|
||||||
numerator = BI.add(
|
numerator = numerator * denominator + BigInt("0b" + frac);
|
||||||
BI.mul(numerator, denominator),
|
|
||||||
BI.BigInt("0b" + frac),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const terms = [
|
const terms = [
|
||||||
@@ -181,7 +181,7 @@ class Polynomial {
|
|||||||
public negation(): Polynomial {
|
public negation(): Polynomial {
|
||||||
const terms = this.terms.map((t) => ({
|
const terms = this.terms.map((t) => ({
|
||||||
indeterminates: t.indeterminates,
|
indeterminates: t.indeterminates,
|
||||||
coefficientNumerator: BI.mul(t.coefficientNumerator, NEGATIVE_ONE),
|
coefficientNumerator: t.coefficientNumerator * -1n,
|
||||||
coefficientDenominator: t.coefficientDenominator,
|
coefficientDenominator: t.coefficientDenominator,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -217,14 +217,9 @@ class Polynomial {
|
|||||||
|
|
||||||
for (const t1 of this.terms) {
|
for (const t1 of this.terms) {
|
||||||
for (const t2 of rhs.terms) {
|
for (const t2 of rhs.terms) {
|
||||||
const numerator = BI.mul(
|
const numerator = t1.coefficientNumerator * t2.coefficientNumerator;
|
||||||
t1.coefficientNumerator,
|
const denominator =
|
||||||
t2.coefficientNumerator,
|
t1.coefficientDenominator * t2.coefficientDenominator;
|
||||||
);
|
|
||||||
const denominator = BI.mul(
|
|
||||||
t1.coefficientDenominator,
|
|
||||||
t2.coefficientDenominator,
|
|
||||||
);
|
|
||||||
const gcd = findGcd(numerator, denominator);
|
const gcd = findGcd(numerator, denominator);
|
||||||
|
|
||||||
terms.push({
|
terms.push({
|
||||||
@@ -232,8 +227,8 @@ class Polynomial {
|
|||||||
t1.indeterminates,
|
t1.indeterminates,
|
||||||
t2.indeterminates,
|
t2.indeterminates,
|
||||||
),
|
),
|
||||||
coefficientNumerator: BI.div(numerator, gcd),
|
coefficientNumerator: numerator / gcd,
|
||||||
coefficientDenominator: BI.div(denominator, gcd),
|
coefficientDenominator: denominator / gcd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,41 +240,49 @@ class Polynomial {
|
|||||||
return this.multiply(rhs.reciprocal());
|
return this.multiply(rhs.reciprocal());
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toExpression(): Expression {
|
||||||
const add: string[] = [];
|
const add: Expression[] = [];
|
||||||
for (const t of this.terms) {
|
for (const t of this.terms) {
|
||||||
const coefficient =
|
const coefficient =
|
||||||
BI.toNumber(t.coefficientNumerator) /
|
Number(t.coefficientNumerator) / Number(t.coefficientDenominator);
|
||||||
BI.toNumber(t.coefficientDenominator);
|
|
||||||
|
|
||||||
const mul: string[] = [];
|
const mul: Expression[] = [];
|
||||||
if (t.indeterminates.sortedKeys.length) {
|
if (t.indeterminates.sortedKeys.length) {
|
||||||
for (const k of t.indeterminates.sortedKeys) {
|
for (const k of t.indeterminates.sortedKeys) {
|
||||||
const w = t.indeterminates.map.get(k);
|
const w = t.indeterminates.map.get(k);
|
||||||
for (let i = Math.abs(w); i > 0; --i) {
|
for (let i = Math.abs(w); i > 0; --i) {
|
||||||
if (w > 0) mul.push(k);
|
if (w > 0) mul.push(k);
|
||||||
else mul.push(`["/",1,${k}]`);
|
else
|
||||||
|
mul.push(
|
||||||
|
new Expression.Binary("/", new Expression.Literal(1), k),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coefficient !== 1) mul.push(coefficient.toString());
|
if (coefficient !== 1) mul.push(new Expression.Literal(coefficient));
|
||||||
|
|
||||||
if (mul.length > 1) add.push(`["*",${mul.join(",")}]`);
|
while (mul.length > 1) {
|
||||||
else add.push(mul["0"]);
|
const r = mul.pop();
|
||||||
|
const l = mul.pop();
|
||||||
|
mul.push(new Expression.Binary("*", l, r));
|
||||||
|
}
|
||||||
|
add.push(mul[0]);
|
||||||
} else {
|
} else {
|
||||||
add.push(coefficient.toString());
|
add.push(new Expression.Literal(coefficient));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!add.length) return "0";
|
while (add.length > 1) {
|
||||||
else if (add.length === 1) return add[0];
|
const r = add.pop();
|
||||||
else return `["+",${add.join(",")}]`;
|
const l = add.pop();
|
||||||
|
add.push(new Expression.Binary("+", l, r));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!add.length) return new Expression.Literal(0);
|
||||||
|
return add[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDITIVE_IDENTITY = Polynomial.fromConstant(0);
|
|
||||||
const MULTIPLICATIVE_IDENTITY = Polynomial.fromConstant(1);
|
|
||||||
|
|
||||||
const SWAPPED_OPS = {
|
const SWAPPED_OPS = {
|
||||||
"=": "=",
|
"=": "=",
|
||||||
"<>": "<>",
|
"<>": "<>",
|
||||||
@@ -289,156 +292,163 @@ const SWAPPED_OPS = {
|
|||||||
"<=": ">=",
|
"<=": ">=",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function cartesianProduct<T>(arrays: T[][]): T[][] {
|
||||||
|
return arrays.reduce<T[][]>(
|
||||||
|
(acc, cur) => acc.flatMap((a) => cur.map((item) => [...a, item])),
|
||||||
|
[[]],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPolynomial(e: Expression): Polynomial {
|
||||||
|
if (e instanceof Polynomial) return e;
|
||||||
|
if (e instanceof Expression.Literal) {
|
||||||
|
if (e.value == null) return null;
|
||||||
|
if (typeof e.value === "number") return Polynomial.fromConstant(e.value);
|
||||||
|
if (typeof e.value === "string")
|
||||||
|
return Polynomial.fromConstant(parseFloat(e.value) || 0);
|
||||||
|
if (typeof e.value === "boolean") return Polynomial.fromConstant(+e.value);
|
||||||
|
}
|
||||||
|
return Polynomial.fromIndeterminate(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromPolynomial(e: Expression): Expression {
|
||||||
|
if (e instanceof Polynomial) return e.toExpression();
|
||||||
|
if (e instanceof Expression.Conditional) return e.map(fromPolynomial);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCallback(exp: Expression): Expression {
|
function normalizeCallback(exp: Expression): Expression {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (exp instanceof Expression.FunctionCall) {
|
||||||
const op = exp[0];
|
if (exp.name === "COALESCE") {
|
||||||
|
let e: Expression = new Expression.Literal(null);
|
||||||
if (op === "FUNC" && exp[1] === "COALESCE") {
|
for (let i = exp.args.length - 1; i >= 0; --i) {
|
||||||
const res: Expression[] = ["CASE"];
|
e = new Expression.Conditional(
|
||||||
for (let i = 2; i < exp.length; ++i)
|
normalizeCallback(new Expression.Unary("IS NOT NULL", exp.args[i])),
|
||||||
res.push(normalizeCallback(["IS NOT NULL", exp[i]]), exp[i]);
|
exp.args[i],
|
||||||
return normalizeCallback(res);
|
e,
|
||||||
}
|
|
||||||
|
|
||||||
if (op === "CASE") {
|
|
||||||
const res = [] as [Expression, Expression][];
|
|
||||||
for (let i = 1; i < exp.length; i += 2) {
|
|
||||||
let w = exp[i];
|
|
||||||
if (w instanceof Polynomial) w = JSON.parse(w.toString());
|
|
||||||
if (!Array.isArray(w) && !w) continue;
|
|
||||||
const t = exp[i + 1];
|
|
||||||
if (!Array.isArray(t) || t[0] !== "CASE") {
|
|
||||||
res.push([w, t]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let j = 1; j < t.length; j += 2) res.push([and(w, t[j]), t[j + 1]]);
|
|
||||||
res.push([w, null]);
|
|
||||||
if (!Array.isArray(w) && w) break;
|
|
||||||
}
|
|
||||||
while (res[res.length - 1][1] == null) res.pop();
|
|
||||||
return ["CASE", ...res.flat()];
|
|
||||||
}
|
|
||||||
|
|
||||||
const permutations: Map<number, [Expression, Expression][]> = new Map();
|
|
||||||
for (const [i, e] of exp.entries()) {
|
|
||||||
if (!Array.isArray(e)) continue;
|
|
||||||
if (e[0] !== "CASE") continue;
|
|
||||||
const perms: [Expression, Expression][] = [];
|
|
||||||
for (let j = 1; j < e.length; j += 2) perms.push([e[j], e[j + 1]]);
|
|
||||||
perms.push([true, null]);
|
|
||||||
permutations.set(i, perms);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permutations.size) {
|
|
||||||
let res: [Expression, Expression][] = [[true, exp]];
|
|
||||||
for (const [i, perms] of permutations) {
|
|
||||||
const res2: [Expression, Expression][] = [];
|
|
||||||
for (const [w, t] of perms) {
|
|
||||||
res2.push(
|
|
||||||
...res.map((r) => {
|
|
||||||
const e = (r[1] as Expression[]).slice();
|
|
||||||
e[i] = t;
|
|
||||||
return [and(w, r[0]), e] as [Expression, Expression];
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
if (!Array.isArray(w) && w) break;
|
|
||||||
}
|
}
|
||||||
res = res2;
|
return normalizeCallback(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const r of res) r[1] = normalizeCallback(r[1]);
|
|
||||||
|
|
||||||
if (res[0][0] === true) return res[0][1];
|
|
||||||
while (res[res.length - 1][1] == null) res.pop();
|
|
||||||
|
|
||||||
return ["CASE", ...res.flat()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPolynomial(e: Expression): Polynomial {
|
if (exp instanceof Expression.Conditional) {
|
||||||
if (e == null) return null;
|
let e = exp;
|
||||||
if (e instanceof Polynomial) return e;
|
|
||||||
if (typeof e === "number") return Polynomial.fromConstant(e);
|
if (e.condition instanceof Polynomial)
|
||||||
if (typeof e === "string")
|
e = new Expression.Conditional(
|
||||||
return Polynomial.fromConstant(parseFloat(e) || 0);
|
e.condition.toExpression(),
|
||||||
if (typeof e === "boolean") return Polynomial.fromConstant(+e);
|
e.then,
|
||||||
return Polynomial.fromIndeterminate(e);
|
e.otherwise,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (e.then instanceof Expression.Conditional) {
|
||||||
|
e = new Expression.Conditional(
|
||||||
|
Expression.and(e.condition, e.then.condition),
|
||||||
|
e.then.then,
|
||||||
|
new Expression.Conditional(e.condition, e.then.otherwise, e.otherwise),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op === "+") {
|
const combs: [Expression, Expression][][] = [];
|
||||||
const args: Polynomial[] = [];
|
const callback: (e: Expression) => Expression = (e) => {
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
if (e instanceof Expression.Conditional) {
|
||||||
const p = toPolynomial(exp[i]);
|
combs[combs.length - 1].push([e.condition, e.then]);
|
||||||
if (p == null) return null;
|
callback(e.otherwise);
|
||||||
args.push(p);
|
} else {
|
||||||
|
combs[combs.length - 1].push([new Expression.Literal(true), e]);
|
||||||
}
|
}
|
||||||
return args.reduce(
|
return e;
|
||||||
(previousValue, currentValue) => previousValue.add(currentValue),
|
};
|
||||||
ADDITIVE_IDENTITY,
|
|
||||||
) as unknown as Expression;
|
exp.map((e) => {
|
||||||
} else if (op === "*") {
|
combs.push([]);
|
||||||
const args: Polynomial[] = [];
|
callback(e);
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
return e;
|
||||||
const p = toPolynomial(exp[i]);
|
});
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
if (combs.some((a) => a.length > 1)) {
|
||||||
|
let res: Expression = new Expression.Literal(null);
|
||||||
|
for (const p of cartesianProduct(combs).reverse()) {
|
||||||
|
let condition: Expression = new Expression.Literal(true);
|
||||||
|
const e = reduce(
|
||||||
|
normalizeCallback(
|
||||||
|
exp.map((_, i) => {
|
||||||
|
condition = Expression.and(condition, p[i][0]);
|
||||||
|
return p[i][1];
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(condition instanceof Expression.Literal))
|
||||||
|
res = new Expression.Conditional(condition, e, res);
|
||||||
|
else if (condition.value) res = e;
|
||||||
}
|
}
|
||||||
return args.reduce(
|
return res;
|
||||||
(previousValue, currentValue) => previousValue.multiply(currentValue),
|
}
|
||||||
MULTIPLICATIVE_IDENTITY,
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (op === "-") {
|
|
||||||
const args: Polynomial[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const p = toPolynomial(exp[i]);
|
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
|
||||||
}
|
|
||||||
return args.reduce((previousValue, currentValue) =>
|
|
||||||
previousValue.subtract(currentValue),
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (op === "/") {
|
|
||||||
const args: Polynomial[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const p = toPolynomial(exp[i]);
|
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
|
||||||
}
|
|
||||||
return args.reduce((previousValue, currentValue) =>
|
|
||||||
previousValue.divide(currentValue),
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
let lhs: Polynomial, rhs: Polynomial;
|
|
||||||
if (exp[1] instanceof Polynomial) lhs = exp[1];
|
|
||||||
else if (typeof exp[1] === "number") lhs = Polynomial.fromConstant(exp[1]);
|
|
||||||
|
|
||||||
if (exp[2] instanceof Polynomial) rhs = exp[2];
|
if (exp instanceof Expression.Binary) {
|
||||||
else if (typeof exp[2] === "number") rhs = Polynomial.fromConstant(exp[2]);
|
if (["+", "-", "*", "/"].includes(exp.operator)) {
|
||||||
|
const lhs = toPolynomial(exp.left);
|
||||||
|
const rhs = toPolynomial(exp.right);
|
||||||
|
if (lhs == null || rhs == null) return new Expression.Literal(null);
|
||||||
|
if (exp.operator === "+") return lhs.add(rhs);
|
||||||
|
if (exp.operator === "-") return lhs.subtract(rhs);
|
||||||
|
if (exp.operator === "*") return lhs.multiply(rhs);
|
||||||
|
if (exp.operator === "/") return lhs.divide(rhs);
|
||||||
|
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(exp.operator)) {
|
||||||
|
let lhs: Polynomial, rhs: Polynomial;
|
||||||
|
|
||||||
if (lhs || rhs) {
|
if (exp.left instanceof Polynomial) lhs = exp.left;
|
||||||
if (!lhs) lhs = Polynomial.fromIndeterminate(exp[1]);
|
else if (exp.left instanceof Expression.Literal) {
|
||||||
if (!rhs) rhs = Polynomial.fromIndeterminate(exp[2]);
|
if (exp.left.value == null) return exp.left;
|
||||||
|
else if (typeof exp.left.value === "number")
|
||||||
|
lhs = Polynomial.fromConstant(exp.left.value);
|
||||||
|
}
|
||||||
|
|
||||||
lhs = lhs.subtract(rhs);
|
if (exp.right instanceof Polynomial) rhs = exp.right;
|
||||||
rhs = lhs.constant().negation();
|
else if (exp.right instanceof Expression.Literal) {
|
||||||
lhs = lhs.add(rhs);
|
if (exp.right.value == null) return exp.right;
|
||||||
|
else if (typeof exp.right.value === "number")
|
||||||
|
rhs = Polynomial.fromConstant(exp.right.value);
|
||||||
|
}
|
||||||
|
|
||||||
if (!lhs.terms.length) {
|
if (lhs || rhs) {
|
||||||
exp = [op, JSON.parse(lhs.toString()), JSON.parse(rhs.toString())];
|
if (!lhs) lhs = Polynomial.fromIndeterminate(exp.left);
|
||||||
|
if (!rhs) rhs = Polynomial.fromIndeterminate(exp.right);
|
||||||
|
|
||||||
|
lhs = lhs.subtract(rhs);
|
||||||
|
rhs = lhs.constant().negation();
|
||||||
|
lhs = lhs.add(rhs);
|
||||||
|
|
||||||
|
if (!lhs.terms.length) {
|
||||||
|
const l = lhs.toExpression() as Expression.Literal;
|
||||||
|
const r = rhs.toExpression() as Expression.Literal;
|
||||||
|
|
||||||
|
if (exp.operator === "=")
|
||||||
|
return new Expression.Literal(l.value === r.value);
|
||||||
|
else if (exp.operator === "<>")
|
||||||
|
return new Expression.Literal(l.value !== r.value);
|
||||||
|
else if (exp.operator === ">")
|
||||||
|
return new Expression.Literal(l.value > r.value);
|
||||||
|
else if (exp.operator === ">=")
|
||||||
|
return new Expression.Literal(l.value >= r.value);
|
||||||
|
else if (exp.operator === "<")
|
||||||
|
return new Expression.Literal(l.value < r.value);
|
||||||
|
else if (exp.operator === "<=")
|
||||||
|
return new Expression.Literal(l.value <= r.value);
|
||||||
|
else throw new Error("Invalid operator");
|
||||||
|
}
|
||||||
|
|
||||||
if (op === "=") return exp[1] === exp[2];
|
|
||||||
else if (op === "<>") return exp[1] !== exp[2];
|
|
||||||
else if (op === ">") return exp[1] > exp[2];
|
|
||||||
else if (op === ">=") return exp[1] >= exp[2];
|
|
||||||
else if (op === "<") return exp[1] < exp[2];
|
|
||||||
else if (op === "<=") return exp[1] <= exp[2];
|
|
||||||
} else {
|
|
||||||
let flipOp = 1;
|
let flipOp = 1;
|
||||||
|
|
||||||
const n = lhs.terms[0].coefficientNumerator;
|
const n = lhs.terms[0].coefficientNumerator;
|
||||||
const d = lhs.terms[0].coefficientDenominator;
|
const d = lhs.terms[0].coefficientDenominator;
|
||||||
|
|
||||||
if (BI.lt(n, ZERO) || BI.lt(d, ZERO)) flipOp *= -1;
|
if (n < 0n || d < 0n) flipOp *= -1;
|
||||||
|
|
||||||
const reciprocal = new Polynomial([
|
const reciprocal = new Polynomial([
|
||||||
{
|
{
|
||||||
@@ -463,30 +473,18 @@ function normalizeCallback(exp: Expression): Expression {
|
|||||||
rhs = rhs.reciprocal();
|
rhs = rhs.reciprocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flipOp < 0) exp = [SWAPPED_OPS[op], lhs, rhs];
|
if (flipOp > 0) exp = new Expression.Binary(exp.operator, lhs, rhs);
|
||||||
else exp = [op, lhs, rhs];
|
else exp = new Expression.Binary(SWAPPED_OPS[exp.operator], lhs, rhs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore polynomial expressions
|
// Restore polynomial expressions
|
||||||
exp = exp.map((e) =>
|
exp = exp.map(fromPolynomial);
|
||||||
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
|
|
||||||
);
|
|
||||||
|
|
||||||
exp = evaluateCallback(exp);
|
|
||||||
|
|
||||||
return exp;
|
return exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function normalize(expr: Expression): Expression {
|
export default function normalize(exp: Expression): Expression {
|
||||||
expr = map(expr, normalizeCallback);
|
return fromPolynomial(exp.evaluate(normalizeCallback));
|
||||||
if (expr instanceof Polynomial) {
|
|
||||||
expr = JSON.parse(expr.toString());
|
|
||||||
} else if (Array.isArray(expr) && expr[0] === "CASE") {
|
|
||||||
expr = expr.map((e) =>
|
|
||||||
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return expr;
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { complement } from "espresso-iisojs";
|
import { complement } from "espresso-iisojs";
|
||||||
import { Expression } from "../../types.ts";
|
import Expression from "../expression.ts";
|
||||||
import normalize from "./normalize.ts";
|
import normalize from "./normalize.ts";
|
||||||
import { Clause, SynthContext } from "./synth.ts";
|
import { Clause, SynthContext } from "./synth.ts";
|
||||||
import { and, or } from "./util.ts";
|
import Path from "../path.ts";
|
||||||
|
|
||||||
type Bookmark = Record<string, null | boolean | number | string>;
|
type Bookmark = Record<string, null | boolean | number | string>;
|
||||||
|
|
||||||
@@ -27,25 +27,35 @@ export function bookmarkToExpression(
|
|||||||
): Expression {
|
): Expression {
|
||||||
return Object.entries(sort)
|
return Object.entries(sort)
|
||||||
.reverse()
|
.reverse()
|
||||||
.reduce((cur, kv) => {
|
.reduce(
|
||||||
const [param, asc] = kv;
|
(cur, kv) => {
|
||||||
if (asc < 0) {
|
const [param, asc] = kv;
|
||||||
if (bookmark[param] == null) {
|
const p = new Expression.Parameter(Path.parse(param));
|
||||||
return or(
|
const b = new Expression.Literal(bookmark[param]);
|
||||||
["IS NOT NULL", ["PARAM", param]],
|
if (asc < 0) {
|
||||||
and(["IS NULL", ["PARAM", param]], cur),
|
if (bookmark[param] == null) {
|
||||||
|
return Expression.or(
|
||||||
|
new Expression.Unary("IS NOT NULL", p),
|
||||||
|
Expression.and(new Expression.Unary("IS NULL", p), cur),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Expression.Binary(
|
||||||
|
"OR",
|
||||||
|
new Expression.Binary(">", p, b),
|
||||||
|
new Expression.Binary("AND", new Expression.Binary("=", p, b), cur),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let f: Expression = new Expression.Unary("IS NULL", p);
|
||||||
|
if (bookmark[param] == null) return Expression.and(f, cur);
|
||||||
|
f = Expression.or(f, new Expression.Binary("<", p, b));
|
||||||
|
return Expression.or(
|
||||||
|
f,
|
||||||
|
Expression.and(new Expression.Binary("=", p, b), cur),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let f = null;
|
},
|
||||||
f = or(f, [">", ["PARAM", param], bookmark[param]]);
|
new Expression.Literal(true) as Expression,
|
||||||
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
);
|
||||||
} else {
|
|
||||||
let f: Expression = ["IS NULL", ["PARAM", param]];
|
|
||||||
if (bookmark[param] == null) return and(f, cur);
|
|
||||||
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
|
|
||||||
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
|
||||||
}
|
|
||||||
}, true as Expression);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCover(
|
function getCover(
|
||||||
@@ -60,7 +70,7 @@ function getCover(
|
|||||||
const nextCov: Minterm = [];
|
const nextCov: Minterm = [];
|
||||||
|
|
||||||
if (sort > 0) {
|
if (sort > 0) {
|
||||||
const lhs = new Clause.Exp(["PARAM", param]);
|
const lhs = new Clause.Exp(new Expression.Parameter(Path.parse(param)));
|
||||||
const isNull = context.getVar(new Clause.IsNull(lhs));
|
const isNull = context.getVar(new Clause.IsNull(lhs));
|
||||||
cov.push((isNull << 2) ^ 2);
|
cov.push((isNull << 2) ^ 2);
|
||||||
}
|
}
|
||||||
@@ -68,15 +78,15 @@ function getCover(
|
|||||||
for (const m of minterm) {
|
for (const m of minterm) {
|
||||||
const clause = context.getClause(m >>> 2);
|
const clause = context.getClause(m >>> 2);
|
||||||
if (clause instanceof Clause.IsNull) {
|
if (clause instanceof Clause.IsNull) {
|
||||||
const lhs = clause.operand.expression();
|
if (!(clause.operand instanceof Clause.Exp)) continue;
|
||||||
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
|
if (!(clause.operand.exp instanceof Expression.Parameter)) continue;
|
||||||
if (lhs[1] !== param) continue;
|
if (clause.operand.exp.path.toString() !== param) continue;
|
||||||
nextCov.push(m);
|
nextCov.push(m);
|
||||||
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
|
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
|
||||||
} else if (clause instanceof Clause.Compare) {
|
} else if (clause instanceof Clause.Compare) {
|
||||||
const lhs = clause.lhs.expression();
|
if (!(clause.lhs instanceof Clause.Exp)) continue;
|
||||||
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
|
if (!(clause.lhs.exp instanceof Expression.Parameter)) continue;
|
||||||
if (lhs[1] !== param) continue;
|
if (clause.lhs.exp.path.toString() !== param) continue;
|
||||||
if (!(m & 1) && sort > 0) continue;
|
if (!(m & 1) && sort > 0) continue;
|
||||||
nextCov.push(m);
|
nextCov.push(m);
|
||||||
|
|
||||||
@@ -117,18 +127,20 @@ export function paginate(
|
|||||||
sort: Record<string, number>,
|
sort: Record<string, number>,
|
||||||
): [Expression, Expression] {
|
): [Expression, Expression] {
|
||||||
fetched = normalize(fetched);
|
fetched = normalize(fetched);
|
||||||
if (!fetched) return [false, toFetch];
|
if (fetched instanceof Expression.Literal && !fetched.value)
|
||||||
|
return [new Expression.Literal(false), toFetch];
|
||||||
|
|
||||||
toFetch = normalize(toFetch);
|
toFetch = normalize(toFetch);
|
||||||
if (!toFetch) return [false, toFetch];
|
if (toFetch instanceof Expression.Literal && !toFetch.value)
|
||||||
|
return [new Expression.Literal(false), toFetch];
|
||||||
|
|
||||||
const synth1 = Clause.fromExpression(fetched);
|
const synth1 = Clause.fromExpression(fetched);
|
||||||
const synth2 = Clause.fromExpression(toFetch);
|
const synth2 = Clause.fromExpression(toFetch);
|
||||||
|
|
||||||
const context = new SynthContext();
|
const context = new SynthContext();
|
||||||
|
|
||||||
const expr1Minterms = synth1.true(context);
|
const expr1Minterms = synth1.getMinterms(context, 0b100);
|
||||||
const expr2MintermsC = complement(synth2.true(context));
|
const expr2MintermsC = synth2.getMinterms(context, 0b011);
|
||||||
|
|
||||||
const gaps = context.sanitizeMinterms(
|
const gaps = context.sanitizeMinterms(
|
||||||
complement([
|
complement([
|
||||||
|
|||||||
+496
-398
@@ -1,5 +1,166 @@
|
|||||||
import parsimmon from "parsimmon";
|
import Path from "../path.ts";
|
||||||
import { Expression } from "../../types.ts";
|
import Expression from "../expression.ts";
|
||||||
|
|
||||||
|
export class Cursor {
|
||||||
|
input: string;
|
||||||
|
pos: number;
|
||||||
|
boundryStack: number[][];
|
||||||
|
boundry: number[];
|
||||||
|
charCode: number;
|
||||||
|
|
||||||
|
constructor(input: string) {
|
||||||
|
this.input = input;
|
||||||
|
this.pos = 0;
|
||||||
|
this.boundryStack = [];
|
||||||
|
this.boundry = [];
|
||||||
|
this.charCode = input.charCodeAt(0) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fork(): Cursor {
|
||||||
|
const cursor = new Cursor(this.input);
|
||||||
|
cursor.pos = this.pos;
|
||||||
|
cursor.boundryStack = this.boundryStack.slice();
|
||||||
|
cursor.boundry = this.boundry;
|
||||||
|
cursor.charCode = this.charCode;
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(cursor: Cursor): void {
|
||||||
|
this.pos = cursor.pos;
|
||||||
|
this.boundryStack = cursor.boundryStack.slice();
|
||||||
|
this.boundry = cursor.boundry;
|
||||||
|
this.charCode = cursor.charCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
read(cur: Cursor): string {
|
||||||
|
return this.input.slice(this.pos, cur.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
step(): Cursor {
|
||||||
|
if (this.charCode === 0) return this;
|
||||||
|
++this.pos;
|
||||||
|
this.charCode = this.input.charCodeAt(this.pos) || 0;
|
||||||
|
if (this.boundry.includes(this.charCode)) this.charCode = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(callback: (charCode: number) => boolean): Cursor {
|
||||||
|
while (this.charCode && callback(this.charCode)) this.step();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
descend(chars: number[], override = true): void {
|
||||||
|
this.boundryStack.push(this.boundry);
|
||||||
|
if (override) this.boundry = chars;
|
||||||
|
else this.boundry = [...this.boundry, ...chars];
|
||||||
|
this.charCode = this.input.charCodeAt(this.pos) || 0;
|
||||||
|
if (this.boundry.includes(this.charCode)) this.charCode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ascend(): void {
|
||||||
|
if (!this.boundryStack.length) throw new Error("Unmatched boundry");
|
||||||
|
this.boundry = this.boundryStack.pop();
|
||||||
|
this.charCode = this.input.charCodeAt(this.pos) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipwhitespace(): Cursor {
|
||||||
|
return this.walk((c) => c <= 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHAR_SINGLE_QUOTE = 39;
|
||||||
|
const CHAR_DOUBLE_QUOTE = 34;
|
||||||
|
const CHAR_OPEN_PAREN = 40;
|
||||||
|
const CHAR_CLOSE_PAREN = 41;
|
||||||
|
const CHAR_COMMA = 44;
|
||||||
|
const CHAR_PERIOD = 46;
|
||||||
|
const CHAR_COLON = 58;
|
||||||
|
const CHAR_OPEN_BRACKET = 91;
|
||||||
|
const CHAR_BACKSLASH = 92;
|
||||||
|
const CHAR_CLOSE_BRACKET = 93;
|
||||||
|
|
||||||
|
const BINARY_OPERATORS = [
|
||||||
|
">=",
|
||||||
|
"<=",
|
||||||
|
"<>",
|
||||||
|
"=",
|
||||||
|
">",
|
||||||
|
"<",
|
||||||
|
"LIKE",
|
||||||
|
"NOT LIKE",
|
||||||
|
"AND",
|
||||||
|
"OR",
|
||||||
|
"*",
|
||||||
|
"/",
|
||||||
|
"%",
|
||||||
|
"||",
|
||||||
|
"-",
|
||||||
|
"+",
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRECEDENCE = {
|
||||||
|
OR: 10,
|
||||||
|
AND: 11,
|
||||||
|
NOT: 12,
|
||||||
|
"=": 20,
|
||||||
|
"<>": 20,
|
||||||
|
">": 20,
|
||||||
|
">=": 20,
|
||||||
|
"<": 20,
|
||||||
|
"<=": 20,
|
||||||
|
LIKE: 20,
|
||||||
|
"NOT LIKE": 20,
|
||||||
|
"IS NULL": 20,
|
||||||
|
"IS NOT NULL": 20,
|
||||||
|
"||": 30,
|
||||||
|
"+": 31,
|
||||||
|
"-": 31,
|
||||||
|
"*": 32,
|
||||||
|
"/": 32,
|
||||||
|
"%": 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
function* range(s: number, e: number): Generator<number> {
|
||||||
|
for (let i = s; i < e; i++) yield i;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PATH_CHARS = new Set<number>([
|
||||||
|
...range(65, 91),
|
||||||
|
...range(97, 123),
|
||||||
|
...range(48, 58),
|
||||||
|
95,
|
||||||
|
45,
|
||||||
|
42,
|
||||||
|
123,
|
||||||
|
125,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function findOperator(cursor: Cursor): string {
|
||||||
|
cursor.skipwhitespace();
|
||||||
|
let found = "";
|
||||||
|
let foundCursor = cursor;
|
||||||
|
let operators = [...BINARY_OPERATORS, "IS NULL", "IS NOT NULL"];
|
||||||
|
for (let i = 0; operators.length && cursor.charCode; ++i) {
|
||||||
|
let c = cursor.charCode;
|
||||||
|
cursor.step();
|
||||||
|
if (c >= 97 && c <= 122) c -= 32;
|
||||||
|
if (c <= 32) {
|
||||||
|
cursor.skipwhitespace();
|
||||||
|
c = 32;
|
||||||
|
}
|
||||||
|
operators = operators.filter((o) => {
|
||||||
|
if (o.charCodeAt(i) !== c) return false;
|
||||||
|
if (o.length === i + 1) {
|
||||||
|
found = o;
|
||||||
|
foundCursor = cursor.fork();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) cursor.sync(foundCursor);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
|
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
|
||||||
function interpretEscapes(str): string {
|
function interpretEscapes(str): string {
|
||||||
@@ -21,423 +182,330 @@ function interpretEscapes(str): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function map<T>(exp: Expression, callback: (e: Expression) => T): T {
|
export function parseExpression(cursor: Cursor, presedence = 0): Expression {
|
||||||
if (!Array.isArray(exp)) return callback(exp);
|
cursor.skipwhitespace();
|
||||||
|
const char = cursor.charCode;
|
||||||
|
let lhs: Expression;
|
||||||
|
if (char === CHAR_OPEN_PAREN) {
|
||||||
|
cursor.step();
|
||||||
|
cursor.descend([CHAR_CLOSE_PAREN]);
|
||||||
|
lhs = parseExpression(cursor, 0);
|
||||||
|
cursor.ascend();
|
||||||
|
|
||||||
let clone;
|
cursor.skipwhitespace();
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
if (cursor.charCode !== CHAR_CLOSE_PAREN)
|
||||||
const sub = map(exp[i], callback);
|
throw new Error("Expected ')'" + cursor.pos + " " + cursor.charCode);
|
||||||
if (sub !== exp[i]) {
|
cursor.step();
|
||||||
clone = clone || exp.slice();
|
} else if (char === CHAR_DOUBLE_QUOTE) {
|
||||||
clone[i] = sub;
|
cursor.step();
|
||||||
|
const cursor2 = cursor.fork();
|
||||||
|
for (
|
||||||
|
cursor2.descend([]);
|
||||||
|
cursor2.charCode !== CHAR_DOUBLE_QUOTE;
|
||||||
|
cursor2.step()
|
||||||
|
) {
|
||||||
|
if (!cursor2.charCode) throw new Error("Unterminated string");
|
||||||
|
if (cursor2.charCode === CHAR_BACKSLASH) cursor2.step();
|
||||||
|
}
|
||||||
|
cursor2.ascend();
|
||||||
|
const str = cursor.read(cursor2);
|
||||||
|
cursor.sync(cursor2);
|
||||||
|
cursor.step();
|
||||||
|
return new Expression.Literal(interpretEscapes(str));
|
||||||
|
} else if (char === CHAR_SINGLE_QUOTE) {
|
||||||
|
cursor.step();
|
||||||
|
const cursor2 = cursor.fork();
|
||||||
|
for (
|
||||||
|
cursor2.descend([]);
|
||||||
|
cursor2.charCode !== CHAR_SINGLE_QUOTE;
|
||||||
|
cursor2.step()
|
||||||
|
) {
|
||||||
|
if (!cursor2.charCode) throw new Error("Unterminated string");
|
||||||
|
if (cursor2.charCode === CHAR_BACKSLASH) cursor2.step();
|
||||||
|
}
|
||||||
|
cursor2.ascend();
|
||||||
|
const str = cursor.read(cursor2);
|
||||||
|
cursor.sync(cursor2);
|
||||||
|
cursor.step();
|
||||||
|
return new Expression.Literal(str.replaceAll("''", "'"));
|
||||||
|
} else {
|
||||||
|
const cursor2 = cursor.fork();
|
||||||
|
cursor.walk(
|
||||||
|
(c) => c > 32 && c !== CHAR_OPEN_PAREN && c !== CHAR_OPEN_BRACKET,
|
||||||
|
);
|
||||||
|
const token = cursor2.read(cursor);
|
||||||
|
if (!token) throw new Error("Invalid expression");
|
||||||
|
if (/^true$/i.test(token)) lhs = new Expression.Literal(true);
|
||||||
|
else if (/^false$/i.test(token)) lhs = new Expression.Literal(false);
|
||||||
|
else if (/^null$/i.test(token)) lhs = new Expression.Literal(null);
|
||||||
|
else if (/^-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?$/.test(token))
|
||||||
|
lhs = new Expression.Literal(Number(token));
|
||||||
|
else if (/^not$/i.test(token)) {
|
||||||
|
lhs = new Expression.Unary(
|
||||||
|
"NOT",
|
||||||
|
parseExpression(cursor, PRECEDENCE["NOT"]),
|
||||||
|
);
|
||||||
|
} else if (/^case$/i.test(token)) {
|
||||||
|
const pairs: [Expression, Expression][] = [];
|
||||||
|
for (;;) {
|
||||||
|
cursor.skipwhitespace();
|
||||||
|
const whenStr = cursor.fork().read(cursor.walk((c) => c > 32));
|
||||||
|
if (/^else$/i.test(whenStr)) {
|
||||||
|
lhs = parseExpression(cursor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^end$/i.test(whenStr)) break;
|
||||||
|
else if (lhs) throw new Error("Expected END");
|
||||||
|
if (!/^when$/i.test(whenStr)) throw new Error("Expected WHEN");
|
||||||
|
const condition = parseExpression(cursor);
|
||||||
|
cursor.skipwhitespace();
|
||||||
|
const thenStr = cursor.fork().read(cursor.walk((c) => c > 32));
|
||||||
|
if (!/^then$/i.test(thenStr)) throw new Error("Expected THEN");
|
||||||
|
const then = parseExpression(cursor);
|
||||||
|
pairs.push([condition, then]);
|
||||||
|
}
|
||||||
|
if (!lhs) lhs = new Expression.Literal(null);
|
||||||
|
while (pairs.length) {
|
||||||
|
const [condition, then] = pairs.pop();
|
||||||
|
lhs = new Expression.Conditional(condition, then, lhs);
|
||||||
|
}
|
||||||
|
} else if (cursor.charCode === CHAR_OPEN_PAREN) {
|
||||||
|
cursor.step();
|
||||||
|
cursor.descend([CHAR_CLOSE_PAREN]);
|
||||||
|
cursor.skipwhitespace();
|
||||||
|
const args = [] as Expression[];
|
||||||
|
while (cursor.charCode) {
|
||||||
|
cursor.descend([CHAR_COMMA], false);
|
||||||
|
const e = parseExpression(cursor);
|
||||||
|
args.push(e);
|
||||||
|
cursor.ascend();
|
||||||
|
cursor.skipwhitespace();
|
||||||
|
if ((cursor.charCode as number) !== CHAR_COMMA) break;
|
||||||
|
cursor.step();
|
||||||
|
}
|
||||||
|
cursor.ascend();
|
||||||
|
cursor.step();
|
||||||
|
lhs = new Expression.FunctionCall(token, args);
|
||||||
|
} else {
|
||||||
|
const p = parsePath(cursor2);
|
||||||
|
lhs = new Expression.Parameter(p);
|
||||||
|
cursor.sync(cursor2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(clone || exp);
|
for (;;) {
|
||||||
}
|
cursor.skipwhitespace();
|
||||||
|
const cursor2 = cursor.fork();
|
||||||
export async function mapAsync(
|
const op = findOperator(cursor2);
|
||||||
exp: Expression,
|
const p = PRECEDENCE[op];
|
||||||
callback: (e: Expression) => Promise<Expression>,
|
if (p <= presedence) return lhs;
|
||||||
): Promise<Expression> {
|
if (op === "IS NULL") {
|
||||||
if (!Array.isArray(exp)) return callback(exp);
|
lhs = new Expression.Unary("IS NULL", lhs);
|
||||||
|
} else if (op === "IS NOT NULL") {
|
||||||
let clone;
|
lhs = new Expression.Unary("IS NOT NULL", lhs);
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
} else if (BINARY_OPERATORS.includes(op)) {
|
||||||
const sub = await mapAsync(exp[i], callback);
|
lhs = new Expression.Binary(op, lhs, parseExpression(cursor2, p));
|
||||||
if (sub !== exp[i]) {
|
} else if (op) throw new Error("Unrecognized operator: " + op);
|
||||||
clone = clone || exp.slice();
|
else break;
|
||||||
clone[i] = sub;
|
cursor.sync(cursor2);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return lhs;
|
||||||
return callback(clone || exp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function binaryLeft(
|
// Parse old-format alias value: unquoted, double-quoted, or single-quoted.
|
||||||
operatorsParser: parsimmon.Parser<string>,
|
// Returns the parsed string value. Cursor is advanced past the value.
|
||||||
nextParser: parsimmon.Parser<Expression>,
|
function parseOldAliasValue(cursor: Cursor): string {
|
||||||
): parsimmon.Parser<Expression> {
|
cursor.walk((c) => c <= 32); // skip leading whitespace
|
||||||
return parsimmon.seqMap(
|
const c = cursor.charCode;
|
||||||
nextParser,
|
if (c === CHAR_DOUBLE_QUOTE) {
|
||||||
parsimmon.seq(operatorsParser, nextParser).many(),
|
// Double-quoted: use JSON.parse semantics (handles \", \n, \uXXXX etc.)
|
||||||
(first, rest) =>
|
// Descend with no boundaries so commas/brackets inside quotes are not
|
||||||
rest.reduce((acc, ch) => {
|
// treated as terminators.
|
||||||
const [op, another] = ch;
|
const start = cursor.pos;
|
||||||
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
|
cursor.step();
|
||||||
return [op, acc, another];
|
cursor.descend([]);
|
||||||
}, first),
|
while (cursor.charCode) {
|
||||||
);
|
if (cursor.charCode === CHAR_BACKSLASH) {
|
||||||
|
cursor.step();
|
||||||
|
if (!cursor.charCode) break;
|
||||||
|
} else if (cursor.charCode === CHAR_DOUBLE_QUOTE) {
|
||||||
|
cursor.ascend();
|
||||||
|
cursor.step();
|
||||||
|
return JSON.parse(cursor.input.slice(start, cursor.pos)) as string;
|
||||||
|
}
|
||||||
|
cursor.step();
|
||||||
|
}
|
||||||
|
throw new Error("Unterminated string");
|
||||||
|
}
|
||||||
|
if (c === CHAR_SINGLE_QUOTE) {
|
||||||
|
// Single-quoted: strip quotes, no escape processing
|
||||||
|
cursor.step();
|
||||||
|
cursor.descend([]);
|
||||||
|
const start = cursor.pos;
|
||||||
|
while (cursor.charCode && cursor.charCode !== CHAR_SINGLE_QUOTE)
|
||||||
|
cursor.step();
|
||||||
|
if (!cursor.charCode) throw new Error("Unterminated string");
|
||||||
|
const value = cursor.input.slice(start, cursor.pos);
|
||||||
|
cursor.ascend();
|
||||||
|
cursor.step();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// Unquoted: read until , or ] (respecting boundary), trim result
|
||||||
|
const start = cursor.pos;
|
||||||
|
cursor.walk(() => true);
|
||||||
|
return cursor.input.slice(start, cursor.pos).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const lang = parsimmon.createLanguage({
|
// Parse old-format alias content: key:value,key:value,...
|
||||||
ComparisonOperator: function () {
|
// Cursor should be positioned at the start of bracket content
|
||||||
return parsimmon
|
// (after '[', with ']' set as boundary).
|
||||||
.alt(
|
// Returns an Expression: Binary("AND", ...) chain of Binary("=", param, literal).
|
||||||
parsimmon.string(">="),
|
// Empty content returns Literal(true).
|
||||||
parsimmon.string("<>"),
|
function parseOldAlias(cursor: Cursor): Expression {
|
||||||
parsimmon.string("<="),
|
cursor.skipwhitespace();
|
||||||
parsimmon.string("="),
|
if (!cursor.charCode) return new Expression.Literal(true);
|
||||||
parsimmon.string(">"),
|
|
||||||
parsimmon.string("<"),
|
let result: Expression | null = null;
|
||||||
)
|
for (;;) {
|
||||||
.skip(parsimmon.optWhitespace);
|
cursor.skipwhitespace();
|
||||||
},
|
// Parse key as a path (supports nested aliases via parsePath).
|
||||||
LikeOperator: function () {
|
// Add colon and comma as boundaries so the path stops there.
|
||||||
return parsimmon
|
cursor.descend([CHAR_COLON, CHAR_COMMA], false);
|
||||||
.alt(
|
const keyPath = parsePath(cursor);
|
||||||
parsimmon.regexp(/like/i).result("LIKE").desc("LIKE"),
|
cursor.ascend();
|
||||||
parsimmon
|
|
||||||
.regexp(/not\s+like/i)
|
// After ascend, check for the colon separator
|
||||||
.result("NOT LIKE")
|
cursor.skipwhitespace();
|
||||||
.desc("NOT LIKE"),
|
if ((cursor.charCode as number) !== CHAR_COLON)
|
||||||
)
|
throw new Error("Expected ':'");
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
cursor.step();
|
||||||
.skip(parsimmon.optWhitespace);
|
|
||||||
},
|
// Parse value (reads until boundary , or ])
|
||||||
IsNullOperator: function () {
|
cursor.descend([CHAR_COMMA], false);
|
||||||
return parsimmon
|
const value = parseOldAliasValue(cursor);
|
||||||
.alt(
|
cursor.ascend();
|
||||||
parsimmon
|
|
||||||
.regexp(/is\s+null/i)
|
const pair = new Expression.Binary(
|
||||||
.result("IS NULL")
|
"=",
|
||||||
.desc("IS NULL"),
|
new Expression.Parameter(keyPath),
|
||||||
parsimmon
|
new Expression.Literal(value),
|
||||||
.regexp(/is\s+not\s+null/i)
|
|
||||||
.result("IS NOT NULL")
|
|
||||||
.desc("IS NOT NULL"),
|
|
||||||
)
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace);
|
|
||||||
},
|
|
||||||
NotOperator: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/not/i)
|
|
||||||
.result("NOT")
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.desc("NOT");
|
|
||||||
},
|
|
||||||
AndOperator: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/and/i)
|
|
||||||
.result("AND")
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.desc("AND");
|
|
||||||
},
|
|
||||||
OrOperator: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/or/i)
|
|
||||||
.result("OR")
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.desc("OR");
|
|
||||||
},
|
|
||||||
Parameter: function (r) {
|
|
||||||
return parsimmon
|
|
||||||
.alt(
|
|
||||||
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
|
|
||||||
r.Expression.wrap(
|
|
||||||
parsimmon.string("{").skip(parsimmon.optWhitespace),
|
|
||||||
parsimmon.string("}"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.atLeast(1)
|
|
||||||
.map((x) => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.desc("parameter");
|
|
||||||
},
|
|
||||||
StringValueSql: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/'([^']*)'/, 1)
|
|
||||||
.atLeast(1)
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.map((s) => s.join("'"))
|
|
||||||
.desc("string");
|
|
||||||
},
|
|
||||||
StringValueJs: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/"((?:\\.|.)*?)"/, 1)
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.map(interpretEscapes)
|
|
||||||
.desc("string");
|
|
||||||
},
|
|
||||||
NumberValue: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.map(Number)
|
|
||||||
.desc("number");
|
|
||||||
},
|
|
||||||
BooleanValue: function () {
|
|
||||||
return parsimmon
|
|
||||||
.alt(
|
|
||||||
parsimmon.regexp(/true/i).result(true).desc("TRUE"),
|
|
||||||
parsimmon.regexp(/false/i).result(false).desc("FALSE"),
|
|
||||||
)
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace);
|
|
||||||
},
|
|
||||||
NullValue: function () {
|
|
||||||
return parsimmon
|
|
||||||
.regexp(/null/i)
|
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.result(null)
|
|
||||||
.desc("NULL");
|
|
||||||
},
|
|
||||||
FuncValue: function (r) {
|
|
||||||
return parsimmon.seqMap(
|
|
||||||
parsimmon
|
|
||||||
.regexp(/([a-zA-Z0-9_]+)/, 1)
|
|
||||||
.skip(parsimmon.optWhitespace)
|
|
||||||
.desc("function"),
|
|
||||||
r.ExpressionList.wrap(
|
|
||||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
|
||||||
parsimmon.string(")").skip(parsimmon.optWhitespace),
|
|
||||||
),
|
|
||||||
(f, args) => ["FUNC", f.toUpperCase()].concat(args),
|
|
||||||
);
|
);
|
||||||
},
|
result = result ? new Expression.Binary("AND", result, pair) : pair;
|
||||||
WhenPair: function (r) {
|
|
||||||
return parsimmon.seq(
|
cursor.skipwhitespace();
|
||||||
parsimmon
|
if ((cursor.charCode as number) !== CHAR_COMMA) break;
|
||||||
.regexp(/when/i)
|
cursor.step();
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
}
|
||||||
.skip(parsimmon.optWhitespace)
|
return result;
|
||||||
.desc("WHEN")
|
}
|
||||||
.then(r.Expression),
|
|
||||||
parsimmon
|
export function parsePath(cur: Cursor): Path {
|
||||||
.regexp(/then/i)
|
const segments: (string | Expression)[] = [];
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
let colon = 0;
|
||||||
.skip(parsimmon.optWhitespace)
|
const cur2 = cur.fork();
|
||||||
.desc("THEN")
|
for (;;) {
|
||||||
.then(r.Expression),
|
let char = cur2.charCode;
|
||||||
);
|
let exp: Expression;
|
||||||
},
|
|
||||||
CaseStatement: function (r) {
|
if (char === CHAR_OPEN_BRACKET) {
|
||||||
return parsimmon.seqMap(
|
if (cur2.pos !== cur.pos) throw new Error("Invalid path");
|
||||||
parsimmon
|
cur2.step();
|
||||||
.regexp(/case/i)
|
cur2.descend([CHAR_CLOSE_BRACKET]);
|
||||||
.result("CASE")
|
// Try parsing as expression first
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
const cur3 = cur2.fork();
|
||||||
.skip(parsimmon.optWhitespace)
|
let err: unknown;
|
||||||
.desc("CASE"),
|
try {
|
||||||
r.WhenPair.many(),
|
exp = parseExpression(cur3);
|
||||||
parsimmon
|
} catch (e) {
|
||||||
.regexp(/else/i)
|
err = e;
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
}
|
||||||
.skip(parsimmon.optWhitespace)
|
// Fall back to old alias format if not a valid expression or produced
|
||||||
.desc("ELSE")
|
// a bare Parameter (old format like [a:1] mis-parsed as a path).
|
||||||
.then(r.Expression)
|
if (err || exp instanceof Expression.Parameter) {
|
||||||
.map((e) => [[true, e]])
|
exp = parseOldAlias(cur2);
|
||||||
.fallback(null)
|
} else {
|
||||||
.skip(
|
cur2.sync(cur3);
|
||||||
parsimmon
|
}
|
||||||
.regex(/end/i)
|
cur2.ascend();
|
||||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)),
|
char = cur2.charCode;
|
||||||
)
|
if (char !== CHAR_CLOSE_BRACKET) throw new Error("Expected ']'");
|
||||||
.skip(parsimmon.optWhitespace),
|
char = cur2.step().charCode;
|
||||||
(...arr) => arr.flat(2),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Value: function (r) {
|
|
||||||
return parsimmon.alt(
|
|
||||||
r.NullValue,
|
|
||||||
r.BooleanValue,
|
|
||||||
r.NumberValue,
|
|
||||||
r.StringValueSql,
|
|
||||||
r.StringValueJs,
|
|
||||||
r.FuncValue,
|
|
||||||
r.CaseStatement,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ValueExpression: function (r) {
|
|
||||||
return binaryLeft(
|
|
||||||
parsimmon.string("||").skip(parsimmon.optWhitespace),
|
|
||||||
binaryLeft(
|
|
||||||
parsimmon
|
|
||||||
.alt(parsimmon.string("+"), parsimmon.string("-"))
|
|
||||||
.skip(parsimmon.optWhitespace),
|
|
||||||
binaryLeft(
|
|
||||||
parsimmon
|
|
||||||
.alt(
|
|
||||||
parsimmon.string("*"),
|
|
||||||
parsimmon.string("/"),
|
|
||||||
parsimmon.string("%"),
|
|
||||||
)
|
|
||||||
.skip(parsimmon.optWhitespace),
|
|
||||||
parsimmon.alt(
|
|
||||||
r.Value,
|
|
||||||
r.Parameter,
|
|
||||||
r.Expression.wrap(
|
|
||||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
|
||||||
parsimmon.string(")").skip(parsimmon.optWhitespace),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Comparison: function (r) {
|
|
||||||
return parsimmon.alt(
|
|
||||||
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
|
|
||||||
parsimmon.seqMap(
|
|
||||||
r.ValueExpression,
|
|
||||||
r.ComparisonOperator,
|
|
||||||
r.ValueExpression,
|
|
||||||
(p, o, v) => [o, p, v],
|
|
||||||
),
|
|
||||||
parsimmon.seqMap(
|
|
||||||
r.ValueExpression,
|
|
||||||
r.LikeOperator,
|
|
||||||
r.ValueExpression.skip(
|
|
||||||
parsimmon
|
|
||||||
.regexp(/escape/i)
|
|
||||||
.result("ESCAPE")
|
|
||||||
.skip(parsimmon.whitespace)
|
|
||||||
.desc("ESCAPE"),
|
|
||||||
),
|
|
||||||
r.ValueExpression,
|
|
||||||
(a, b, c, d) => [b, a, c, d],
|
|
||||||
),
|
|
||||||
parsimmon.seqMap(
|
|
||||||
r.ValueExpression,
|
|
||||||
r.LikeOperator,
|
|
||||||
r.ValueExpression,
|
|
||||||
(a, b, c) => [b, a, c],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ExpressionList: function (r) {
|
|
||||||
return r.Expression.sepBy(
|
|
||||||
parsimmon.string(",").skip(parsimmon.optWhitespace),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
Expression: function (r) {
|
|
||||||
function unary(
|
|
||||||
operatorsParser: parsimmon.Parser<string>,
|
|
||||||
nextParser: parsimmon.Parser<Expression>,
|
|
||||||
): parsimmon.Parser<Expression> {
|
|
||||||
return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return binaryLeft(
|
if (!PATH_CHARS.has(char)) {
|
||||||
r.OrOperator,
|
if (cur2.pos <= cur.pos) throw new Error("Invalid path");
|
||||||
binaryLeft(
|
|
||||||
r.AndOperator,
|
|
||||||
unary(r.NotOperator, r.Comparison.or(r.ValueExpression)),
|
|
||||||
),
|
|
||||||
).trim(parsimmon.optWhitespace);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function parse(str: string): Expression {
|
if (char === CHAR_COLON) {
|
||||||
if (!str) return null;
|
if (colon) throw new Error("Multiple colons");
|
||||||
return lang.Expression.tryParse(str);
|
colon = segments.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exp) segments.push(exp);
|
||||||
|
else segments.push(cur.read(cur2));
|
||||||
|
|
||||||
|
if (char !== CHAR_PERIOD && char !== CHAR_COLON) break;
|
||||||
|
cur2.step();
|
||||||
|
cur.sync(cur2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exp) throw new Error("Invalid path");
|
||||||
|
cur2.step();
|
||||||
|
}
|
||||||
|
cur.sync(cur2);
|
||||||
|
|
||||||
|
if (colon) colon = segments.length - colon;
|
||||||
|
return new Path(segments, colon);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseList(str: string): Expression[] {
|
export function stringifyExpression(exp: Expression, level = 0): string {
|
||||||
if (!str) return [];
|
function wrap(e: string, op: string): string {
|
||||||
return lang.ExpressionList.tryParse(str);
|
if (PRECEDENCE[op] <= level) return `(${e})`;
|
||||||
}
|
|
||||||
|
|
||||||
export function stringify(exp: Expression, level = 0): string {
|
|
||||||
if (!Array.isArray(exp)) return JSON.stringify(exp);
|
|
||||||
|
|
||||||
const opLevels = {
|
|
||||||
OR: 10,
|
|
||||||
AND: 11,
|
|
||||||
NOT: 12,
|
|
||||||
"=": 20,
|
|
||||||
"<>": 20,
|
|
||||||
">": 20,
|
|
||||||
">=": 20,
|
|
||||||
"<": 20,
|
|
||||||
"<=": 20,
|
|
||||||
LIKE: 20,
|
|
||||||
"NOT LIKE": 20,
|
|
||||||
"IS NULL": 20,
|
|
||||||
"IS NOT NULL": 20,
|
|
||||||
"||": 30,
|
|
||||||
"+": 31,
|
|
||||||
"-": 31,
|
|
||||||
"*": 32,
|
|
||||||
"/": 32,
|
|
||||||
"%": 32,
|
|
||||||
};
|
|
||||||
|
|
||||||
const op = exp[0].toUpperCase();
|
|
||||||
|
|
||||||
function wrap(e): string {
|
|
||||||
if (opLevels[op] <= level) return `(${e})`;
|
|
||||||
else return e;
|
else return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op === "FUNC") {
|
if (exp instanceof Expression.Literal) {
|
||||||
|
if (exp.value == null) return "NULL";
|
||||||
|
if (exp.value === true) return "TRUE";
|
||||||
|
if (exp.value === false) return "FALSE";
|
||||||
|
return JSON.stringify(exp.value);
|
||||||
|
} else if (exp instanceof Expression.Unary) {
|
||||||
|
if (exp.operator === "NOT") {
|
||||||
|
return wrap(
|
||||||
|
`NOT ${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])}`,
|
||||||
|
"NOT",
|
||||||
|
);
|
||||||
|
} else if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") {
|
||||||
|
return wrap(
|
||||||
|
`${stringifyExpression(exp.operand, PRECEDENCE[exp.operator])} ${exp.operator}`,
|
||||||
|
exp.operator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (exp instanceof Expression.Binary) {
|
||||||
|
const op = exp.operator;
|
||||||
|
if (!(op in PRECEDENCE)) throw new Error("Invalid operator");
|
||||||
return wrap(
|
return wrap(
|
||||||
`${exp[1]}(${exp
|
`${stringifyExpression(exp.left, PRECEDENCE[op] - 1)} ${exp.operator} ${stringifyExpression(exp.right, PRECEDENCE[op])}`,
|
||||||
.slice(2)
|
op,
|
||||||
.map((e) => stringify(e))
|
|
||||||
.join(", ")})`,
|
|
||||||
);
|
);
|
||||||
} else if (op === "PARAM") {
|
} else if (exp instanceof Expression.Parameter) {
|
||||||
if (typeof exp[1] === "string") {
|
return exp.path.toString();
|
||||||
return wrap(exp[1]);
|
} else if (exp instanceof Expression.FunctionCall) {
|
||||||
} else if (Array.isArray(exp[1]) && exp[1][0] === "||") {
|
return `${exp.name}(${exp.args.map((a) => stringifyExpression(a)).join(", ")})`;
|
||||||
return wrap(
|
} else if (exp instanceof Expression.Conditional) {
|
||||||
exp[1]
|
let str = `CASE WHEN ${stringifyExpression(exp.condition)} THEN ${stringifyExpression(exp.then)}`;
|
||||||
.slice(1)
|
if (
|
||||||
.map((p) => {
|
exp.otherwise instanceof Expression.Literal &&
|
||||||
if (typeof p === "string") return p;
|
exp.otherwise.value == null
|
||||||
else return `{${stringify(p)}}`;
|
)
|
||||||
})
|
str += " END";
|
||||||
.join(""),
|
else if (exp.otherwise instanceof Expression.Conditional)
|
||||||
);
|
str += stringifyExpression(exp.otherwise).slice(4);
|
||||||
} else {
|
else str += ` ELSE ${stringifyExpression(exp.otherwise)} END`;
|
||||||
return wrap(`{${stringify(exp[1])}}`);
|
return str;
|
||||||
}
|
|
||||||
} else if (op === "IS NULL" || op === "IS NOT NULL") {
|
|
||||||
return wrap(`${stringify(exp[1], opLevels[op])} ${op}`);
|
|
||||||
} else if (op === "LIKE" || op === "NOT LIKE") {
|
|
||||||
if (exp[3]) {
|
|
||||||
return wrap(
|
|
||||||
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
|
||||||
exp[2],
|
|
||||||
opLevels[op],
|
|
||||||
)} ESCAPE ${stringify(exp[3], opLevels[op])}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return wrap(
|
|
||||||
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
|
||||||
exp[2],
|
|
||||||
opLevels[op],
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (op === "CASE") {
|
|
||||||
const parts: string[] = ["CASE"];
|
|
||||||
for (let i = 1; i < exp.length - 1; i += 2) {
|
|
||||||
if (!Array.isArray(exp[i]) && exp[i]) {
|
|
||||||
if (exp[i + 1] != null) parts.push("ELSE", stringify(exp[i + 1]));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
parts.push("WHEN", stringify(exp[i]), "THEN", stringify(exp[i + 1]));
|
|
||||||
}
|
|
||||||
parts.push("END");
|
|
||||||
return parts.join(" ");
|
|
||||||
} else if (op in opLevels) {
|
|
||||||
const parts = exp.slice(1).map((e, i) => {
|
|
||||||
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (op === "NOT") return wrap(`${op} ${parts[0]}`);
|
|
||||||
else return wrap(parts.join(` ${op} `));
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unrecognized operator ${exp[0]}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseLikePattern(pat: string, esc: string): string[] {
|
export function parseLikePattern(pat: string, esc: string): string[] {
|
||||||
@@ -457,3 +525,33 @@ export function parseLikePattern(pat: string, esc: string): string[] {
|
|||||||
}
|
}
|
||||||
return chars.filter((c) => c);
|
return chars.filter((c) => c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
|
||||||
|
const convChars = {
|
||||||
|
"-": "\\-",
|
||||||
|
"/": "\\/",
|
||||||
|
"\\": "\\/",
|
||||||
|
"^": "\\^",
|
||||||
|
$: "\\$",
|
||||||
|
"*": "\\*",
|
||||||
|
"+": "\\+",
|
||||||
|
"?": "\\?",
|
||||||
|
".": "\\.",
|
||||||
|
"(": "\\(",
|
||||||
|
")": "\\)",
|
||||||
|
"|": "\\|",
|
||||||
|
"[": "\\[",
|
||||||
|
"]": "\\]",
|
||||||
|
"{": "\\{",
|
||||||
|
"}": "\\}",
|
||||||
|
"\\%": ".*",
|
||||||
|
"\\_": ".",
|
||||||
|
};
|
||||||
|
let chars = parseLikePattern(pat, esc);
|
||||||
|
if (!chars.length) return new RegExp("^$", flags);
|
||||||
|
chars = chars.map((c) => convChars[c] || c);
|
||||||
|
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
||||||
|
const l = chars.length - 1;
|
||||||
|
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
||||||
|
return new RegExp(chars.join(""), flags);
|
||||||
|
}
|
||||||
+295
-374
@@ -1,7 +1,7 @@
|
|||||||
import { espresso, complement, tautology } from "espresso-iisojs";
|
import { espresso, complement, tautology } from "espresso-iisojs";
|
||||||
import { Expression } from "../../types.ts";
|
import Expression from "../expression.ts";
|
||||||
|
import { parseLikePattern } from "./parser.ts";
|
||||||
import normalize from "./normalize.ts";
|
import normalize from "./normalize.ts";
|
||||||
import { map, parseLikePattern } from "./parser.ts";
|
|
||||||
|
|
||||||
type Minterm = number[];
|
type Minterm = number[];
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export abstract class SynthContextBase<
|
|||||||
return this.clauses.get(v);
|
return this.clauses.get(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getMinterms(exp: T, res: true | false | null): Minterm[];
|
abstract getMinterms(exp: T, res: number): Minterm[];
|
||||||
abstract getDcSet(minterms: Minterm[]): Minterm[];
|
abstract getDcSet(minterms: Minterm[]): Minterm[];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
canRaise(idx: number, set: Set<number>): boolean {
|
canRaise(idx: number, set: Set<number>): boolean {
|
||||||
@@ -67,36 +67,34 @@ export abstract class SynthContextBase<
|
|||||||
}
|
}
|
||||||
|
|
||||||
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
|
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
|
||||||
if (!Array.isArray(exp)) return;
|
if (exp instanceof Expression.Literal) return;
|
||||||
const op = exp[0];
|
else if (exp instanceof Expression.Unary) {
|
||||||
if (op === "IS NULL" || op === "IS NOT NULL") return;
|
if (exp.operator === "IS NULL" || exp.operator === "IS NOT NULL") return;
|
||||||
|
yield* findIsNullDeps(exp.operand);
|
||||||
if (op === "FUNC") {
|
} else if (exp instanceof Expression.Binary) {
|
||||||
if (exp[1] === "NOW") {
|
yield* findIsNullDeps(exp.left);
|
||||||
return;
|
yield* findIsNullDeps(exp.right);
|
||||||
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") {
|
} else if (exp instanceof Expression.FunctionCall) {
|
||||||
yield* findIsNullDeps(exp[2]);
|
if (exp.name === "NOW") return;
|
||||||
} else if (exp[1] === "ROUND") {
|
else if (exp.name === "LOWER" || exp.name === "UPPER")
|
||||||
for (const e of exp.slice(2, 4)) yield* findIsNullDeps(e);
|
yield* findIsNullDeps(exp.args[0]);
|
||||||
return;
|
else if (exp.name === "ROUND") {
|
||||||
|
for (const e of exp.args.slice(0, 2)) yield* findIsNullDeps(e);
|
||||||
}
|
}
|
||||||
} else if (op !== "PARAM") {
|
} else yield exp;
|
||||||
for (const e of exp.slice(1)) yield* findIsNullDeps(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
yield exp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Clause {
|
export abstract class Clause {
|
||||||
private _isNullable: Set<string>;
|
private _isNullable: Set<string>;
|
||||||
protected _expression: Expression;
|
protected _expression: Expression;
|
||||||
abstract true(context: SynthContextBase<Clause>): Minterm[];
|
abstract getMinterms(
|
||||||
abstract false(context: SynthContextBase<Clause>): Minterm[];
|
context: SynthContextBase<Clause>,
|
||||||
abstract null(context: SynthContextBase<Clause>): Minterm[];
|
res: number,
|
||||||
|
): Minterm[];
|
||||||
expression(): Expression {
|
expression(): Expression {
|
||||||
if (this._expression !== undefined) return this._expression;
|
if (this._expression !== undefined) return this._expression;
|
||||||
const context = createSynthContext();
|
const context = createSynthContext();
|
||||||
const minterms = this.true(context);
|
const minterms = this.getMinterms(context, 0b100);
|
||||||
const minimized = context.minimize(minterms);
|
const minimized = context.minimize(minterms);
|
||||||
this._expression = context.toExpression(minimized) as Expression;
|
this._expression = context.toExpression(minimized) as Expression;
|
||||||
return this._expression;
|
return this._expression;
|
||||||
@@ -120,7 +118,7 @@ export abstract class Clause {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return JSON.stringify(this.expression());
|
return this.expression().toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,68 +128,11 @@ export namespace Clause {
|
|||||||
constructor(public operand: Clause) {
|
constructor(public operand: Clause) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
return this.operand.false(context);
|
let r = res & 0b001;
|
||||||
}
|
if (res & 0b010) r |= 0b100;
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
if (res & 0b100) r |= 0b010;
|
||||||
return this.operand.true(context);
|
return this.operand.getMinterms(context, r);
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.operand.null(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Or extends Clause {
|
|
||||||
constructor(public operands: Clause[]) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
for (const s of this.operands) {
|
|
||||||
const m = s.true(context);
|
|
||||||
if (m.length === 1 && !m[0].length) return [[]];
|
|
||||||
minterms.push(...m);
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[][] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const e of operands) {
|
|
||||||
if (e instanceof Or) {
|
|
||||||
operands.push(...e.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const m = e.false(context);
|
|
||||||
if (!m.length) return [];
|
|
||||||
minterms.push(m);
|
|
||||||
}
|
|
||||||
if (!minterms.length) return [[]];
|
|
||||||
if (minterms.length === 1) return minterms[0];
|
|
||||||
return complement(minterms.map((m) => complement(m)).flat());
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const trueMinterms: Minterm[] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const s of operands) {
|
|
||||||
if (s instanceof Or) {
|
|
||||||
operands.push(...s.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const t = s.true(context);
|
|
||||||
if (t.length === 1 && !t[0].length) return [];
|
|
||||||
trueMinterms.push(...t);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullMinterms: Minterm[] = [];
|
|
||||||
for (const s of operands) {
|
|
||||||
const n = s.null(context);
|
|
||||||
if (n.length === 1 && !n[0].length && !trueMinterms.length) return [[]];
|
|
||||||
nullMinterms.push(...n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!trueMinterms.length) return nullMinterms;
|
|
||||||
return complement([...complement(nullMinterms), ...trueMinterms]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,175 +140,43 @@ export namespace Clause {
|
|||||||
constructor(public operands: Clause[]) {
|
constructor(public operands: Clause[]) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
const minterms: Minterm[][] = [];
|
res = res & 0b111;
|
||||||
const operands = [...this.operands];
|
if (!res) return [];
|
||||||
for (const s of operands) {
|
if (res === 0b111) return [[]];
|
||||||
if (s instanceof And) {
|
|
||||||
operands.push(...s.operands);
|
if (res === 0b110)
|
||||||
continue;
|
return [
|
||||||
}
|
...this.getMinterms(context, 0b010),
|
||||||
const m = s.true(context);
|
...this.getMinterms(context, 0b100),
|
||||||
if (!m.length) return [];
|
];
|
||||||
if (m.length === 1 && !m[0].length) continue;
|
|
||||||
minterms.push(m);
|
if (!(res & 0b010)) return complement(this.getMinterms(context, ~res));
|
||||||
}
|
|
||||||
if (!minterms.length) return [[]];
|
|
||||||
if (minterms.length === 1) return minterms[0];
|
|
||||||
return complement(minterms.map((m) => complement(m)).flat());
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
const minterms: Minterm[] = [];
|
||||||
for (const s of this.operands) {
|
for (const o of this.operands) {
|
||||||
const m = s.false(context);
|
const m = o.getMinterms(context, res);
|
||||||
if (m.length === 1 && !m[0].length) return [[]];
|
if (m.length === 1 && !m[0].length) return [[]];
|
||||||
minterms.push(...m);
|
minterms.push(...m);
|
||||||
}
|
}
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const falseMinterms: Minterm[] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const s of operands) {
|
|
||||||
if (s instanceof And) {
|
|
||||||
operands.push(...s.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const f = s.false(context);
|
|
||||||
if (f.length === 1 && !f[0].length) return [];
|
|
||||||
falseMinterms.push(...f);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullMinterms: Minterm[] = [];
|
|
||||||
for (const s of operands) {
|
|
||||||
const n = s.null(context);
|
|
||||||
if (n.length === 1 && !n[0].length && !falseMinterms.length)
|
|
||||||
return [[]];
|
|
||||||
nullMinterms.push(...n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!falseMinterms.length) return nullMinterms;
|
|
||||||
return complement([...complement(nullMinterms), ...falseMinterms]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Case extends Clause {
|
|
||||||
constructor(public clauses: Clause[]) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
const cumulative: Minterm[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
const w = this.clauses[i].true(context);
|
|
||||||
const t = this.clauses[i + 1].true(context);
|
|
||||||
minterms.push(
|
|
||||||
...complement([...cumulative, ...complement(w), ...complement(t)]),
|
|
||||||
);
|
|
||||||
if (i < this.clauses.length - 2) {
|
|
||||||
cumulative.push(
|
|
||||||
...complement([
|
|
||||||
...this.clauses[i].false(context),
|
|
||||||
...this.clauses[i].null(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return minterms;
|
return minterms;
|
||||||
}
|
}
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
const cumulative: Minterm[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
const w = this.clauses[i].true(context);
|
|
||||||
const t = this.clauses[i + 1].false(context);
|
|
||||||
minterms.push(
|
|
||||||
...complement([...cumulative, ...complement(w), ...complement(t)]),
|
|
||||||
);
|
|
||||||
if (i < this.clauses.length - 2) {
|
|
||||||
cumulative.push(
|
|
||||||
...complement([
|
|
||||||
...this.clauses[i].false(context),
|
|
||||||
...this.clauses[i].null(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
const cumulative: Minterm[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
const w = this.clauses[i].true(context);
|
|
||||||
const t = this.clauses[i + 1].null(context);
|
|
||||||
minterms.push(
|
|
||||||
...complement([...cumulative, ...complement(w), ...complement(t)]),
|
|
||||||
);
|
|
||||||
cumulative.push(
|
|
||||||
...complement([
|
|
||||||
...this.clauses[i].false(context),
|
|
||||||
...this.clauses[i].null(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
minterms.push(...complement([...cumulative]));
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
expression(): Expression {
|
|
||||||
if (this._expression != null) return this._expression;
|
|
||||||
if (this.isBoolean()) {
|
|
||||||
this._expression = new Clause.Not(new Clause.Not(this)).expression();
|
|
||||||
return this._expression;
|
|
||||||
}
|
|
||||||
const context = createSynthContext();
|
|
||||||
const cases: { when: Minterm[]; then: Expression }[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
let minterms = this.clauses[i].true(context);
|
|
||||||
const then = this.clauses[i + 1].expression();
|
|
||||||
if (
|
|
||||||
cases.length &&
|
|
||||||
JSON.stringify(then) === JSON.stringify(cases[cases.length - 1].then)
|
|
||||||
)
|
|
||||||
minterms.push(...cases.pop().when);
|
|
||||||
minterms = context.minimize(
|
|
||||||
minterms,
|
|
||||||
cases.flatMap((c) => c.when),
|
|
||||||
);
|
|
||||||
if (!minterms.length) continue;
|
|
||||||
cases.push({ when: minterms, then });
|
|
||||||
if (minterms.length === 1 && !minterms[0].length) break;
|
|
||||||
}
|
|
||||||
while (cases[cases.length - 1].then == null) cases.pop();
|
|
||||||
if (!cases.length) return null;
|
|
||||||
this._expression = [
|
|
||||||
"CASE",
|
|
||||||
...cases.flatMap((c) => [context.toExpression(c.when), c.then]),
|
|
||||||
];
|
|
||||||
return this._expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
isBoolean(): boolean {
|
|
||||||
for (let i = 1; i < this.clauses.length; i += 2)
|
|
||||||
if (!this.clauses[i].isBoolean()) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IsNull extends Clause {
|
export class IsNull extends Clause {
|
||||||
private _boolean: Clause;
|
|
||||||
constructor(public operand: Clause) {
|
constructor(public operand: Clause) {
|
||||||
super();
|
super();
|
||||||
this._boolean = new Not(new Not(this));
|
|
||||||
}
|
}
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
return this.operand.null(context);
|
res = res & 0b111;
|
||||||
}
|
const minterms: Minterm[] = [];
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
if ((res & 0b110) === 0b110) return [[]];
|
||||||
return complement(this.true(context));
|
if (res & 0b100)
|
||||||
}
|
minterms.push(...this.operand.getMinterms(context, 0b001));
|
||||||
null(): Minterm[] {
|
if (res & 0b010)
|
||||||
return [];
|
minterms.push(...this.operand.getMinterms(context, 0b110));
|
||||||
|
return minterms;
|
||||||
}
|
}
|
||||||
isBoolean(): boolean {
|
isBoolean(): boolean {
|
||||||
return true;
|
return true;
|
||||||
@@ -378,7 +187,7 @@ export namespace Clause {
|
|||||||
expression(): Expression {
|
expression(): Expression {
|
||||||
const nullables = [...this.operand.getNullables()];
|
const nullables = [...this.operand.getNullables()];
|
||||||
if (nullables.length === 1 && nullables[0].operand === this.operand)
|
if (nullables.length === 1 && nullables[0].operand === this.operand)
|
||||||
return ["IS NULL", this.operand.expression()];
|
return new Expression.Unary("IS NULL", this.operand.expression());
|
||||||
return super.expression();
|
return super.expression();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,26 +196,26 @@ export namespace Clause {
|
|||||||
constructor(public exp: Expression) {
|
constructor(public exp: Expression) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
if (Array.isArray(this.exp)) return context.getMinterms(this, true);
|
if (!(this.exp instanceof Expression.Literal))
|
||||||
if (this.exp) return [[]];
|
return context.getMinterms(this, res);
|
||||||
return [];
|
if (this.exp.value == null) return res & 0b001 ? [[]] : [];
|
||||||
}
|
if (this.exp.value && res & 0b100) return [[]];
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
if (!this.exp.value && res & 0b010) return [[]];
|
||||||
if (Array.isArray(this.exp)) return context.getMinterms(this, false);
|
|
||||||
if (!this.exp && this.exp != null) return [[]];
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
if (Array.isArray(this.exp)) return context.getMinterms(this, null);
|
|
||||||
if (this.exp == null) return [[]];
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
expression(): Expression {
|
expression(): Expression {
|
||||||
return this.exp;
|
return this.exp;
|
||||||
}
|
}
|
||||||
isBoolean(): boolean {
|
isBoolean(): boolean {
|
||||||
return this.exp === true || this.exp === false || this.exp == null;
|
if (this.exp instanceof Expression.Literal) {
|
||||||
|
return (
|
||||||
|
this.exp.value === true ||
|
||||||
|
this.exp.value === false ||
|
||||||
|
this.exp.value == null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,14 +227,13 @@ export namespace Clause {
|
|||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
return context.getMinterms(this, true);
|
res = res & 0b111;
|
||||||
}
|
if (!res) return [];
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
if (res === 0b111) return [[]];
|
||||||
return context.getMinterms(this, false);
|
if (res === 0b001 || res === 0b110)
|
||||||
}
|
return this.lhs.getMinterms(context, res);
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
return context.getMinterms(this, res);
|
||||||
return this.lhs.null(context);
|
|
||||||
}
|
}
|
||||||
*getNullables(): IterableIterator<IsNull> {
|
*getNullables(): IterableIterator<IsNull> {
|
||||||
yield* this.lhs.getNullables();
|
yield* this.lhs.getNullables();
|
||||||
@@ -434,7 +242,11 @@ export namespace Clause {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
expression(): Expression {
|
expression(): Expression {
|
||||||
return [this.op, this.lhs.expression(), this.rhs];
|
return new Expression.Binary(
|
||||||
|
this.op,
|
||||||
|
this.lhs.expression(),
|
||||||
|
new Expression.Literal(this.rhs),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,15 +265,13 @@ export namespace Clause {
|
|||||||
const exp = lhs.expression();
|
const exp = lhs.expression();
|
||||||
let caseSensitive = true;
|
let caseSensitive = true;
|
||||||
let contradiction = false;
|
let contradiction = false;
|
||||||
if (Array.isArray(exp)) {
|
if (exp instanceof Expression.FunctionCall) {
|
||||||
if (exp[0] === "FUNC") {
|
if (exp.name === "UPPER" || exp.name === "LOWER") {
|
||||||
const func = exp[1];
|
const p =
|
||||||
if (func === "UPPER" || func === "LOWER") {
|
exp.name === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
|
||||||
const p = func === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
|
if (p === rhs) caseSensitive = false;
|
||||||
if (p === rhs) caseSensitive = false;
|
else contradiction = true;
|
||||||
else contradiction = true;
|
lhs = new Exp(exp.args[0]);
|
||||||
lhs = new Exp(exp[2]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.lhs = lhs;
|
this.lhs = lhs;
|
||||||
@@ -469,14 +279,13 @@ export namespace Clause {
|
|||||||
this.caseSensitive = caseSensitive;
|
this.caseSensitive = caseSensitive;
|
||||||
this.contradiction = contradiction;
|
this.contradiction = contradiction;
|
||||||
}
|
}
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
return context.getMinterms(this, true);
|
res = res & 0b111;
|
||||||
}
|
if (!res) return [];
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
if (res === 0b111) return [[]];
|
||||||
return context.getMinterms(this, false);
|
if (res === 0b001 || res === 0b110)
|
||||||
}
|
return this.lhs.getMinterms(context, res);
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
return context.getMinterms(this, res);
|
||||||
return this.lhs.null(context);
|
|
||||||
}
|
}
|
||||||
isBoolean(): boolean {
|
isBoolean(): boolean {
|
||||||
return true;
|
return true;
|
||||||
@@ -491,67 +300,157 @@ export namespace Clause {
|
|||||||
let lhs = this.lhs.expression();
|
let lhs = this.lhs.expression();
|
||||||
if (this.contradiction) {
|
if (this.contradiction) {
|
||||||
if (this.rhs === this.rhs.toLocaleUpperCase())
|
if (this.rhs === this.rhs.toLocaleUpperCase())
|
||||||
lhs = ["FUNC", "LOWER", lhs];
|
lhs = new Expression.FunctionCall("LOWER", [lhs]);
|
||||||
else lhs = ["FUNC", "UPPER", lhs];
|
else lhs = new Expression.FunctionCall("UPPER", [lhs]);
|
||||||
} else if (!this.caseSensitive) {
|
} else if (!this.caseSensitive) {
|
||||||
if (this.rhs === this.rhs.toLocaleUpperCase())
|
if (this.rhs === this.rhs.toLocaleUpperCase())
|
||||||
lhs = ["FUNC", "UPPER", lhs];
|
lhs = new Expression.FunctionCall("UPPER", [lhs]);
|
||||||
else lhs = ["FUNC", "LOWER", lhs];
|
else lhs = new Expression.FunctionCall("LOWER", [lhs]);
|
||||||
}
|
}
|
||||||
return ["LIKE", lhs, this.rhs, ...(this.esc ? [this.esc] : [])];
|
return new Expression.Binary(
|
||||||
|
"LIKE",
|
||||||
|
lhs,
|
||||||
|
new Expression.Literal(this.rhs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Conditional extends Clause {
|
||||||
|
constructor(
|
||||||
|
public condition: Clause,
|
||||||
|
public then: Clause,
|
||||||
|
public otherwise: Clause,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
getMinterms(context: SynthContextBase<Clause>, res: number): Minterm[] {
|
||||||
|
const condition = this.condition.getMinterms(context, 0b011);
|
||||||
|
if (!condition.length) return this.then.getMinterms(context, res);
|
||||||
|
return [
|
||||||
|
...complement([...condition, ...this.then.getMinterms(context, ~res)]),
|
||||||
|
...complement([
|
||||||
|
...complement(condition),
|
||||||
|
...this.otherwise.getMinterms(context, ~res),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
expression(): Expression {
|
||||||
|
if (this._expression != null) return this._expression;
|
||||||
|
if (this.isBoolean()) {
|
||||||
|
this._expression = new Clause.Not(new Clause.Not(this)).expression();
|
||||||
|
return this._expression;
|
||||||
|
}
|
||||||
|
const context = createSynthContext();
|
||||||
|
const cases: { when: Minterm[]; then: Expression }[] = [];
|
||||||
|
let clause = this as Conditional;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
let minterms = clause.condition.getMinterms(context, 0b100);
|
||||||
|
const then = clause.then.expression();
|
||||||
|
if (
|
||||||
|
cases.length &&
|
||||||
|
then.toString() === cases[cases.length - 1].then.toString()
|
||||||
|
)
|
||||||
|
minterms.push(...cases.pop().when);
|
||||||
|
minterms = context.minimize(
|
||||||
|
minterms,
|
||||||
|
cases.flatMap((c) => c.when),
|
||||||
|
);
|
||||||
|
if (!minterms.length) continue;
|
||||||
|
cases.push({ when: minterms, then });
|
||||||
|
if (minterms.length === 1 && !minterms[0].length) break;
|
||||||
|
if (clause.otherwise instanceof Conditional) clause = clause.otherwise;
|
||||||
|
else
|
||||||
|
clause = new Conditional(
|
||||||
|
new Exp(new Expression.Literal(true)),
|
||||||
|
clause.otherwise,
|
||||||
|
new Exp(new Expression.Literal(null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while (
|
||||||
|
cases[cases.length - 1].then instanceof Expression.Literal &&
|
||||||
|
(cases[cases.length - 1].then as Expression.Literal).value == null
|
||||||
|
)
|
||||||
|
cases.pop();
|
||||||
|
let res: Expression = new Expression.Literal(null);
|
||||||
|
while (cases.length) {
|
||||||
|
const c = cases.pop();
|
||||||
|
if (c.when.length === 1 && !c.when[0].length) {
|
||||||
|
res = c.then;
|
||||||
|
} else {
|
||||||
|
res = new Expression.Conditional(
|
||||||
|
context.toExpression(c.when),
|
||||||
|
c.then,
|
||||||
|
res,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._expression = res;
|
||||||
|
return this._expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBoolean(): boolean {
|
||||||
|
return this.then.isBoolean() && this.otherwise.isBoolean();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromExpression(exp: Expression): Clause {
|
export function fromExpression(exp: Expression): Clause {
|
||||||
const res = map(exp, (e) => {
|
let res: Clause;
|
||||||
if (!Array.isArray(e)) return new Clause.Exp(e);
|
let negate = false;
|
||||||
let op = e[0];
|
if (exp instanceof Expression.Unary) {
|
||||||
let negate = true;
|
let op = exp.operator;
|
||||||
|
negate = true;
|
||||||
|
if (op === "IS NOT NULL") op = "IS NULL";
|
||||||
|
else negate = false;
|
||||||
|
if (op === "NOT") res = new Clause.Not(fromExpression(exp.operand));
|
||||||
|
else if (op === "IS NULL") res = new IsNull(fromExpression(exp.operand));
|
||||||
|
} else if (exp instanceof Expression.Binary) {
|
||||||
|
let op = exp.operator;
|
||||||
|
negate = true;
|
||||||
if (op === "NOT LIKE") op = "LIKE";
|
if (op === "NOT LIKE") op = "LIKE";
|
||||||
else if (op === "IS NOT NULL") op = "IS NULL";
|
|
||||||
else if (op === "<>") op = "=";
|
else if (op === "<>") op = "=";
|
||||||
else if (op === ">=") op = "<";
|
else if (op === ">=") op = "<";
|
||||||
else if (op === "<=") op = ">";
|
else if (op === "<=") op = ">";
|
||||||
else negate = false;
|
else negate = false;
|
||||||
|
|
||||||
let clause: Clause;
|
if (op === "AND") {
|
||||||
if (op === "IS NULL") {
|
res = new Clause.And([
|
||||||
clause = new Clause.IsNull(e[1]);
|
fromExpression(exp.left),
|
||||||
} else if (op === "NOT") {
|
fromExpression(exp.right),
|
||||||
clause = new Clause.Not(e[1]);
|
]);
|
||||||
} else if (op === "OR") {
|
} else if (op === "OR") {
|
||||||
clause = new Clause.Or(e.slice(1));
|
negate = true;
|
||||||
} else if (op === "AND") {
|
res = new Clause.And([
|
||||||
clause = new Clause.And(e.slice(1));
|
new Clause.Not(fromExpression(exp.left)),
|
||||||
} else if (op === "CASE") {
|
new Clause.Not(fromExpression(exp.right)),
|
||||||
clause = new Clause.Case(e.slice(1));
|
]);
|
||||||
} else if (op === "LIKE") {
|
} else if (exp.right instanceof Expression.Literal) {
|
||||||
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
|
if (["=", ">", "<"].includes(op)) {
|
||||||
const esc = e[3] instanceof Exp ? e[3].expression() : null;
|
if (
|
||||||
if (typeof rhs === "string" && typeof esc === "string")
|
["boolean", "number", "string"].includes(typeof exp.right.value)
|
||||||
clause = new Clause.Like(e[1], rhs, esc);
|
) {
|
||||||
else if (typeof rhs === "string" && esc == null)
|
res = new Compare(
|
||||||
clause = new Clause.Like(e[1], rhs);
|
fromExpression(exp.left),
|
||||||
} else if (op === ">" || op === "<" || op === "=") {
|
op as ">" | "<" | "=",
|
||||||
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
|
exp.right.value,
|
||||||
if (["boolean", "number", "string"].includes(typeof rhs)) {
|
);
|
||||||
clause = new Clause.Compare(
|
}
|
||||||
e[1],
|
} else if (op === "LIKE") {
|
||||||
op,
|
if (typeof exp.right.value === "string")
|
||||||
rhs as boolean | number | string,
|
res = new Like(fromExpression(exp.left), exp.right.value);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (exp instanceof Expression.Conditional) {
|
||||||
|
res = new Conditional(
|
||||||
|
fromExpression(exp.condition),
|
||||||
|
fromExpression(exp.then),
|
||||||
|
fromExpression(exp.otherwise),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!clause) {
|
if (!res) res = new Exp(exp);
|
||||||
const args = e.slice(1).map((a) => (a as Clause).expression());
|
if (negate) res = new Not(res);
|
||||||
clause = new Clause.Exp([op, ...args]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (negate) clause = new Clause.Not(clause);
|
|
||||||
return clause;
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,11 +475,24 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinterms(clause: Clause, res: true | false | null): number[][] {
|
getMinterms(clause: Clause, res: number): number[][] {
|
||||||
const v = this.getVar(clause);
|
const v = this.getVar(clause);
|
||||||
if (res === true) return [[(v << 2) ^ 3]];
|
switch (res & 0b111) {
|
||||||
else if (res === false) return [[(v << 2) ^ 1]];
|
case 0b100:
|
||||||
else return [[v << 2, (v << 2) ^ 2]];
|
return [[(v << 2) ^ 3]];
|
||||||
|
case 0b010:
|
||||||
|
return [[(v << 2) ^ 1]];
|
||||||
|
case 0b001:
|
||||||
|
return [[v << 2, (v << 2) ^ 2]];
|
||||||
|
case 0b110:
|
||||||
|
return [[(v << 2) ^ 3], [(v << 2) ^ 1]];
|
||||||
|
case 0b101:
|
||||||
|
return [[(v << 2) ^ 3], [v << 2, (v << 2) ^ 2]];
|
||||||
|
case 0b011:
|
||||||
|
return [[(v << 2) ^ 1], [v << 2, (v << 2) ^ 2]];
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid minterms");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDcSet(minterms: Minterm[]): number[][] {
|
getDcSet(minterms: Minterm[]): number[][] {
|
||||||
@@ -594,9 +506,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
) as Clause.Compare[];
|
) as Clause.Compare[];
|
||||||
|
|
||||||
// Comparisons
|
// Comparisons
|
||||||
for (const [, clauses] of groupBy(comparisons, (c) =>
|
for (const [, clauses] of groupBy(comparisons, (c) => c.lhs.toString())) {
|
||||||
JSON.stringify(c.lhs),
|
|
||||||
)) {
|
|
||||||
const lhs = clauses[0].lhs;
|
const lhs = clauses[0].lhs;
|
||||||
const values = new Set(clauses.map((c) => c.rhs));
|
const values = new Set(clauses.map((c) => c.rhs));
|
||||||
const valuesSorted = [...values].sort((a, b) => {
|
const valuesSorted = [...values].sort((a, b) => {
|
||||||
@@ -657,7 +567,7 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
(c) => c instanceof Clause.Like,
|
(c) => c instanceof Clause.Like,
|
||||||
) as Clause.Like[];
|
) as Clause.Like[];
|
||||||
|
|
||||||
for (const [, clauses] of groupBy(likes, (c) => JSON.stringify(c.lhs))) {
|
for (const [, clauses] of groupBy(likes, (c) => c.lhs.toString())) {
|
||||||
for (let i1 = 0; i1 < clauses.length; ++i1) {
|
for (let i1 = 0; i1 < clauses.length; ++i1) {
|
||||||
const l1 = clauses[i1];
|
const l1 = clauses[i1];
|
||||||
if (l1.contradiction) {
|
if (l1.contradiction) {
|
||||||
@@ -724,9 +634,13 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (clause instanceof Clause.IsNull) {
|
if (clause instanceof Clause.IsNull) {
|
||||||
const str = clause.operand.toString();
|
if (clause.operand instanceof Clause.Exp) {
|
||||||
if (str === '["PARAM","DeviceID.ID"]' || str === '["PARAM","_id"]')
|
if (clause.operand.exp instanceof Expression.Parameter) {
|
||||||
dcSet.push([(v << 2) ^ 3]);
|
const str = clause.operand.exp.path.toString();
|
||||||
|
if (str === "DeviceID.ID" || str === "_id")
|
||||||
|
dcSet.push([(v << 2) ^ 3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2)));
|
return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2)));
|
||||||
@@ -808,11 +722,9 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toExpression(sop: number[][]): Expression {
|
toExpression(sop: number[][]): Expression {
|
||||||
if (!sop.length) return false;
|
let res: Expression = new Expression.Literal(false);
|
||||||
const res: Expression[] = [];
|
|
||||||
for (const s of sop) {
|
for (const s of sop) {
|
||||||
if (!s.length) return true;
|
let conjs: Expression = new Expression.Literal(true);
|
||||||
const conjs: Expression[] = [];
|
|
||||||
for (const i of s) {
|
for (const i of s) {
|
||||||
const clause = this.getClause(i >>> 2);
|
const clause = this.getClause(i >>> 2);
|
||||||
if (!clause) throw new Error("Invalid literal");
|
if (!clause) throw new Error("Invalid literal");
|
||||||
@@ -823,7 +735,8 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
const isNullVars = [...clause.getNullables()].map((c) =>
|
const isNullVars = [...clause.getNullables()].map((c) =>
|
||||||
this.getVar(c),
|
this.getVar(c),
|
||||||
);
|
);
|
||||||
conjs.push(
|
conjs = Expression.and(
|
||||||
|
conjs,
|
||||||
this.toExpression([
|
this.toExpression([
|
||||||
[i ^ 3],
|
[i ^ 3],
|
||||||
...isNullVars.map((n) => [(n << 2) ^ 3]),
|
...isNullVars.map((n) => [(n << 2) ^ 3]),
|
||||||
@@ -832,31 +745,38 @@ export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let expr = clause.expression() as Expression;
|
let expr = clause.expression();
|
||||||
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
|
if (!(i & 1) !== !(i & 2)) expr = new Expression.Unary("NOT", expr);
|
||||||
if (
|
if (expr instanceof Expression.Unary && expr.operator === "NOT") {
|
||||||
Array.isArray(expr) &&
|
const e = expr.operand;
|
||||||
expr[0] === "NOT" &&
|
if (e instanceof Expression.Unary) {
|
||||||
Array.isArray(expr[1])
|
if (e.operator === "IS NULL")
|
||||||
) {
|
expr = new Expression.Unary("IS NOT NULL", e.operand);
|
||||||
const e: Expression[] = expr[1];
|
else if (e.operator === "IS NOT NULL")
|
||||||
if (e[0] === "IS NULL") expr = ["IS NOT NULL", ...e.slice(1)];
|
expr = new Expression.Unary("IS NULL", e.operand);
|
||||||
else if (e[0] === "LIKE") expr = ["NOT LIKE", ...e.slice(1)];
|
else if (e.operator === "NOT") expr = e.operand;
|
||||||
else if (e[0] === "=") expr = ["<>", ...e.slice(1)];
|
} else if (e instanceof Expression.Binary) {
|
||||||
else if (e[0] === "<>") expr = ["=", ...e.slice(1)];
|
if (e.operator === "LIKE")
|
||||||
else if (e[0] === ">") expr = ["<=", ...e.slice(1)];
|
expr = new Expression.Binary("NOT LIKE", e.left, e.right);
|
||||||
else if (e[0] === ">=") expr = ["<", ...e.slice(1)];
|
else if (e.operator === "=")
|
||||||
else if (e[0] === "<") expr = [">=", ...e.slice(1)];
|
expr = new Expression.Binary("<>", e.left, e.right);
|
||||||
else if (e[0] === "<=") expr = [">", ...e.slice(1)];
|
else if (e.operator === "<>")
|
||||||
else if (e[0] === "NOT") expr = e[1];
|
expr = new Expression.Binary("=", e.left, e.right);
|
||||||
|
else if (e.operator === ">")
|
||||||
|
expr = new Expression.Binary("<=", e.left, e.right);
|
||||||
|
else if (e.operator === ">=")
|
||||||
|
expr = new Expression.Binary("<", e.left, e.right);
|
||||||
|
else if (e.operator === "<")
|
||||||
|
expr = new Expression.Binary(">=", e.left, e.right);
|
||||||
|
else if (e.operator === "<=")
|
||||||
|
expr = new Expression.Binary(">", e.left, e.right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conjs.push(expr);
|
conjs = Expression.and(conjs, expr);
|
||||||
}
|
}
|
||||||
if (conjs.length > 1) res.push(["AND", ...conjs]);
|
res = Expression.or(res, conjs);
|
||||||
else res.push(conjs[0]);
|
|
||||||
}
|
}
|
||||||
if (res.length > 1) return ["OR", ...res];
|
return res;
|
||||||
return res[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,11 +845,12 @@ export function unionDiff(
|
|||||||
): [Expression, Expression] {
|
): [Expression, Expression] {
|
||||||
expr2 = normalize(expr2);
|
expr2 = normalize(expr2);
|
||||||
|
|
||||||
if (!expr2) return [expr1, false];
|
if (expr2 instanceof Expression.Literal && !expr2.value)
|
||||||
|
return [expr1, new Expression.Literal(false)];
|
||||||
|
|
||||||
const synth2 = Clause.fromExpression(expr2);
|
const synth2 = Clause.fromExpression(expr2);
|
||||||
|
|
||||||
if (!expr1) {
|
if (expr1 instanceof Expression.Literal && !expr1.value) {
|
||||||
const e = synth2.expression();
|
const e = synth2.expression();
|
||||||
return [e, e];
|
return [e, e];
|
||||||
}
|
}
|
||||||
@@ -939,8 +860,8 @@ export function unionDiff(
|
|||||||
|
|
||||||
const context = new SynthContext();
|
const context = new SynthContext();
|
||||||
|
|
||||||
const expr2Minterms = synth2.true(context);
|
const expr2Minterms = synth2.getMinterms(context, 0b100);
|
||||||
const expr1Minterms = synth1.true(context);
|
const expr1Minterms = synth1.getMinterms(context, 0b100);
|
||||||
|
|
||||||
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
|
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
|
||||||
|
|
||||||
@@ -953,16 +874,16 @@ export function unionDiff(
|
|||||||
|
|
||||||
export function covers(expr1: Expression, expr2: Expression): boolean {
|
export function covers(expr1: Expression, expr2: Expression): boolean {
|
||||||
expr2 = normalize(expr2);
|
expr2 = normalize(expr2);
|
||||||
if (!expr2) return true;
|
if (expr2 instanceof Expression.Literal && !expr2.value) return true;
|
||||||
expr1 = normalize(expr1);
|
expr1 = normalize(expr1);
|
||||||
if (!Array.isArray(expr1) && expr1) return true;
|
if (expr1 instanceof Expression.Literal && expr1.value) return true;
|
||||||
|
|
||||||
const synt1 = Clause.fromExpression(expr1);
|
const synt1 = Clause.fromExpression(expr1);
|
||||||
const synt2 = Clause.fromExpression(expr2);
|
const synt2 = Clause.fromExpression(expr2);
|
||||||
|
|
||||||
const context = new SynthContext();
|
const context = new SynthContext();
|
||||||
const expr1Minterms = synt1.true(context);
|
const expr1Minterms = synt1.getMinterms(context, 0b100);
|
||||||
const expr2Minterms = synt2.true(context);
|
const expr2Minterms = synt2.getMinterms(context, 0b100);
|
||||||
|
|
||||||
return tautology([
|
return tautology([
|
||||||
...context.sanitizeMinterms(complement(expr2Minterms)),
|
...context.sanitizeMinterms(complement(expr2Minterms)),
|
||||||
|
|||||||
@@ -1,393 +0,0 @@
|
|||||||
import { map, mapAsync, parseLikePattern } from "./parser.ts";
|
|
||||||
import { Expression } from "../../types.ts";
|
|
||||||
|
|
||||||
const isArray = Array.isArray;
|
|
||||||
|
|
||||||
const regExpCache = new WeakMap<any, RegExp>();
|
|
||||||
|
|
||||||
const REDUCE_SKIP = {};
|
|
||||||
function reduce(exp, callback): Expression {
|
|
||||||
let loop = true;
|
|
||||||
while (loop) {
|
|
||||||
loop = false;
|
|
||||||
for (let i = 2; i < exp.length; ++i) {
|
|
||||||
const res = callback(exp[i - 1], exp[i], i - 2);
|
|
||||||
if (res !== REDUCE_SKIP) {
|
|
||||||
loop = true;
|
|
||||||
exp = exp.slice();
|
|
||||||
exp.splice(i - 1, 2, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exp.length === 2) return exp[1];
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
|
|
||||||
const convChars = {
|
|
||||||
"-": "\\-",
|
|
||||||
"/": "\\/",
|
|
||||||
"\\": "\\/",
|
|
||||||
"^": "\\^",
|
|
||||||
$: "\\$",
|
|
||||||
"*": "\\*",
|
|
||||||
"+": "\\+",
|
|
||||||
"?": "\\?",
|
|
||||||
".": "\\.",
|
|
||||||
"(": "\\(",
|
|
||||||
")": "\\)",
|
|
||||||
"|": "\\|",
|
|
||||||
"[": "\\[",
|
|
||||||
"]": "\\]",
|
|
||||||
"{": "\\{",
|
|
||||||
"}": "\\}",
|
|
||||||
"\\%": ".*",
|
|
||||||
"\\_": ".",
|
|
||||||
};
|
|
||||||
let chars = parseLikePattern(pat, esc);
|
|
||||||
if (!chars.length) return new RegExp("^$", flags);
|
|
||||||
chars = chars.map((c) => convChars[c] || c);
|
|
||||||
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
|
||||||
const l = chars.length - 1;
|
|
||||||
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
|
||||||
return new RegExp(chars.join(""), flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(
|
|
||||||
a: boolean | number | string,
|
|
||||||
b: boolean | number | string,
|
|
||||||
): number {
|
|
||||||
if (typeof a === "boolean") a = +a;
|
|
||||||
if (typeof b === "boolean") b = +b;
|
|
||||||
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
|
|
||||||
return a > b ? 1 : a < b ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toNumber(a: boolean | number | string): number {
|
|
||||||
switch (typeof a) {
|
|
||||||
case "number":
|
|
||||||
return a;
|
|
||||||
case "boolean":
|
|
||||||
return +a;
|
|
||||||
case "string":
|
|
||||||
return parseFloat(a) || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toString(a: boolean | number | string): string {
|
|
||||||
switch (typeof a) {
|
|
||||||
case "string":
|
|
||||||
return a;
|
|
||||||
case "number":
|
|
||||||
return a.toString();
|
|
||||||
case "boolean":
|
|
||||||
return (+a).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evaluateCallback(exp: Expression): Expression {
|
|
||||||
if (!Array.isArray(exp)) return exp;
|
|
||||||
if (exp[0] === "CASE") {
|
|
||||||
for (let i = 1; i < exp.length; i += 2) {
|
|
||||||
if (Array.isArray(exp[i])) return exp;
|
|
||||||
if (exp[i]) return exp[i + 1];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} else if (exp[0] === "FUNC") {
|
|
||||||
if (exp[1] === "COALESCE") {
|
|
||||||
const args: Expression[] = [];
|
|
||||||
for (let i = 2; i < exp.length; ++i) {
|
|
||||||
const e = exp[i];
|
|
||||||
if (e == null) continue;
|
|
||||||
args.push(e);
|
|
||||||
if (!Array.isArray(e)) break;
|
|
||||||
}
|
|
||||||
if (!args.length) return null;
|
|
||||||
if (args.length === 1) return args[0];
|
|
||||||
return ["FUNC", "COALESCE", ...args];
|
|
||||||
} else if (exp[1] === "UPPER") {
|
|
||||||
if (exp[2] == null) return null;
|
|
||||||
if (!isArray(exp[2])) return toString(exp[2]).toUpperCase();
|
|
||||||
} else if (exp[1] === "LOWER") {
|
|
||||||
if (exp[2] == null) return null;
|
|
||||||
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
|
|
||||||
} else if (exp[1] === "ROUND") {
|
|
||||||
const n = exp[2];
|
|
||||||
const p = exp.length > 3 ? exp[3] : 0;
|
|
||||||
if (n == null || p == null) return null;
|
|
||||||
if (!isArray(n) && !isArray(p)) {
|
|
||||||
const d = 10 ** p;
|
|
||||||
const m = n * d * (1 + Number.EPSILON);
|
|
||||||
return Math.round(m) / d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (exp[0] === "PARAM") {
|
|
||||||
if (exp[1] == null) return null;
|
|
||||||
} else if (exp[0] === "AND") {
|
|
||||||
for (let i = 1; i < exp.length; ++i)
|
|
||||||
if (!Array.isArray(exp[i]) && exp[i] != null && !exp[i]) return false;
|
|
||||||
const args: Expression[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const ee = exp[i];
|
|
||||||
if (ee == null) return null;
|
|
||||||
if (Array.isArray(ee)) {
|
|
||||||
if (ee[0] === "AND") args.push(...ee.slice(1));
|
|
||||||
else args.push(ee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!args.length) return true;
|
|
||||||
if (args.length === 1) args.push(true);
|
|
||||||
return ["AND", ...args];
|
|
||||||
} else if (exp[0] === "OR") {
|
|
||||||
const args: Expression[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const ee = exp[i];
|
|
||||||
if (Array.isArray(ee)) {
|
|
||||||
if (ee[0] === "OR") args.push(...ee.slice(1));
|
|
||||||
else args.push(ee);
|
|
||||||
} else if (ee) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!args.length) return exp.some((ee) => ee == null) ? null : false;
|
|
||||||
if (args.length === 1) args.push(false);
|
|
||||||
return ["OR", ...args];
|
|
||||||
} else if (exp[0] === "NOT") {
|
|
||||||
if (exp[1] == null) return null;
|
|
||||||
if (!isArray(exp[1])) return !exp[1];
|
|
||||||
else if (exp[1][0] === "NOT") return exp[1][1];
|
|
||||||
} else if (exp[0] === "IS NULL") {
|
|
||||||
if (isArray(exp[1])) return exp;
|
|
||||||
else if (exp[1] == null) return true;
|
|
||||||
else return false;
|
|
||||||
} else if (exp[0] === "IS NOT NULL") {
|
|
||||||
if (isArray(exp[1])) return exp;
|
|
||||||
else if (exp[1] != null) return true;
|
|
||||||
else return false;
|
|
||||||
} else if (exp[0] === "LIKE") {
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
|
||||||
else if (
|
|
||||||
exp[1] == null ||
|
|
||||||
exp[2] == null ||
|
|
||||||
((exp as any[]).length >= 4 && exp[3] == null)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
let r = regExpCache.get(exp as any[]);
|
|
||||||
if (!r) {
|
|
||||||
r = likePatternToRegExp(exp[2], exp[3]);
|
|
||||||
regExpCache.set(exp as any[], r);
|
|
||||||
}
|
|
||||||
return r.test(exp[1]);
|
|
||||||
} else if (exp[0] === "NOT LIKE") {
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
|
||||||
else if (
|
|
||||||
exp[1] == null ||
|
|
||||||
exp[2] == null ||
|
|
||||||
((exp as any[]).length >= 4 && exp[3] == null)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
let r = regExpCache.get(exp as any[]);
|
|
||||||
if (!r) {
|
|
||||||
r = likePatternToRegExp(exp[2], exp[3]);
|
|
||||||
regExpCache.set(exp as any[], r);
|
|
||||||
}
|
|
||||||
return !r.test(exp[1]);
|
|
||||||
} else if (exp[0] === "=") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) === 0;
|
|
||||||
} else if (exp[0] === "<>") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) !== 0;
|
|
||||||
} else if (exp[0] === ">") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) > 0;
|
|
||||||
} else if (exp[0] === ">=") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) >= 0;
|
|
||||||
} else if (exp[0] === "<") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) < 0;
|
|
||||||
} else if (exp[0] === "<=") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) <= 0;
|
|
||||||
} else if (exp[0] === "*") {
|
|
||||||
return reduce(exp, (a, b) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (!isArray(a) && !isArray(b)) return toNumber(a) * toNumber(b);
|
|
||||||
return REDUCE_SKIP;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "/") {
|
|
||||||
return reduce(exp, (a, b, i) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
|
|
||||||
const n1 = toNumber(a);
|
|
||||||
const n2 = toNumber(b);
|
|
||||||
if (i !== 0) return n1 * n2;
|
|
||||||
if (n2 === 0) return null;
|
|
||||||
return n1 / n2;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "+") {
|
|
||||||
return reduce(exp, (a, b) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (!isArray(a) && !isArray(b)) return toNumber(a) + toNumber(b);
|
|
||||||
return REDUCE_SKIP;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "-") {
|
|
||||||
return reduce(exp, (a, b, i) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (!isArray(a) && !isArray(b))
|
|
||||||
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
|
|
||||||
return REDUCE_SKIP;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "%") {
|
|
||||||
return reduce(exp, (a, b, i) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
|
|
||||||
const n1 = toNumber(a);
|
|
||||||
const n2 = Math.trunc(toNumber(b));
|
|
||||||
if (n2 === 0) return null;
|
|
||||||
return n1 % n2;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "||") {
|
|
||||||
return reduce(exp, (a, b) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (!isArray(a) && !isArray(b)) return toString(a) + toString(b);
|
|
||||||
return REDUCE_SKIP;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evaluate(
|
|
||||||
exp: Expression,
|
|
||||||
obj: Record<string, unknown> | ((e: string) => Expression),
|
|
||||||
now: number,
|
|
||||||
cb?: (e: Expression) => Expression,
|
|
||||||
): string | number | boolean | null;
|
|
||||||
export function evaluate(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown> | ((e: string) => Expression),
|
|
||||||
now?: number,
|
|
||||||
cb?: (e: Expression) => Expression,
|
|
||||||
): Expression;
|
|
||||||
export function evaluate(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown> | ((e: string) => Expression),
|
|
||||||
now?: number,
|
|
||||||
cb?: (e: Expression) => Expression,
|
|
||||||
): Expression {
|
|
||||||
return map(exp, (e) => {
|
|
||||||
if (cb) e = cb(e);
|
|
||||||
if (!isArray(e)) return e;
|
|
||||||
|
|
||||||
if (e[0] === "FUNC" && e[1] === "NOW") {
|
|
||||||
if (now) return now;
|
|
||||||
} else if (e[0] === "PARAM") {
|
|
||||||
if (e[1] == null) return null;
|
|
||||||
if (obj && !isArray(e[1])) {
|
|
||||||
let v;
|
|
||||||
if (typeof obj === "function") v = obj(e[1]);
|
|
||||||
else v = obj[e[1]];
|
|
||||||
|
|
||||||
if (v == null) return null;
|
|
||||||
if (typeof v === "object") v = v.value ? v.value[0] : null;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evaluateCallback(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function evaluateAsync(
|
|
||||||
exp: Expression,
|
|
||||||
obj: Record<string, unknown>,
|
|
||||||
now: number,
|
|
||||||
cb?: (e: Expression) => Promise<Expression>,
|
|
||||||
): Promise<string | number | boolean | null>;
|
|
||||||
export async function evaluateAsync(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown>,
|
|
||||||
now?: number,
|
|
||||||
cb?: (e: Expression) => Promise<Expression>,
|
|
||||||
): Promise<Expression>;
|
|
||||||
export async function evaluateAsync(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown>,
|
|
||||||
now?: number,
|
|
||||||
cb?: (e: Expression) => Promise<Expression>,
|
|
||||||
): Promise<Expression> {
|
|
||||||
return mapAsync(exp, async (e) => {
|
|
||||||
if (cb) e = await cb(e);
|
|
||||||
if (!isArray(e)) return e;
|
|
||||||
|
|
||||||
if (e[0] === "FUNC") {
|
|
||||||
if (e[1] === "NOW") if (now) return now;
|
|
||||||
} else if (e[0] === "PARAM") {
|
|
||||||
if (e[1] == null) return null;
|
|
||||||
if (obj && !isArray(e[1])) {
|
|
||||||
let v = obj[e[1]];
|
|
||||||
if (v == null) return null;
|
|
||||||
if (typeof v === "object") v = v["value"] ? v["value"][0] : null;
|
|
||||||
return v as Expression;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evaluateCallback(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function and(exp1: Expression, exp2: Expression): Expression {
|
|
||||||
if (exp1 != null && !exp1) return false;
|
|
||||||
if (exp2 != null && !exp2) return false;
|
|
||||||
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
|
|
||||||
if (exp1 == null || exp2 == null) return null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(exp2) && exp1[0] === "AND") return exp1;
|
|
||||||
if (!Array.isArray(exp1) && exp2[0] === "AND") return exp2;
|
|
||||||
|
|
||||||
const res: Expression = ["AND"];
|
|
||||||
|
|
||||||
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1));
|
|
||||||
else res.push(exp1);
|
|
||||||
|
|
||||||
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1));
|
|
||||||
else res.push(exp2);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function or(exp1: Expression, exp2: Expression): Expression {
|
|
||||||
if (!Array.isArray(exp1) && exp1) return true;
|
|
||||||
if (!Array.isArray(exp2) && exp2) return true;
|
|
||||||
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
|
|
||||||
if (exp1 == null || exp2 == null) return null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(exp2) && exp1[0] === "OR") return exp1;
|
|
||||||
if (!Array.isArray(exp1) && exp2[0] === "OR") return exp2;
|
|
||||||
|
|
||||||
const res: Expression = ["OR"];
|
|
||||||
|
|
||||||
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1));
|
|
||||||
else res.push(exp1);
|
|
||||||
|
|
||||||
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1));
|
|
||||||
else res.push(exp2);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractParams(exp: Expression): Expression[] {
|
|
||||||
const params = new Set<Expression>();
|
|
||||||
map(exp, (e) => {
|
|
||||||
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
return Array.from(params);
|
|
||||||
}
|
|
||||||
+69
-65
@@ -1,40 +1,32 @@
|
|||||||
import Path from "./path.ts";
|
import Path from "./path.ts";
|
||||||
|
|
||||||
export default class PathSet {
|
export default class PathSet {
|
||||||
declare private lengthIndex: Set<Path>[];
|
private paramSegmentIndex: Map<string, Set<Path>>[] = [];
|
||||||
declare private fragmentIndex: Map<string, Set<Path>>[];
|
private attrSegmentIndex: Map<string, Set<Path>>[] = [];
|
||||||
declare private stringIndex: Map<string, Path>;
|
private stringIndex: Map<string, Path> = new Map();
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {}
|
||||||
this.lengthIndex = [];
|
|
||||||
this.fragmentIndex = [];
|
|
||||||
this.stringIndex = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get depth(): number {
|
public add(pathStr: string): Path {
|
||||||
return this.lengthIndex.length;
|
let path: Path = this.get(pathStr);
|
||||||
}
|
if (path) return path;
|
||||||
|
path = Path.parse(pathStr);
|
||||||
public add(path: Path): Path {
|
|
||||||
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
||||||
const p = this.get(path);
|
|
||||||
|
|
||||||
if (p) return p;
|
|
||||||
|
|
||||||
this.stringIndex.set(path.toString(), path);
|
this.stringIndex.set(path.toString(), path);
|
||||||
|
|
||||||
while (this.lengthIndex.length <= path.length) {
|
while (this.paramSegmentIndex.length < path.paramLength)
|
||||||
this.lengthIndex.push(new Set());
|
this.paramSegmentIndex.push(new Map());
|
||||||
// fragmentIndex is one less than lengthIndex
|
|
||||||
if (this.lengthIndex.length > 1) this.fragmentIndex.push(new Map());
|
|
||||||
}
|
|
||||||
|
|
||||||
const lengthIndex = this.lengthIndex[path.length];
|
while (this.attrSegmentIndex.length < path.attrLength)
|
||||||
lengthIndex.add(path);
|
this.attrSegmentIndex.push(new Map());
|
||||||
|
|
||||||
for (let i = 0; i < path.length; ++i) {
|
for (let i = 0; i < path.length; ++i) {
|
||||||
const fragment = path.segments[i] as string;
|
const fragment = path.segments[i] as string;
|
||||||
const fragmentIndex = this.fragmentIndex[i];
|
const fragmentIndex =
|
||||||
|
i < path.paramLength
|
||||||
|
? this.paramSegmentIndex[i]
|
||||||
|
: this.attrSegmentIndex[i - path.paramLength];
|
||||||
|
|
||||||
let fragmentIndexSet = fragmentIndex.get(fragment);
|
let fragmentIndexSet = fragmentIndex.get(fragment);
|
||||||
if (!fragmentIndexSet) {
|
if (!fragmentIndexSet) {
|
||||||
@@ -48,64 +40,76 @@ export default class PathSet {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(path: Path): Path {
|
public get(path: string): Path {
|
||||||
return this.stringIndex.get(path.toString()) || null;
|
return this.stringIndex.get(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public find(
|
public findCompat(
|
||||||
path: Path,
|
path: Path,
|
||||||
superset = false,
|
superset = false,
|
||||||
subset = false,
|
subset = false,
|
||||||
depth: number = path.length,
|
depth = path.length,
|
||||||
): Path[] {
|
): Path[] {
|
||||||
|
if (path.attrLength)
|
||||||
|
throw new Error("findCompat() does not support attribute paths");
|
||||||
|
|
||||||
|
depth = Math.min(31, depth);
|
||||||
|
const paramMask = (1 << path.paramLength) - 1;
|
||||||
|
let mask = ((0b10 << depth) - 1) & ~paramMask;
|
||||||
|
if (superset) mask |= ~path.wildcard & paramMask;
|
||||||
|
if (subset) mask |= path.wildcard;
|
||||||
|
return this.find(path, mask, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public find(path: Path, paramMask: number, attrMask: number): Path[] {
|
||||||
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
||||||
|
if (path.paramLength > this.paramSegmentIndex.length) return [];
|
||||||
|
if (path.attrLength > this.attrSegmentIndex.length) return [];
|
||||||
|
|
||||||
const len = path.length;
|
const emptySet: Set<Path> = new Set();
|
||||||
|
|
||||||
if (!superset && depth === len && (!subset || !path.wildcard)) {
|
const indexes: [Set<Path>, Set<Path>][] = [];
|
||||||
const p = this.get(path);
|
|
||||||
return p ? [p] : [];
|
const paramLengthMask = (1 << path.paramLength) - 1;
|
||||||
|
const attrLengthMask = (1 << path.attrLength) - 1;
|
||||||
|
|
||||||
|
const mask = (paramMask & paramLengthMask) | (attrMask << path.paramLength);
|
||||||
|
|
||||||
|
for (const [i, s] of path.segments.entries()) {
|
||||||
|
const b = 1 << i;
|
||||||
|
const m = mask & b;
|
||||||
|
const w = b & path.wildcard;
|
||||||
|
if (w && m) continue;
|
||||||
|
const idxSet =
|
||||||
|
i < path.paramLength
|
||||||
|
? this.paramSegmentIndex[i]
|
||||||
|
: this.attrSegmentIndex[i - path.paramLength];
|
||||||
|
const idx1 = idxSet.get(s as string) || emptySet;
|
||||||
|
let idx2 = emptySet;
|
||||||
|
if (m) idx2 = idxSet.get("*") || emptySet;
|
||||||
|
indexes.push([idx1, idx2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lengthIndex = this.lengthIndex.slice(len, depth + 1);
|
indexes.sort((a, b) => a[0].size + a[1].size - (b[0].size + b[1].size));
|
||||||
if (!lengthIndex.length) return [];
|
|
||||||
|
|
||||||
let res;
|
let res: Path[];
|
||||||
for (let i = len - 1; i >= 0; --i) {
|
|
||||||
let fragmentIndexSet2;
|
|
||||||
const fragmentIndex = this.fragmentIndex[i];
|
|
||||||
|
|
||||||
if ((path.wildcard >> i) & 1) {
|
if (!indexes.length) res = [...this.stringIndex.values()];
|
||||||
if (subset) continue;
|
else res = [...indexes[0][0], ...indexes[0][1]];
|
||||||
} else if (superset) {
|
|
||||||
fragmentIndexSet2 = fragmentIndex.get("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
const fragment = path.segments[i] as string;
|
const paramCover = ~paramLengthMask & paramMask;
|
||||||
const fragmentIndexSet1 = fragmentIndex.get(fragment);
|
const attrCover = ~attrLengthMask & attrMask;
|
||||||
|
|
||||||
if (!fragmentIndexSet1) {
|
res = res.filter(
|
||||||
if (!fragmentIndexSet2) return [];
|
(p) =>
|
||||||
if (!res) res = [...fragmentIndexSet2];
|
(1 << p.paramLength) & paramCover && (1 << p.attrLength) & attrCover,
|
||||||
else res = res.filter((r) => fragmentIndexSet2.has(r));
|
);
|
||||||
} else if (!fragmentIndexSet2) {
|
|
||||||
if (!res) res = [...fragmentIndexSet1];
|
for (let i = 1; i < indexes.length; ++i) {
|
||||||
else res = res.filter((r) => fragmentIndexSet1.has(r));
|
const [idx1, idx2] = indexes[i];
|
||||||
} else {
|
res = res.filter((p) => idx1.has(p) || idx2.has(p));
|
||||||
if (!res) {
|
|
||||||
res = [...fragmentIndexSet1, ...fragmentIndexSet2];
|
|
||||||
} else {
|
|
||||||
res = res.filter(
|
|
||||||
(r) => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!res.length) return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res) res = [].concat(...lengthIndex.map((a) => [...a]));
|
|
||||||
else res = res.filter((r) => lengthIndex.some((a) => a.has(r)));
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+75
-170
@@ -1,162 +1,31 @@
|
|||||||
type Segments = (string | Alias)[];
|
import Expression from "./expression.ts";
|
||||||
type Alias = [Path, string][];
|
import { Cursor, parsePath } from "./expression/parser.ts";
|
||||||
|
|
||||||
|
type Segments = (string | Expression)[];
|
||||||
|
|
||||||
let cache1 = new Map<string, Path>();
|
let cache1 = new Map<string, Path>();
|
||||||
let cache2 = new Map<string, Path>();
|
let cache2 = new Map<string, Path>();
|
||||||
|
|
||||||
const CHAR_SPACE = 32;
|
|
||||||
const CHAR_BACKSLASH = 92;
|
|
||||||
const CHAR_COLON = 58;
|
|
||||||
const CHAR_QUOTE = 34;
|
|
||||||
const CHAR_OPEN_BRACKET = 91;
|
|
||||||
const CHAR_CLOSE_BRACKET = 93;
|
|
||||||
const CHAR_ASTERISK = 42;
|
|
||||||
const CHAR_COMMA = 44;
|
|
||||||
const CHAR_DOT = 46;
|
|
||||||
|
|
||||||
function charCodeAt(str: string, index: number): number {
|
|
||||||
if (index >= str.length) return 0;
|
|
||||||
return str.charCodeAt(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function legalChar(c: number): boolean {
|
|
||||||
return (
|
|
||||||
(c >= 97 && c <= 122) ||
|
|
||||||
(c >= 65 && c <= 90) ||
|
|
||||||
(c >= 48 && c <= 57) ||
|
|
||||||
c === 95 ||
|
|
||||||
c === 45
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Path {
|
export default class Path {
|
||||||
declare public readonly segments: Segments;
|
declare public readonly segments: Segments;
|
||||||
|
declare public readonly colon: number;
|
||||||
declare public readonly wildcard: number;
|
declare public readonly wildcard: number;
|
||||||
declare public readonly alias: number;
|
declare public readonly alias: number;
|
||||||
declare protected _string: string;
|
declare protected _string: string;
|
||||||
declare protected _stringIndex: number[];
|
declare protected _stringIndex: number[];
|
||||||
|
|
||||||
protected static parseAliasValue(
|
constructor(segments: Segments, colon: number) {
|
||||||
pattern: string,
|
if (!(colon <= segments.length)) throw new Error("Invalid path");
|
||||||
index: number,
|
if (segments.length > 32) throw new Error("Path too long");
|
||||||
): { index: number; value: string } {
|
|
||||||
let i = index;
|
|
||||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
|
||||||
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
|
|
||||||
for (let j = i + 1; j < pattern.length; ++j) {
|
|
||||||
if (
|
|
||||||
pattern.charCodeAt(j) === CHAR_QUOTE &&
|
|
||||||
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
++j;
|
|
||||||
const v = JSON.parse(pattern.slice(i, j));
|
|
||||||
return { index: j, value: v };
|
|
||||||
} catch {
|
|
||||||
return { index, value: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { index, value: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; i < pattern.length; ++i) {
|
|
||||||
const c = pattern.charCodeAt(i);
|
|
||||||
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { index: i, value: pattern.slice(index, i).trim() };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static parseAliasPath(
|
|
||||||
pattern: string,
|
|
||||||
index: number,
|
|
||||||
): { index: number; path: Path } {
|
|
||||||
let i = index;
|
|
||||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
|
||||||
const { index: idx, segments } = Path.parsePath(pattern, i);
|
|
||||||
if (idx === i) return { index, path: null };
|
|
||||||
i = idx;
|
|
||||||
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
|
|
||||||
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
|
|
||||||
return { index: i + 1, path: new Path(segments) };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static parseAlias(
|
|
||||||
pattern: string,
|
|
||||||
index: number,
|
|
||||||
): { index: number; alias: Alias } {
|
|
||||||
const alias: Alias = [];
|
|
||||||
let i = index;
|
|
||||||
for (;;) {
|
|
||||||
let path: Path;
|
|
||||||
let value: string;
|
|
||||||
({ index: i, path } = Path.parseAliasPath(pattern, i));
|
|
||||||
if (!path) break;
|
|
||||||
({ index: i, value } = Path.parseAliasValue(pattern, i));
|
|
||||||
if (value == null) break;
|
|
||||||
alias.push([path, value]);
|
|
||||||
index = i;
|
|
||||||
|
|
||||||
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure identical expressions have idential string representation
|
|
||||||
alias.sort((a, b) => {
|
|
||||||
if (a[0].toString() > b[0].toString()) return 1;
|
|
||||||
else if (a[0].toString() < b[0].toString()) return -1;
|
|
||||||
else if (a[1] > b[1]) return 1;
|
|
||||||
else if (a[1] < b[1]) return -1;
|
|
||||||
else return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.freeze(alias);
|
|
||||||
return { index, alias };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static parsePath(
|
|
||||||
pattern: string,
|
|
||||||
index: number,
|
|
||||||
): { index: number; segments: Segments } {
|
|
||||||
const segments = [];
|
|
||||||
|
|
||||||
let segStart = index;
|
|
||||||
let wildcard = -1;
|
|
||||||
for (let i = index; i <= pattern.length; ++i) {
|
|
||||||
const c = charCodeAt(pattern, i);
|
|
||||||
if (c === CHAR_OPEN_BRACKET && i === segStart) {
|
|
||||||
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
|
|
||||||
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
|
|
||||||
segments.push(alias);
|
|
||||||
index = i = idx + 1;
|
|
||||||
segStart = i + 1;
|
|
||||||
} else if (c === CHAR_ASTERISK && i === segStart) {
|
|
||||||
wildcard = i + 1;
|
|
||||||
} else if (!legalChar(c) || i === wildcard) {
|
|
||||||
if (i === segStart) break;
|
|
||||||
const s = pattern.slice(segStart, i);
|
|
||||||
segments.push(s);
|
|
||||||
index = i;
|
|
||||||
if (c !== CHAR_DOT) break;
|
|
||||||
segStart = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.freeze(segments);
|
Object.freeze(segments);
|
||||||
return { index, segments };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(segments: Segments) {
|
|
||||||
let alias = 0;
|
let alias = 0;
|
||||||
let wildcard = 0;
|
let wildcard = 0;
|
||||||
const arr = segments.map((s, i) => {
|
const arr = segments.map((s, i) => {
|
||||||
if (Array.isArray(s)) {
|
if (s instanceof Expression) {
|
||||||
alias |= 1 << i;
|
alias |= 1 << i;
|
||||||
const parts = s.map(
|
return `[${s.toString()}]`;
|
||||||
(al) => `${al[0].toString()}:${JSON.stringify(al[1])}`,
|
|
||||||
);
|
|
||||||
return `[${parts.join(",")}]`;
|
|
||||||
} else if (s === "*") {
|
} else if (s === "*") {
|
||||||
wildcard |= 1 << i;
|
wildcard |= 1 << i;
|
||||||
}
|
}
|
||||||
@@ -167,23 +36,27 @@ export default class Path {
|
|||||||
const stringIndex = arr.map((s, i) => (offset += s.length) + i);
|
const stringIndex = arr.map((s, i) => (offset += s.length) + i);
|
||||||
|
|
||||||
this.segments = segments;
|
this.segments = segments;
|
||||||
|
this.colon = colon;
|
||||||
this.wildcard = wildcard;
|
this.wildcard = wildcard;
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
this._string = arr.join(".");
|
if (!colon) this._string = arr.join(".");
|
||||||
|
else
|
||||||
|
this._string =
|
||||||
|
arr.slice(0, -colon).join(".") + ":" + arr.slice(-colon).join(".");
|
||||||
this._stringIndex = stringIndex;
|
this._stringIndex = stringIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parse(str: string): Path {
|
public static parse(input: string): Path {
|
||||||
let path = cache1.get(str);
|
let path = cache1.get(input);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(input);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const { index, segments } = Path.parsePath(str, 0);
|
const cursor = new Cursor(input);
|
||||||
if (index < str.length) throw new Error("Invalid parameter path");
|
path = parsePath(cursor);
|
||||||
path = new Path(segments);
|
if (cursor.charCode) throw new Error("Unexpected character");
|
||||||
if (path.toString() !== str) cache1.set(path.toString(), path);
|
if (path.toString() !== input) cache1.set(path.toString(), path);
|
||||||
}
|
}
|
||||||
cache1.set(str, path);
|
cache1.set(input, path);
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@@ -192,6 +65,14 @@ export default class Path {
|
|||||||
return this.segments.length;
|
return this.segments.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get paramLength(): number {
|
||||||
|
return this.segments.length - this.colon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get attrLength(): number {
|
||||||
|
return this.colon;
|
||||||
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this._string;
|
return this._string;
|
||||||
}
|
}
|
||||||
@@ -200,25 +81,27 @@ export default class Path {
|
|||||||
if (start < 0) start = Math.max(0, this.segments.length + start);
|
if (start < 0) start = Math.max(0, this.segments.length + start);
|
||||||
if (end < 0) end = Math.max(0, this.segments.length + end);
|
if (end < 0) end = Math.max(0, this.segments.length + end);
|
||||||
|
|
||||||
let str;
|
if (start >= end) return Path.root;
|
||||||
if (start >= end) {
|
|
||||||
str = "";
|
let i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
|
||||||
} else {
|
// Include the ":" when slicing exactly at the colon boundary
|
||||||
const i1 = start > 0 ? this._stringIndex[start - 1] + 1 : 0;
|
if (this.colon && start === this.segments.length - this.colon) --i1;
|
||||||
const i2 =
|
const i2 =
|
||||||
end <= this.segments.length
|
end <= this.segments.length
|
||||||
? this._stringIndex[end - 1]
|
? this._stringIndex[end - 1]
|
||||||
: this._string.length;
|
: this._string.length;
|
||||||
str = this._string.slice(i1, i2);
|
const str = this._string.slice(i1, i2);
|
||||||
}
|
|
||||||
|
|
||||||
let path = cache1.get(str);
|
let path = cache1.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const segments = this.segments.slice(start, end);
|
const segments = this.segments.slice(start, end);
|
||||||
Object.freeze(segments);
|
const colon =
|
||||||
path = new Path(segments);
|
start <= this.segments.length - this.colon
|
||||||
|
? Math.max(0, this.colon - this.segments.length + end)
|
||||||
|
: 0;
|
||||||
|
path = new Path(segments, colon);
|
||||||
}
|
}
|
||||||
cache1.set(str, path);
|
cache1.set(str, path);
|
||||||
}
|
}
|
||||||
@@ -230,15 +113,28 @@ export default class Path {
|
|||||||
if (!path2._string) return this;
|
if (!path2._string) return this;
|
||||||
else if (!this._string) return path2;
|
else if (!this._string) return path2;
|
||||||
|
|
||||||
const str = `${this._string}.${path2._string}`;
|
if (this.colon && path2.colon && path2.colon < path2.segments.length)
|
||||||
|
throw new Error("Invalid path");
|
||||||
|
|
||||||
|
const colon = this.colon ? this.colon + path2.segments.length : path2.colon;
|
||||||
|
|
||||||
|
let str;
|
||||||
|
if (this.colon && path2.colon === path2.segments.length) {
|
||||||
|
// Right is all-colon; strip its ":" prefix and join with "."
|
||||||
|
str = `${this._string}.${path2._string.slice(1)}`;
|
||||||
|
} else if (path2.colon === path2.segments.length) {
|
||||||
|
// Left has no colon; right is all-colon; concatenate directly
|
||||||
|
str = `${this._string}${path2._string}`;
|
||||||
|
} else {
|
||||||
|
str = `${this._string}.${path2._string}`;
|
||||||
|
}
|
||||||
|
|
||||||
let path = cache1.get(str);
|
let path = cache1.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const segments = this.segments.concat(path2.segments);
|
const segments = this.segments.concat(path2.segments);
|
||||||
Object.freeze(segments);
|
path = new Path(segments, colon);
|
||||||
path = new Path(segments);
|
|
||||||
}
|
}
|
||||||
cache1.set(str, path);
|
cache1.set(str, path);
|
||||||
}
|
}
|
||||||
@@ -248,21 +144,30 @@ export default class Path {
|
|||||||
|
|
||||||
public stripAlias(): Path {
|
public stripAlias(): Path {
|
||||||
if (!this.alias) return this;
|
if (!this.alias) return this;
|
||||||
const segments = this.segments.map((s) => (Array.isArray(s) ? "*" : s));
|
const segments = this.segments.map((s) =>
|
||||||
const str = segments.join(".");
|
s instanceof Expression ? "*" : s,
|
||||||
|
);
|
||||||
|
let str: string;
|
||||||
|
if (!this.colon) str = segments.join(".");
|
||||||
|
else
|
||||||
|
str =
|
||||||
|
segments.slice(0, -this.colon).join(".") +
|
||||||
|
":" +
|
||||||
|
segments.slice(-this.colon).join(".");
|
||||||
|
|
||||||
let path = cache1.get(str);
|
let path = cache1.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
Object.freeze(segments);
|
path = new Path(segments, this.colon);
|
||||||
path = new Path(segments);
|
|
||||||
}
|
}
|
||||||
cache1.set(str, path);
|
cache1.set(str, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static root = new Path([], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|||||||
+24
-21
@@ -1,8 +1,7 @@
|
|||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
import * as dgram from "node:dgram";
|
import * as dgram from "node:dgram";
|
||||||
import * as http from "node:http";
|
import * as http from "node:http";
|
||||||
import { evaluateAsync } from "./common/expression/util.ts";
|
import Expression, { Value } from "./common/expression.ts";
|
||||||
import { Expression } from "./types.ts";
|
|
||||||
import * as auth from "./auth.ts";
|
import * as auth from "./auth.ts";
|
||||||
import * as extensions from "./extensions.ts";
|
import * as extensions from "./extensions.ts";
|
||||||
import * as debug from "./debug.ts";
|
import * as debug from "./debug.ts";
|
||||||
@@ -13,29 +12,33 @@ import * as logger from "../lib/logger.ts";
|
|||||||
|
|
||||||
async function extractAuth(
|
async function extractAuth(
|
||||||
exp: Expression,
|
exp: Expression,
|
||||||
dflt: any,
|
dflt: Value,
|
||||||
): Promise<[string, string, Expression]> {
|
): Promise<[string, string, Expression]> {
|
||||||
let username, password;
|
let username: string, password: string;
|
||||||
const _exp = await evaluateAsync(
|
const _exp = await exp.evaluateAsync(
|
||||||
exp,
|
|
||||||
{},
|
|
||||||
0,
|
|
||||||
async (e: Expression): Promise<Expression> => {
|
async (e: Expression): Promise<Expression> => {
|
||||||
if (!username && Array.isArray(e) && e[0] === "FUNC") {
|
if (e instanceof Expression.Parameter)
|
||||||
if (e[1] === "EXT") {
|
return new Expression.Literal(null);
|
||||||
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null;
|
if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(0);
|
||||||
|
if (!username) {
|
||||||
|
if (e.name === "EXT") {
|
||||||
|
if (!e.args.every((a) => a instanceof Expression.Literal))
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
const args = e.args.map((a) => a.value.toString());
|
||||||
|
if (typeof args[0] !== "string" || typeof args[1] !== "string")
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
|
||||||
for (let i = 4; i < e.length; i++)
|
const { fault, value } = await extensions.run(args);
|
||||||
if (Array.isArray(e[i])) return null;
|
if (fault) return new Expression.Literal(null);
|
||||||
|
return new Expression.Literal(value);
|
||||||
const { fault, value } = await extensions.run(e.slice(2));
|
} else if (e.name === "AUTH") {
|
||||||
return fault ? null : value;
|
if (e.args.every((a) => a instanceof Expression.Literal)) {
|
||||||
} else if (e[1] === "AUTH") {
|
username = `${e.args[0].value ?? ""}`;
|
||||||
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) {
|
password = `${e.args[1].value ?? ""}`;
|
||||||
username = e[2] || "";
|
}
|
||||||
password = e[3] || "";
|
return new Expression.Literal(dflt);
|
||||||
}
|
}
|
||||||
return dflt;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
|
|||||||
+105
-99
@@ -10,11 +10,7 @@ import * as config from "./config.ts";
|
|||||||
import { generateDeviceId, once, setTimeoutPromise } from "./util.ts";
|
import { generateDeviceId, once, setTimeoutPromise } from "./util.ts";
|
||||||
import * as soap from "./soap.ts";
|
import * as soap from "./soap.ts";
|
||||||
import * as session from "./session.ts";
|
import * as session from "./session.ts";
|
||||||
import {
|
import Expression, { Value } from "./common/expression.ts";
|
||||||
evaluateAsync,
|
|
||||||
evaluate,
|
|
||||||
extractParams,
|
|
||||||
} from "./common/expression/util.ts";
|
|
||||||
import * as cache from "./cache.ts";
|
import * as cache from "./cache.ts";
|
||||||
import * as lock from "./lock.ts";
|
import * as lock from "./lock.ts";
|
||||||
import * as localCache from "./cwmp/local-cache.ts";
|
import * as localCache from "./cwmp/local-cache.ts";
|
||||||
@@ -39,7 +35,6 @@ import {
|
|||||||
AcsRequest,
|
AcsRequest,
|
||||||
SessionFault,
|
SessionFault,
|
||||||
Fault,
|
Fault,
|
||||||
Expression,
|
|
||||||
SoapMessage,
|
SoapMessage,
|
||||||
InformRequest,
|
InformRequest,
|
||||||
Preset,
|
Preset,
|
||||||
@@ -80,7 +75,7 @@ async function authenticate(
|
|||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.auth",
|
"cwmp.auth",
|
||||||
);
|
);
|
||||||
if (authExpression == null) return true;
|
if (!authExpression) return true;
|
||||||
|
|
||||||
let authentication;
|
let authentication;
|
||||||
|
|
||||||
@@ -107,56 +102,65 @@ async function authenticate(
|
|||||||
authentication["body"] = body;
|
authentication["body"] = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await evaluateAsync(
|
const res = await authExpression.evaluateAsync(
|
||||||
authExpression,
|
|
||||||
{},
|
|
||||||
sessionContext.timestamp,
|
|
||||||
async (e: Expression): Promise<Expression> => {
|
async (e: Expression): Promise<Expression> => {
|
||||||
e = session.configContextCallback(sessionContext, e);
|
e = session.configContextCallback(sessionContext, e);
|
||||||
if (Array.isArray(e) && e[0] === "FUNC") {
|
if (e instanceof Expression.Parameter)
|
||||||
if (e[1] === "EXT") {
|
return new Expression.Literal(null);
|
||||||
if (typeof e[2] !== "string" || typeof e[3] !== "string") return null;
|
|
||||||
|
|
||||||
for (let i = 4; i < e.length; i++)
|
if (e instanceof Expression.FunctionCall) {
|
||||||
if (Array.isArray(e[i])) return null;
|
if (e.name === "NOW")
|
||||||
|
return new Expression.Literal(sessionContext.timestamp);
|
||||||
|
if (e.name === "EXT") {
|
||||||
|
if (!e.args.every((a) => a instanceof Expression.Literal))
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
|
||||||
const { fault, value } = await extensions.run(e.slice(2));
|
const args = e.args.map((a) => a.value.toString());
|
||||||
return fault ? null : value;
|
if (typeof args[0] !== "string" || typeof args[1] !== "string")
|
||||||
} else if (e[1] === "AUTH") {
|
return new Expression.Literal(null);
|
||||||
const username = e[2];
|
|
||||||
const password = e[3];
|
|
||||||
if (username != null && password != null && authentication) {
|
|
||||||
if (authentication["method"] === "Basic") {
|
|
||||||
return (
|
|
||||||
authentication["username"] === e[2] &&
|
|
||||||
authentication["password"] === e[3]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authentication["method"] === "Digest") {
|
const { fault, value } = await extensions.run(args);
|
||||||
const expected = auth.digest(
|
if (fault) return new Expression.Literal(null);
|
||||||
username,
|
return new Expression.Literal(value);
|
||||||
REALM,
|
} else if (e.name === "AUTH") {
|
||||||
password,
|
if (e.args.every((a) => a instanceof Expression.Literal)) {
|
||||||
authentication["nonce"],
|
const username = e.args[0].value;
|
||||||
"POST",
|
const password = e.args[1].value;
|
||||||
authentication["uri"],
|
if (username != null && password != null && authentication) {
|
||||||
authentication["qop"],
|
if (authentication["method"] === "Basic") {
|
||||||
authentication["body"],
|
return new Expression.Literal(
|
||||||
authentication["cnonce"],
|
authentication["username"] === username.toString() &&
|
||||||
authentication["nc"],
|
authentication["password"] === password.toString(),
|
||||||
);
|
);
|
||||||
return expected === authentication["response"];
|
}
|
||||||
|
|
||||||
|
if (authentication["method"] === "Digest") {
|
||||||
|
const expected = auth.digest(
|
||||||
|
username.toString(),
|
||||||
|
REALM,
|
||||||
|
password.toString(),
|
||||||
|
authentication["nonce"],
|
||||||
|
"POST",
|
||||||
|
authentication["uri"],
|
||||||
|
authentication["qop"],
|
||||||
|
body,
|
||||||
|
authentication["cnonce"],
|
||||||
|
authentication["nc"],
|
||||||
|
);
|
||||||
|
return new Expression.Literal(
|
||||||
|
expected === authentication["response"],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return new Expression.Literal(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res && !Array.isArray(res)) return true;
|
if (res instanceof Expression.Literal) return !!res.value;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -315,8 +319,7 @@ async function inform(
|
|||||||
const cookiesPath = localCache.getConfig(
|
const cookiesPath = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.cookiesPath",
|
"cwmp.cookiesPath",
|
||||||
{},
|
"",
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => session.configContextCallback(sessionContext, e),
|
(e) => session.configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -406,8 +409,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
const RETRY_DELAY = +localCache.getConfig(
|
const RETRY_DELAY = +localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.retryDelay",
|
"cwmp.retryDelay",
|
||||||
{},
|
300,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => session.configContextCallback(sessionContext, e),
|
(e) => session.configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -434,14 +436,14 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
deviceData.attributes.revision = 1;
|
deviceData.attributes.revision = 1;
|
||||||
|
|
||||||
const deviceEvents = {};
|
const deviceEvents = {};
|
||||||
for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) {
|
for (const p of deviceData.paths.find(Path.parse("Events"), 0b001, 0b1)) {
|
||||||
const attrs = deviceData.attributes.get(p);
|
const attrs = deviceData.attributes.get(p);
|
||||||
const t = attrs?.value[1][0] as number;
|
const t = attrs?.value[1][0] as number;
|
||||||
if (t >= sessionContext.timestamp)
|
if (t >= sessionContext.timestamp)
|
||||||
deviceEvents[p.segments[1] as string] = true;
|
deviceEvents[p.segments[1] as string] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters: { [name: string]: Path } = {};
|
const parameters = new Set<string>();
|
||||||
const filteredPresets: Preset[] = [];
|
const filteredPresets: Preset[] = [];
|
||||||
|
|
||||||
for (const preset of presets) {
|
for (const preset of presets) {
|
||||||
@@ -470,29 +472,31 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredPresets.push(preset);
|
const pre = { ...preset };
|
||||||
for (const k of extractParams(
|
const evalCallback = (e: Expression): Expression => {
|
||||||
evaluate(preset.precondition, null, sessionContext.timestamp),
|
if (e instanceof Expression.FunctionCall && e.name === "NOW")
|
||||||
)) {
|
return new Expression.Literal(sessionContext.timestamp);
|
||||||
// Mark channel in case of fault during fetching precondition
|
if (e instanceof Expression.Parameter) {
|
||||||
sessionContext.channels[preset.channel] = 0;
|
// Mark channel in case of fault during fetching precondition
|
||||||
if (typeof k === "string") parameters[k] = Path.parse(k);
|
sessionContext.channels[preset.channel] = 0;
|
||||||
}
|
parameters.add(e.path.toString());
|
||||||
for (const prov of preset.provisions) {
|
|
||||||
for (const arg of prov.slice(1)) {
|
|
||||||
for (const k of extractParams(
|
|
||||||
evaluate(arg, null, sessionContext.timestamp),
|
|
||||||
)) {
|
|
||||||
// Mark channel in case of fault during fetching precondition
|
|
||||||
sessionContext.channels[preset.channel] = 0;
|
|
||||||
if (typeof k === "string") parameters[k] = Path.parse(k);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
pre.precondition = preset.precondition.evaluate(evalCallback);
|
||||||
|
|
||||||
|
pre.provisions = pre.provisions.map((prov) => {
|
||||||
|
let args = prov.slice(1) as Expression[];
|
||||||
|
args = args.map((arg) => arg.evaluate(evalCallback));
|
||||||
|
return [prov[0], ...args];
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredPresets.push(pre);
|
||||||
}
|
}
|
||||||
|
|
||||||
const declarations = Object.values(parameters).map((v) => ({
|
const declarations = [...parameters].map((v) => ({
|
||||||
path: v,
|
path: Path.parse(v),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { value: 1 },
|
||||||
@@ -520,22 +524,21 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
session.addProvisions(sessionContext, whiteList, whiteListProvisions);
|
session.addProvisions(sessionContext, whiteList, whiteListProvisions);
|
||||||
|
|
||||||
const appendProvisionsToFaults = {};
|
const appendProvisionsToFaults = {};
|
||||||
|
|
||||||
|
const evalCallback2 = (e: Expression): Expression.Literal => {
|
||||||
|
e = session.configContextCallback(sessionContext, e);
|
||||||
|
if (!(e instanceof Expression.Literal)) return new Expression.Literal(null);
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
for (const p of filteredPresets) {
|
for (const p of filteredPresets) {
|
||||||
if (
|
if (p.precondition.evaluate(evalCallback2).value) {
|
||||||
evaluate(p.precondition, {}, sessionContext.timestamp, (e) =>
|
|
||||||
session.configContextCallback(sessionContext, e),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const provs = p.provisions.map((pp) => [
|
const provs = p.provisions.map((pp) => [
|
||||||
pp[0],
|
pp[0],
|
||||||
...pp
|
...pp
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((arg) =>
|
.map((arg) => (arg as Expression).evaluate(evalCallback2).value),
|
||||||
evaluate(arg, {}, sessionContext.timestamp, (e) =>
|
]) as [string, ...Value[]][];
|
||||||
session.configContextCallback(sessionContext, e),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]) as [string, ...Expression[]][];
|
|
||||||
if (blackList[p.channel] === 2) {
|
if (blackList[p.channel] === 2) {
|
||||||
appendProvisionsToFaults[p.channel] = (
|
appendProvisionsToFaults[p.channel] = (
|
||||||
appendProvisionsToFaults[p.channel] || []
|
appendProvisionsToFaults[p.channel] || []
|
||||||
@@ -925,16 +928,18 @@ export async function onConnection(socket: Socket): Promise<void> {
|
|||||||
export async function onClientError(err: Error, socket: Socket): Promise<void> {
|
export async function onClientError(err: Error, socket: Socket): Promise<void> {
|
||||||
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
|
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
|
||||||
const cacheSnapshot = await localCache.getRevision();
|
const cacheSnapshot = await localCache.getRevision();
|
||||||
const debugEnabled = !!localCache.getConfig(
|
const debugEnabled = localCache.getConfig(
|
||||||
cacheSnapshot,
|
cacheSnapshot,
|
||||||
"cwmp.debug",
|
"cwmp.debug",
|
||||||
{
|
false,
|
||||||
remoteAddress: remoteAddress,
|
|
||||||
},
|
|
||||||
Date.now(),
|
|
||||||
(e) => {
|
(e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return remoteAddress;
|
if (e.name === "REMOTE_ADDRESS")
|
||||||
|
return new Expression.Literal(remoteAddress);
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(Date.now());
|
||||||
|
}
|
||||||
|
if (!(e instanceof Expression.Literal))
|
||||||
|
return new Expression.Literal(null);
|
||||||
return e;
|
return e;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1030,8 +1035,7 @@ async function processRequest(
|
|||||||
sessionContext.debug = !!localCache.getConfig(
|
sessionContext.debug = !!localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.debug",
|
"cwmp.debug",
|
||||||
{},
|
false,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => session.configContextCallback(sessionContext, e),
|
(e) => session.configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1039,8 +1043,7 @@ async function processRequest(
|
|||||||
sessionContext.timeout = +localCache.getConfig(
|
sessionContext.timeout = +localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.sessionTimeout",
|
"cwmp.sessionTimeout",
|
||||||
{},
|
30,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => session.configContextCallback(sessionContext, e),
|
(e) => session.configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1282,16 +1285,19 @@ async function clientError(
|
|||||||
deviceId = sessionContext.deviceId;
|
deviceId = sessionContext.deviceId;
|
||||||
} else {
|
} else {
|
||||||
const cacheSnapshot = await localCache.getRevision();
|
const cacheSnapshot = await localCache.getRevision();
|
||||||
debugEnabled = !!localCache.getConfig(
|
const remoteAddress = getRequestOrigin(httpRequest).remoteAddress;
|
||||||
|
debugEnabled = localCache.getConfig(
|
||||||
cacheSnapshot,
|
cacheSnapshot,
|
||||||
"cwmp.debug",
|
"cwmp.debug",
|
||||||
{
|
false,
|
||||||
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
|
|
||||||
},
|
|
||||||
Date.now(),
|
|
||||||
(e) => {
|
(e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return getRequestOrigin(httpRequest).remoteAddress;
|
if (e.name === "REMOTE_ADDRESS")
|
||||||
|
return new Expression.Literal(remoteAddress);
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(Date.now());
|
||||||
|
}
|
||||||
|
if (!(e instanceof Expression.Literal))
|
||||||
|
return new Expression.Literal(null);
|
||||||
return e;
|
return e;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+21
-26
@@ -7,7 +7,6 @@ import {
|
|||||||
Task,
|
Task,
|
||||||
Operation,
|
Operation,
|
||||||
} from "../types.ts";
|
} from "../types.ts";
|
||||||
import Path from "../common/path.ts";
|
|
||||||
import { collections } from "../db/db.ts";
|
import { collections } from "../db/db.ts";
|
||||||
import { optimizeProjection } from "../db/util.ts";
|
import { optimizeProjection } from "../db/util.ts";
|
||||||
import * as MongoTypes from "../db/types.ts";
|
import * as MongoTypes from "../db/types.ts";
|
||||||
@@ -23,21 +22,17 @@ function compareAccessLists(list1: string[], list2: string[]): boolean {
|
|||||||
export async function fetchDevice(
|
export async function fetchDevice(
|
||||||
id: string,
|
id: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
): Promise<[Path, number, Attributes?][]> {
|
): Promise<[string, number, Attributes?][]> {
|
||||||
const res: [Path, number, Attributes?][] = [
|
const res: [string, number, Attributes?][] = [
|
||||||
|
["Events", timestamp, { object: [timestamp, 1], writable: [timestamp, 0] }],
|
||||||
[
|
[
|
||||||
Path.parse("Events"),
|
"DeviceID",
|
||||||
timestamp,
|
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Path.parse("DeviceID"),
|
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
const device = await collections.devices.findOne({ _id: id });
|
const device = (await collections.devices.findOne({ _id: id })) as any;
|
||||||
if (!device) return null;
|
if (!device) return null;
|
||||||
|
|
||||||
function storeParams(
|
function storeParams(
|
||||||
@@ -77,7 +72,7 @@ export async function fetchDevice(
|
|||||||
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res.push([Path.parse(path), t, attrs]);
|
res.push([path, t, attrs]);
|
||||||
} catch {
|
} catch {
|
||||||
// The path parser is now more strict so we might be in a situation where
|
// The path parser is now more strict so we might be in a situation where
|
||||||
// the database contains invalid paths from before this change So here we
|
// the database contains invalid paths from before this change So here we
|
||||||
@@ -86,7 +81,7 @@ export async function fetchDevice(
|
|||||||
splits[splits.length - 1] =
|
splits[splits.length - 1] =
|
||||||
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
|
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
|
||||||
path = splits.join(".");
|
path = splits.join(".");
|
||||||
res.push([Path.parse(path), t, attrs]);
|
res.push([path, t, attrs]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,17 +93,17 @@ export async function fetchDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (obj["_object"] && obj["_timestamp"])
|
if (obj["_object"] && obj["_timestamp"])
|
||||||
res.push([Path.parse(path + ".*"), obj["_timestamp"]]);
|
res.push([path + ".*", obj["_timestamp"]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ts: number = +device["_timestamp"] || 0;
|
const ts: number = +device["_timestamp"] || 0;
|
||||||
if (ts) res.push([Path.parse("*"), ts]);
|
if (ts) res.push(["*", ts]);
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(device)) {
|
for (const [k, v] of Object.entries(device)) {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case "_lastInform":
|
case "_lastInform":
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("Events.Inform"),
|
"Events.Inform",
|
||||||
+v,
|
+v,
|
||||||
{
|
{
|
||||||
object: [+v, 0],
|
object: [+v, 0],
|
||||||
@@ -119,7 +114,7 @@ export async function fetchDevice(
|
|||||||
break;
|
break;
|
||||||
case "_lastBoot":
|
case "_lastBoot":
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("Events.1_BOOT"),
|
"Events.1_BOOT",
|
||||||
+v,
|
+v,
|
||||||
{
|
{
|
||||||
object: [+v, 0],
|
object: [+v, 0],
|
||||||
@@ -130,7 +125,7 @@ export async function fetchDevice(
|
|||||||
break;
|
break;
|
||||||
case "_lastBootstrap":
|
case "_lastBootstrap":
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("Events.0_BOOTSTRAP"),
|
"Events.0_BOOTSTRAP",
|
||||||
+v,
|
+v,
|
||||||
{
|
{
|
||||||
object: [+v, 0],
|
object: [+v, 0],
|
||||||
@@ -142,7 +137,7 @@ export async function fetchDevice(
|
|||||||
case "_registered":
|
case "_registered":
|
||||||
// Use current timestamp for registered event attribute timestamps
|
// Use current timestamp for registered event attribute timestamps
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("Events.Registered"),
|
"Events.Registered",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -153,7 +148,7 @@ export async function fetchDevice(
|
|||||||
break;
|
break;
|
||||||
case "_id":
|
case "_id":
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("DeviceID.ID"),
|
"DeviceID.ID",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -165,7 +160,7 @@ export async function fetchDevice(
|
|||||||
case "_tags":
|
case "_tags":
|
||||||
if ((v as string[]).length) {
|
if ((v as string[]).length) {
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("Tags"),
|
"Tags",
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||||
]);
|
]);
|
||||||
@@ -173,7 +168,7 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
for (const t of v as string[]) {
|
for (const t of v as string[]) {
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("Tags." + encodeTag(t)),
|
"Tags." + encodeTag(t),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -186,7 +181,7 @@ export async function fetchDevice(
|
|||||||
case "_deviceId":
|
case "_deviceId":
|
||||||
if (v["_Manufacturer"] != null) {
|
if (v["_Manufacturer"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("DeviceID.Manufacturer"),
|
"DeviceID.Manufacturer",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -198,7 +193,7 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
if (v["_OUI"] != null) {
|
if (v["_OUI"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("DeviceID.OUI"),
|
"DeviceID.OUI",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -210,7 +205,7 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
if (v["_ProductClass"] != null) {
|
if (v["_ProductClass"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("DeviceID.ProductClass"),
|
"DeviceID.ProductClass",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -222,7 +217,7 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
if (v["_SerialNumber"] != null) {
|
if (v["_SerialNumber"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
Path.parse("DeviceID.SerialNumber"),
|
"DeviceID.SerialNumber",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -257,7 +252,7 @@ export async function saveDevice(
|
|||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const parent = deviceData.paths.get(diff[0].slice(0, -1));
|
const parent = deviceData.paths.get(diff[0].slice(0, -1).toString());
|
||||||
|
|
||||||
// Param timestamps may be greater than session timestamp to track revisions
|
// Param timestamps may be greater than session timestamp to track revisions
|
||||||
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
|
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
|
||||||
|
|||||||
+72
-56
@@ -1,15 +1,12 @@
|
|||||||
import * as vm from "node:vm";
|
import * as vm from "node:vm";
|
||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
import * as config from "../config.ts";
|
|
||||||
import { collections } from "../db/db.ts";
|
import { collections } from "../db/db.ts";
|
||||||
import { convertOldPrecondition } from "../db/util.ts";
|
import { convertOldPrecondition } from "../db/util.ts";
|
||||||
import * as logger from "../logger.ts";
|
import * as logger from "../logger.ts";
|
||||||
import * as scheduling from "../scheduling.ts";
|
import * as scheduling from "../scheduling.ts";
|
||||||
import * as expression from "../common/expression/util.ts";
|
import Expression, { Value } from "../common/expression.ts";
|
||||||
import { parse } from "../common/expression/parser.ts";
|
|
||||||
import {
|
import {
|
||||||
Preset,
|
Preset,
|
||||||
Expression,
|
|
||||||
Provisions,
|
Provisions,
|
||||||
VirtualParameters,
|
VirtualParameters,
|
||||||
Files,
|
Files,
|
||||||
@@ -93,10 +90,10 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const events = preset["events"] || {};
|
const events = preset["events"] || {};
|
||||||
let precondition = true as Expression;
|
let precondition: Expression = new Expression.Literal(true);
|
||||||
if (preset["precondition"]) {
|
if (preset["precondition"]) {
|
||||||
try {
|
try {
|
||||||
precondition = parse(preset["precondition"]);
|
precondition = Expression.parse(preset["precondition"]);
|
||||||
} catch {
|
} catch {
|
||||||
precondition = convertOldPrecondition(
|
precondition = convertOldPrecondition(
|
||||||
JSON.parse(preset["precondition"]),
|
JSON.parse(preset["precondition"]),
|
||||||
@@ -104,7 +101,7 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Simplify expression
|
// Simplify expression
|
||||||
precondition = expression.evaluate(precondition);
|
precondition = precondition.evaluate((e) => e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _provisions: Preset["provisions"] = [];
|
const _provisions: Preset["provisions"] = [];
|
||||||
@@ -113,23 +110,42 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
|||||||
for (const c of preset.configurations) {
|
for (const c of preset.configurations) {
|
||||||
switch (c.type) {
|
switch (c.type) {
|
||||||
case "age":
|
case "age":
|
||||||
_provisions.push(["refresh", c.name, +c.age]);
|
_provisions.push([
|
||||||
|
"refresh",
|
||||||
|
new Expression.Literal(c.name),
|
||||||
|
new Expression.Literal(+c.age),
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "value":
|
case "value":
|
||||||
_provisions.push(["value", c.name, c.value]);
|
_provisions.push([
|
||||||
|
"value",
|
||||||
|
new Expression.Literal(c.name),
|
||||||
|
new Expression.Literal(c.value),
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "add_tag":
|
case "add_tag":
|
||||||
_provisions.push(["tag", c.tag, true]);
|
_provisions.push([
|
||||||
|
"tag",
|
||||||
|
new Expression.Literal(c.tag),
|
||||||
|
new Expression.Literal(true),
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "delete_tag":
|
case "delete_tag":
|
||||||
_provisions.push(["tag", c.tag, false]);
|
_provisions.push([
|
||||||
|
"tag",
|
||||||
|
new Expression.Literal(c.tag),
|
||||||
|
new Expression.Literal(false),
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "provision":
|
case "provision":
|
||||||
_provisions.push([c.name, ...(c.args || [])]);
|
_provisions.push([
|
||||||
|
c.name,
|
||||||
|
...(c.args || []).map((a) => Expression.parse(a)),
|
||||||
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "add_object":
|
case "add_object":
|
||||||
@@ -139,11 +155,19 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
|||||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
||||||
.join(",");
|
.join(",");
|
||||||
const p = `${c.name}.[${alias}]`;
|
const p = `${c.name}.[${alias}]`;
|
||||||
_provisions.push(["instances", p, 1]);
|
_provisions.push([
|
||||||
|
"instances",
|
||||||
|
new Expression.Literal(p),
|
||||||
|
new Expression.Literal(1),
|
||||||
|
]);
|
||||||
|
|
||||||
for (const k in obj) {
|
for (const k in obj) {
|
||||||
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
|
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
|
||||||
_provisions.push(["value", `${p}.${k}`, obj[k]]);
|
_provisions.push([
|
||||||
|
"value",
|
||||||
|
new Expression.Literal(`${p}.${k}`),
|
||||||
|
new Expression.Literal(obj[k]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +181,11 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
|||||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
||||||
.join(",");
|
.join(",");
|
||||||
const p = `${c.name}.[${alias}]`;
|
const p = `${c.name}.[${alias}]`;
|
||||||
_provisions.push(["instances", p, 0]);
|
_provisions.push([
|
||||||
|
"instances",
|
||||||
|
new Expression.Literal(p),
|
||||||
|
new Expression.Literal(0),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +279,7 @@ async function fetchConfig(): Promise<[string, Config]> {
|
|||||||
|
|
||||||
for (const c of conf) {
|
for (const c of conf) {
|
||||||
// Evaluate expressions to simplify them
|
// Evaluate expressions to simplify them
|
||||||
_config[c._id] = expression.evaluate(parse(c.value));
|
_config[c._id] = Expression.parse(c.value).evaluate((e) => e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [h, _config];
|
return [h, _config];
|
||||||
@@ -303,49 +331,37 @@ export function getFiles(revision: string): Files {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getConfig(
|
export function getConfig(
|
||||||
snapshotKey: string,
|
revision: string,
|
||||||
key: string,
|
key: string,
|
||||||
context: Record<string, unknown>,
|
dflt: string,
|
||||||
now: number,
|
fn: (e: Expression) => Expression.Literal,
|
||||||
cb?: (e: Expression) => Expression,
|
): string;
|
||||||
): string | number | boolean | null {
|
export function getConfig(
|
||||||
const snapshot = localCache.get(snapshotKey);
|
revision: string,
|
||||||
|
key: string,
|
||||||
|
dflt: number,
|
||||||
|
fn: (e: Expression) => Expression.Literal,
|
||||||
|
): number;
|
||||||
|
export function getConfig(
|
||||||
|
revision: string,
|
||||||
|
key: string,
|
||||||
|
dflt: boolean,
|
||||||
|
fn: (e: Expression) => Expression.Literal,
|
||||||
|
): boolean;
|
||||||
|
export function getConfig(
|
||||||
|
revision: string,
|
||||||
|
key: string,
|
||||||
|
dflt: Value,
|
||||||
|
fn: (e: Expression) => Expression.Literal,
|
||||||
|
): Value {
|
||||||
|
const snapshot = localCache.get(revision);
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
const oldOpts = {
|
const e = snapshot.config[key];
|
||||||
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
|
if (!e) return dflt;
|
||||||
"cwmp.debug": "DEBUG",
|
const v = e.evaluate(fn).value;
|
||||||
"cwmp.retryDelay": "RETRY_DELAY",
|
if (typeof v !== typeof dflt) return dflt;
|
||||||
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
|
return v;
|
||||||
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
|
|
||||||
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
|
|
||||||
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
|
|
||||||
"cwmp.cookiesPath": "COOKIES_PATH",
|
|
||||||
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
|
|
||||||
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
|
|
||||||
"cwmp.connectionRequestAllowBasicAuth":
|
|
||||||
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
|
||||||
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
|
||||||
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
|
||||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(key in snapshot.config)) {
|
|
||||||
if (key in oldOpts) {
|
|
||||||
let id;
|
|
||||||
if (context?.["id"]) {
|
|
||||||
id = context["id"];
|
|
||||||
} else if (cb) {
|
|
||||||
id = cb(["PARAM", "DeviceID.ID"]);
|
|
||||||
if (Array.isArray(id)) id = null;
|
|
||||||
}
|
|
||||||
return config.get(oldOpts[key], id);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = expression.evaluate(snapshot.config[key], context, now, cb);
|
|
||||||
return Array.isArray(v) ? null : v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfigExpression(
|
export function getConfigExpression(
|
||||||
|
|||||||
+56
-50
@@ -1,7 +1,8 @@
|
|||||||
import { Filter } from "mongodb";
|
import { Filter } from "mongodb";
|
||||||
import { EJSON } from "bson";
|
import { EJSON } from "bson";
|
||||||
|
import { complement } from "espresso-iisojs";
|
||||||
import { parseLikePattern } from "../common/expression/parser.ts";
|
import { parseLikePattern } from "../common/expression/parser.ts";
|
||||||
import { Expression } from "../types.ts";
|
import Expression from "../common/expression.ts";
|
||||||
import { decodeTag } from "../util.ts";
|
import { decodeTag } from "../util.ts";
|
||||||
import {
|
import {
|
||||||
SynthContextBase,
|
SynthContextBase,
|
||||||
@@ -10,21 +11,14 @@ import {
|
|||||||
Clause,
|
Clause,
|
||||||
} from "../common/expression/synth.ts";
|
} from "../common/expression/synth.ts";
|
||||||
import normalize from "../common/expression/normalize.ts";
|
import normalize from "../common/expression/normalize.ts";
|
||||||
import * as BI from "../common/expression/bigint.ts";
|
|
||||||
|
|
||||||
type Minterm = number[];
|
type Minterm = number[];
|
||||||
|
|
||||||
function getParam(exp: Expression, collection: string): string {
|
function getParam(exp: Expression, collection: string): string {
|
||||||
if (!Array.isArray(exp))
|
if (!(exp instanceof Expression.Parameter))
|
||||||
throw new Error("Left-hand operand must be a parameter");
|
throw new Error("Left-hand operand must be a parameter");
|
||||||
|
|
||||||
if (exp[0] !== "PARAM")
|
const p = exp.path.toString();
|
||||||
throw new Error("Left-hand operand must be a parameter");
|
|
||||||
|
|
||||||
if (typeof exp[1] !== "string")
|
|
||||||
throw new Error("Left-hand operand must be a parameter");
|
|
||||||
|
|
||||||
const p = exp[1];
|
|
||||||
if (collection === "devices") {
|
if (collection === "devices") {
|
||||||
if (p === "DeviceID.ID") return "_id";
|
if (p === "DeviceID.ID") return "_id";
|
||||||
else if (p === "DeviceID") return "_deviceId";
|
else if (p === "DeviceID") return "_deviceId";
|
||||||
@@ -117,12 +111,12 @@ function getTypes(parameter: string, collection: string): string[] {
|
|||||||
|
|
||||||
function roundOid(oid: string, roundUp: boolean): string {
|
function roundOid(oid: string, roundUp: boolean): string {
|
||||||
const match = (oid.match(/^[0-9a-f]*/)?.[0] ?? "").slice(0, 24);
|
const match = (oid.match(/^[0-9a-f]*/)?.[0] ?? "").slice(0, 24);
|
||||||
let num = BI.BigInt("0x0" + match);
|
let num = BigInt("0x0" + match);
|
||||||
let lastChar = 0;
|
let lastChar = 0;
|
||||||
if (oid.length > match.length) lastChar += oid.charCodeAt(match.length);
|
if (oid.length > match.length) lastChar += oid.charCodeAt(match.length);
|
||||||
if (match.length < 24) lastChar -= 48;
|
if (match.length < 24) lastChar -= 48;
|
||||||
if (lastChar > 0 && roundUp) num++;
|
if (lastChar > 0 && roundUp) num++;
|
||||||
num <<= BI.BigInt(4 * (24 - match.length));
|
num <<= BigInt(4 * (24 - match.length));
|
||||||
if (lastChar < 0 && !roundUp) --num;
|
if (lastChar < 0 && !roundUp) --num;
|
||||||
const str = num.toString(16);
|
const str = num.toString(16);
|
||||||
if (str.length > 24 || str.startsWith("-")) return null;
|
if (str.length > 24 || str.startsWith("-")) return null;
|
||||||
@@ -259,11 +253,12 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinterms(clause: Clause, res: true | false | null): number[][] {
|
getMinterms(clause: Clause, res: number): number[][] {
|
||||||
const exp = clause.expression();
|
res = res & 0b111;
|
||||||
if (!Array.isArray(exp)) throw new Error("Invalid query expression");
|
if (res & (res - 1)) return complement(this.getMinterms(clause, ~res));
|
||||||
|
|
||||||
if (res === null) {
|
const exp = clause.expression();
|
||||||
|
if (res === 0b001) {
|
||||||
const minterms: number[][] = [];
|
const minterms: number[][] = [];
|
||||||
for (const dep of clause.getNullables()) {
|
for (const dep of clause.getNullables()) {
|
||||||
const e = dep.operand.expression();
|
const e = dep.operand.expression();
|
||||||
@@ -280,36 +275,42 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
return minterms;
|
return minterms;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([">", "<", "="].includes(exp[0])) {
|
if (!(exp instanceof Expression.Binary))
|
||||||
const param = getParam(exp[1], this.collection);
|
throw new Error("Invalid query expression");
|
||||||
|
|
||||||
let rhs = exp[2];
|
const truthy = res === 0b100;
|
||||||
if (typeof rhs === "boolean") rhs = +rhs;
|
|
||||||
else if (typeof rhs !== "string" && typeof rhs !== "number")
|
if ([">", "<", "="].includes(exp.operator)) {
|
||||||
|
const param = getParam(exp.left, this.collection);
|
||||||
|
|
||||||
|
if (!(exp.right instanceof Expression.Literal))
|
||||||
throw new Error(`Right-hand operand must be a literal value`);
|
throw new Error(`Right-hand operand must be a literal value`);
|
||||||
|
|
||||||
|
let rhs = exp.right.value;
|
||||||
|
if (typeof rhs === "boolean") rhs = +rhs;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
param.startsWith("Tags.") &&
|
param.startsWith("Tags.") &&
|
||||||
this.collection === "devices" &&
|
this.collection === "devices" &&
|
||||||
exp[0] === "="
|
exp.operator === "="
|
||||||
) {
|
) {
|
||||||
const t = decodeTag(param.slice(5));
|
const t = decodeTag(param.slice(5));
|
||||||
const c = new MongoClauseArray("_tags", t);
|
const c = new MongoClauseArray("_tags", t);
|
||||||
if (typeof rhs === "string") rhs = 2;
|
if (typeof rhs === "string") rhs = 2;
|
||||||
if (exp[0] === "=") {
|
if (exp.operator === "=") {
|
||||||
if ((rhs === 1) !== res) return [];
|
if ((rhs === 1) !== truthy) return [];
|
||||||
} else if (exp[0] === ">") {
|
} else if (exp.operator === ">") {
|
||||||
if ((res && rhs >= 1) || (!res && rhs < 1)) return [];
|
if ((truthy && rhs >= 1) || (!truthy && rhs < 1)) return [];
|
||||||
} else if (exp[0] === "<") {
|
} else if (exp.operator === "<") {
|
||||||
if ((res && rhs <= 1) || (!res && rhs > 1)) return [];
|
if ((truthy && rhs <= 1) || (!truthy && rhs > 1)) return [];
|
||||||
}
|
}
|
||||||
return [[(this.getVar(c) << 1) ^ 1]];
|
return [[(this.getVar(c) << 1) ^ 1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte";
|
let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte";
|
||||||
if (exp[0] === "=") op = "$eq";
|
if (exp.operator === "=") op = "$eq";
|
||||||
else if (exp[0] === ">") op = res ? "$gt" : "$lte";
|
else if (exp.operator === ">") op = truthy ? "$gt" : "$lte";
|
||||||
else if (exp[0] === "<") op = res ? "$lt" : "$gte";
|
else if (exp.operator === "<") op = truthy ? "$lt" : "$gte";
|
||||||
|
|
||||||
const possibleTypes = new Set(getTypes(param, this.collection));
|
const possibleTypes = new Set(getTypes(param, this.collection));
|
||||||
const clauses: MongoClause[] = [];
|
const clauses: MongoClause[] = [];
|
||||||
@@ -333,7 +334,7 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op === "$eq" && !res) {
|
if (op === "$eq" && !truthy) {
|
||||||
clauses.push(new MongoClauseCompare(param, "$eq", null, ""));
|
clauses.push(new MongoClauseCompare(param, "$eq", null, ""));
|
||||||
return [clauses.map((c) => this.getVar(c) << 1)];
|
return [clauses.map((c) => this.getVar(c) << 1)];
|
||||||
}
|
}
|
||||||
@@ -382,30 +383,35 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return clauses.map((c) => [(this.getVar(c) << 1) ^ 1]);
|
return clauses.map((c) => [(this.getVar(c) << 1) ^ 1]);
|
||||||
} else if (exp[0] === "LIKE") {
|
} else if (exp.operator === "LIKE") {
|
||||||
const pat = exp[2];
|
if (
|
||||||
if (typeof pat !== "string")
|
!(
|
||||||
|
exp.right instanceof Expression.Literal &&
|
||||||
|
typeof exp.right.value === "string"
|
||||||
|
)
|
||||||
|
)
|
||||||
throw new Error("Right-hand operand of 'LIKE' must be a string");
|
throw new Error("Right-hand operand of 'LIKE' must be a string");
|
||||||
let p = exp[1];
|
const pat = exp.right.value;
|
||||||
|
|
||||||
|
let p = exp.left;
|
||||||
let caseSensitive = true;
|
let caseSensitive = true;
|
||||||
if (
|
if (
|
||||||
Array.isArray(p) &&
|
p instanceof Expression.FunctionCall &&
|
||||||
p[0] === "FUNC" &&
|
["UPPER", "LOWER"].includes(p.name)
|
||||||
["UPPER", "LOWER"].includes(p[1])
|
|
||||||
) {
|
) {
|
||||||
if (p[1] === "UPPER" && pat !== pat.toUpperCase())
|
if (p.name === "UPPER" && pat !== pat.toUpperCase())
|
||||||
return res ? [] : [[]];
|
return truthy ? [] : [[]];
|
||||||
if (p[1] === "LOWER" && pat !== pat.toLowerCase())
|
if (p.name === "LOWER" && pat !== pat.toLowerCase())
|
||||||
return res ? [] : [[]];
|
return truthy ? [] : [[]];
|
||||||
caseSensitive = false;
|
caseSensitive = false;
|
||||||
p = p[2];
|
p = p.args[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const param = getParam(p, this.collection);
|
const param = getParam(p, this.collection);
|
||||||
const c = new MongoClauseLike(param, pat, exp[3], caseSensitive);
|
const c = new MongoClauseLike(param, pat, null, caseSensitive);
|
||||||
if (res) return [[(this.getVar(c) << 1) ^ 1]];
|
if (truthy) return [[(this.getVar(c) << 1) ^ 1]];
|
||||||
const typeClase = new MongoClauseType(param, "string");
|
const typeClause = new MongoClauseCompare(param, "$gte", "", "string");
|
||||||
const r = [[this.getVar(c) << 1, (this.getVar(typeClase) << 1) ^ 1]];
|
const r = [[this.getVar(c) << 1, (this.getVar(typeClause) << 1) ^ 1]];
|
||||||
return r;
|
return r;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid query expression");
|
throw new Error("Invalid query expression");
|
||||||
@@ -641,7 +647,7 @@ export function toMongoQuery(
|
|||||||
exp = normalize(exp);
|
exp = normalize(exp);
|
||||||
const clause = Clause.fromExpression(normalize(exp));
|
const clause = Clause.fromExpression(normalize(exp));
|
||||||
const context = new MongoSynthContext(resource);
|
const context = new MongoSynthContext(resource);
|
||||||
const minterms = clause.true(context);
|
const minterms = clause.getMinterms(context, 0b100);
|
||||||
const minimized = context.minimize(minterms);
|
const minimized = context.minimize(minterms);
|
||||||
if (!minimized.length) return false;
|
if (!minimized.length) return false;
|
||||||
return EJSON.deserialize(context.toQuery(minimized));
|
return EJSON.deserialize(context.toQuery(minimized));
|
||||||
@@ -650,5 +656,5 @@ export function toMongoQuery(
|
|||||||
export function validQuery(exp: Expression, resource: string): void {
|
export function validQuery(exp: Expression, resource: string): void {
|
||||||
const clause = Clause.fromExpression(normalize(exp));
|
const clause = Clause.fromExpression(normalize(exp));
|
||||||
const context = new MongoSynthContext(resource);
|
const context = new MongoSynthContext(resource);
|
||||||
clause.true(context);
|
clause.getMinterms(context, 0b100);
|
||||||
}
|
}
|
||||||
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { Expression, FaultStruct } from "../types.ts";
|
import { FaultStruct } from "../types.ts";
|
||||||
|
import { Value } from "../common/expression.ts";
|
||||||
|
|
||||||
export interface Fault {
|
export interface Fault {
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -71,7 +72,7 @@ interface TaskDeleteObject extends TaskBase {
|
|||||||
|
|
||||||
interface TaskProvisions extends TaskBase {
|
interface TaskProvisions extends TaskBase {
|
||||||
name: "provisions";
|
name: "provisions";
|
||||||
provisions?: [string, ...Expression[]][];
|
provisions?: [string, ...Value[]][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Task =
|
export type Task =
|
||||||
@@ -122,7 +123,7 @@ type Configuration =
|
|||||||
| { type: "delete_tag"; tag: string }
|
| { type: "delete_tag"; tag: string }
|
||||||
| { type: "add_object"; name: string; object: string }
|
| { type: "add_object"; name: string; object: string }
|
||||||
| { type: "delete_object"; name: string; object: string }
|
| { type: "delete_object"; name: string; object: string }
|
||||||
| { type: "provision"; name: string; args?: Expression[] };
|
| { type: "provision"; name: string; args?: string[] };
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
|||||||
+101
-41
@@ -1,4 +1,5 @@
|
|||||||
import { Expression } from "../types.ts";
|
import Expression, { Value } from "../common/expression.ts";
|
||||||
|
import Path from "../common/path.ts";
|
||||||
import { encodeTag } from "../util.ts";
|
import { encodeTag } from "../util.ts";
|
||||||
|
|
||||||
// Optimize projection by removing overlaps
|
// Optimize projection by removing overlaps
|
||||||
@@ -26,44 +27,62 @@ export function optimizeProjection(obj: { [path: string]: 1 }): {
|
|||||||
|
|
||||||
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
|
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
|
||||||
function recursive(_query): Expression {
|
function recursive(_query): Expression {
|
||||||
const expressions: Expression[] = [];
|
let res: Expression = new Expression.Literal(true);
|
||||||
for (const [k, v] of Object.entries(_query)) {
|
for (const [k, v] of Object.entries(_query)) {
|
||||||
if (k[0] === "$") {
|
if (k[0] === "$") {
|
||||||
if (k === "$and") {
|
if (k === "$and") {
|
||||||
const and: Expression = ["AND"];
|
for (const vv of Object.values(v))
|
||||||
for (const vv of Object.values(v)) and.push(recursive(vv));
|
res = Expression.and(res, recursive(vv));
|
||||||
expressions.push(and);
|
|
||||||
} else if (k === "$or") {
|
} else if (k === "$or") {
|
||||||
const or: Expression = ["OR"];
|
let or: Expression = new Expression.Literal(false);
|
||||||
for (const vv of Object.values(v)) or.push(recursive(vv));
|
for (const vv of Object.values(v))
|
||||||
expressions.push(or);
|
or = Expression.or(or, recursive(vv));
|
||||||
|
res = Expression.and(res, or);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Operator ${k} not supported`);
|
throw new Error(`Operator ${k} not supported`);
|
||||||
}
|
}
|
||||||
} else if (k === "_tags") {
|
} else if (k === "_tags") {
|
||||||
if (typeof v === "object") {
|
if (typeof v === "object") {
|
||||||
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
||||||
const conjs: Expression[] = [];
|
|
||||||
for (const [op, val] of Object.entries(v)) {
|
for (const [op, val] of Object.entries(v)) {
|
||||||
if (op === "$ne") {
|
if (op === "$ne") {
|
||||||
if (typeof v["$ne"] !== "string")
|
if (typeof v["$ne"] !== "string")
|
||||||
throw new Error("Only string values are allowed for _tags");
|
throw new Error("Only string values are allowed for _tags");
|
||||||
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Unary(
|
||||||
|
"IS NULL",
|
||||||
|
new Expression.Parameter(
|
||||||
|
Path.parse(`Tags.${encodeTag(val)}`),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (op === "$eq") {
|
} else if (op === "$eq") {
|
||||||
if (typeof v["$eq"] !== "string")
|
if (typeof v["$eq"] !== "string")
|
||||||
throw new Error("Only string values are allowed for _tags");
|
throw new Error("Only string values are allowed for _tags");
|
||||||
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Unary(
|
||||||
|
"IS NOT NULL",
|
||||||
|
new Expression.Parameter(
|
||||||
|
Path.parse(`Tags.${encodeTag(val)}`),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid tag query`);
|
throw new Error(`Invalid tag query`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (conjs.length === 1) expressions.push(conjs[0]);
|
|
||||||
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
|
|
||||||
} else {
|
} else {
|
||||||
expressions.push([
|
res = Expression.and(
|
||||||
"IS NOT NULL",
|
res,
|
||||||
["PARAM", `Tags.${encodeTag(v as string)}`],
|
new Expression.Unary(
|
||||||
]);
|
"IS NOT NULL",
|
||||||
|
new Expression.Parameter(
|
||||||
|
Path.parse(`Tags.${encodeTag(v as string)}`),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (k.startsWith("Tags.")) {
|
} else if (k.startsWith("Tags.")) {
|
||||||
let exists: boolean;
|
let exists: boolean;
|
||||||
@@ -73,51 +92,92 @@ export function convertOldPrecondition(q: Record<string, unknown>): Expression {
|
|||||||
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
|
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
|
||||||
else throw new Error(`Invalid tag query`);
|
else throw new Error(`Invalid tag query`);
|
||||||
|
|
||||||
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Unary(
|
||||||
|
exists ? "IS NOT NULL" : "IS NULL",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (typeof v === "object") {
|
} else if (typeof v === "object") {
|
||||||
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
||||||
|
|
||||||
const exps: Expression[] = [];
|
|
||||||
for (const [kk, vv] of Object.entries(v)) {
|
for (const [kk, vv] of Object.entries(v)) {
|
||||||
if (kk === "$eq") {
|
if (kk === "$eq") {
|
||||||
exps.push(["=", ["PARAM", k], vv]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Binary(
|
||||||
|
"=",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
new Expression.Literal(vv),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (kk === "$ne") {
|
} else if (kk === "$ne") {
|
||||||
exps.push([
|
const p = new Expression.Parameter(Path.parse(k));
|
||||||
"OR",
|
res = Expression.and(
|
||||||
["<>", ["PARAM", k], vv],
|
res,
|
||||||
["IS NULL", ["PARAM", k]],
|
Expression.or(
|
||||||
]);
|
new Expression.Binary("<>", p, new Expression.Literal(vv)),
|
||||||
|
new Expression.Unary("IS NULL", p),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (kk === "$lt") {
|
} else if (kk === "$lt") {
|
||||||
exps.push(["<", ["PARAM", k], vv]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Binary(
|
||||||
|
"<",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
new Expression.Literal(vv),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (kk === "$lte") {
|
} else if (kk === "$lte") {
|
||||||
exps.push(["<=", ["PARAM", k], vv]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Binary(
|
||||||
|
"<=",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
new Expression.Literal(vv),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (kk === "$gt") {
|
} else if (kk === "$gt") {
|
||||||
exps.push([">", ["PARAM", k], vv]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Binary(
|
||||||
|
">",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
new Expression.Literal(vv),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (kk === "$gte") {
|
} else if (kk === "$gte") {
|
||||||
exps.push([">=", ["PARAM", k], vv]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Binary(
|
||||||
|
">=",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
new Expression.Literal(vv),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Operator ${kk} not supported`);
|
throw new Error(`Operator ${kk} not supported`);
|
||||||
}
|
}
|
||||||
if (!["string", "number", "boolean"].includes(typeof vv))
|
if (!["string", "number", "boolean"].includes(typeof vv))
|
||||||
throw new Error(`Invalid value for ${kk} operator`);
|
throw new Error(`Invalid value for ${kk} operator`);
|
||||||
}
|
}
|
||||||
if (exps.length === 1) {
|
|
||||||
expressions.push(exps[0]);
|
|
||||||
} else if (exps.length > 1) {
|
|
||||||
const and: Expression = ["AND"];
|
|
||||||
expressions.push(and.concat(exps));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
expressions.push(["=", ["PARAM", k], v]);
|
res = Expression.and(
|
||||||
|
res,
|
||||||
|
new Expression.Binary(
|
||||||
|
"=",
|
||||||
|
new Expression.Parameter(Path.parse(k)),
|
||||||
|
new Expression.Literal(v as Value),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (expressions.length === 1) return expressions[0];
|
return res;
|
||||||
if (expressions.length === 0) return true;
|
|
||||||
return ["AND", ...expressions];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty filter
|
// empty filter
|
||||||
if (!Object.keys(q).length) return true;
|
if (!Object.keys(q).length) return new Expression.Literal(true);
|
||||||
|
|
||||||
return recursive(q);
|
return recursive(q);
|
||||||
}
|
}
|
||||||
+42
-13
@@ -1,3 +1,4 @@
|
|||||||
|
import Expression, { Value } from "./common/expression.ts";
|
||||||
import Path from "./common/path.ts";
|
import Path from "./common/path.ts";
|
||||||
import {
|
import {
|
||||||
DeviceData,
|
DeviceData,
|
||||||
@@ -84,9 +85,9 @@ export function getAliasDeclarations(
|
|||||||
|
|
||||||
if (path.alias) {
|
if (path.alias) {
|
||||||
for (const [i, alias] of path.segments.entries()) {
|
for (const [i, alias] of path.segments.entries()) {
|
||||||
if (Array.isArray(alias)) {
|
if (alias instanceof Expression) {
|
||||||
const parent = stripped.slice(0, i + 1);
|
const parent = stripped.slice(0, i + 1);
|
||||||
for (const [p] of alias as [Path, string][]) {
|
for (const [p] of expressionToAlias(alias)) {
|
||||||
decs = decs.concat(
|
decs = decs.concat(
|
||||||
getAliasDeclarations(parent.concat(p), timestamp, {
|
getAliasDeclarations(parent.concat(p), timestamp, {
|
||||||
value: timestamp,
|
value: timestamp,
|
||||||
@@ -100,6 +101,22 @@ export function getAliasDeclarations(
|
|||||||
return decs;
|
return decs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function expressionToAlias(exp: Expression): [Path, Value][] {
|
||||||
|
if (exp instanceof Expression.Literal && exp.value === true) return [];
|
||||||
|
if (exp instanceof Expression.Binary) {
|
||||||
|
if (exp.operator === "AND")
|
||||||
|
return [...expressionToAlias(exp.left), ...expressionToAlias(exp.right)];
|
||||||
|
else if (exp.operator === "=") {
|
||||||
|
if (
|
||||||
|
exp.left instanceof Expression.Parameter &&
|
||||||
|
exp.right instanceof Expression.Literal
|
||||||
|
)
|
||||||
|
return [[exp.left.path, exp.right.value]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Invalid alias expression");
|
||||||
|
}
|
||||||
|
|
||||||
export function unpack(
|
export function unpack(
|
||||||
deviceData: DeviceData,
|
deviceData: DeviceData,
|
||||||
path: Path,
|
path: Path,
|
||||||
@@ -107,17 +124,19 @@ export function unpack(
|
|||||||
): Path[] {
|
): Path[] {
|
||||||
let allMatches = [] as Path[];
|
let allMatches = [] as Path[];
|
||||||
if (!path.alias) {
|
if (!path.alias) {
|
||||||
for (const p of deviceData.paths.find(path, false, true))
|
for (const p of deviceData.paths.findCompat(path, false, true))
|
||||||
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
||||||
} else {
|
} else {
|
||||||
const wildcardPath = path.stripAlias();
|
const wildcardPath = path.stripAlias();
|
||||||
|
|
||||||
for (const p of deviceData.paths.find(wildcardPath, false, true))
|
for (const p of deviceData.paths.findCompat(wildcardPath, false, true))
|
||||||
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
||||||
|
|
||||||
for (let i = path.length - 1; i >= 0; --i) {
|
for (let i = path.length - 1; i >= 0; --i) {
|
||||||
if (path.alias & (1 << i)) {
|
if (path.alias & (1 << i)) {
|
||||||
for (const [param, val] of path.segments[i] as [Path, string][]) {
|
for (const [param, val] of expressionToAlias(
|
||||||
|
path.segments[i] as Expression,
|
||||||
|
)) {
|
||||||
const p = wildcardPath.slice(0, i + 1).concat(param);
|
const p = wildcardPath.slice(0, i + 1).concat(param);
|
||||||
const unpacked = unpack(deviceData, p, revision);
|
const unpacked = unpack(deviceData, p, revision);
|
||||||
const filtered: Path[] = [];
|
const filtered: Path[] = [];
|
||||||
@@ -189,7 +208,7 @@ export function clear(
|
|||||||
attributes.value = attributes.object;
|
attributes.value = attributes.object;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const p of deviceData.paths.find(
|
for (const p of deviceData.paths.findCompat(
|
||||||
path,
|
path,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
@@ -252,12 +271,12 @@ function compareEquality(a, b): boolean {
|
|||||||
|
|
||||||
export function set(
|
export function set(
|
||||||
deviceData: DeviceData,
|
deviceData: DeviceData,
|
||||||
path: Path,
|
pathStr: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
attributes: Attributes,
|
attributes: Attributes,
|
||||||
toClear?: Clear[],
|
toClear?: Clear[],
|
||||||
): Clear[] {
|
): Clear[] {
|
||||||
path = deviceData.paths.add(path);
|
const path = deviceData.paths.add(pathStr);
|
||||||
|
|
||||||
const currentTimestamp = deviceData.timestamps.get(path);
|
const currentTimestamp = deviceData.timestamps.get(path);
|
||||||
|
|
||||||
@@ -312,7 +331,7 @@ export function set(
|
|||||||
if (path.length > 1) {
|
if (path.length > 1) {
|
||||||
toClear = set(
|
toClear = set(
|
||||||
deviceData,
|
deviceData,
|
||||||
path.slice(0, path.length - 1),
|
path.slice(0, path.length - 1).toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1] },
|
{ object: [timestamp, 1] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -326,7 +345,12 @@ export function set(
|
|||||||
deviceData.attributes.delete(path);
|
deviceData.attributes.delete(path);
|
||||||
changeFlags |= 1;
|
changeFlags |= 1;
|
||||||
} else if (path.wildcard) {
|
} else if (path.wildcard) {
|
||||||
for (const p of deviceData.paths.find(path, false, true, path.length)) {
|
for (const p of deviceData.paths.findCompat(
|
||||||
|
path,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
path.length,
|
||||||
|
)) {
|
||||||
if (timestamp > deviceData.timestamps.get(p)) {
|
if (timestamp > deviceData.timestamps.get(p)) {
|
||||||
toClear = toClear || [];
|
toClear = toClear || [];
|
||||||
toClear.push([p, timestamp]);
|
toClear.push([p, timestamp]);
|
||||||
@@ -343,7 +367,12 @@ export function set(
|
|||||||
toClear = toClear || [];
|
toClear = toClear || [];
|
||||||
toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]);
|
toClear.push([path, 0, { object: attributes.object[0] }, changeFlags]);
|
||||||
} else {
|
} else {
|
||||||
for (const p of deviceData.paths.find(path, true, false, path.length)) {
|
for (const p of deviceData.paths.findCompat(
|
||||||
|
path,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
path.length,
|
||||||
|
)) {
|
||||||
const tracker = deviceData.trackers.get(p);
|
const tracker = deviceData.trackers.get(p);
|
||||||
for (const k in tracker)
|
for (const k in tracker)
|
||||||
if (tracker[k] & changeFlags) deviceData.changes.add(k);
|
if (tracker[k] & changeFlags) deviceData.changes.add(k);
|
||||||
@@ -356,11 +385,11 @@ export function set(
|
|||||||
|
|
||||||
export function track(
|
export function track(
|
||||||
deviceData: DeviceData,
|
deviceData: DeviceData,
|
||||||
path: Path,
|
pathStr: string,
|
||||||
marker: string,
|
marker: string,
|
||||||
attributes?: string[],
|
attributes?: string[],
|
||||||
): void {
|
): void {
|
||||||
path = deviceData.paths.add(path);
|
const path = deviceData.paths.add(pathStr);
|
||||||
let f = 1;
|
let f = 1;
|
||||||
|
|
||||||
if (attributes)
|
if (attributes)
|
||||||
|
|||||||
+14
-5
@@ -79,14 +79,23 @@ export async function getStatus(): Promise<Status> {
|
|||||||
const users = getUsers(configSnapshot);
|
const users = getUsers(configSnapshot);
|
||||||
const ui = getUiConfig(configSnapshot);
|
const ui = getUiConfig(configSnapshot);
|
||||||
|
|
||||||
return {
|
const status = {
|
||||||
users: !Object.keys(users).length,
|
users: !Object.keys(users).length,
|
||||||
presets: !presetCount,
|
presets: !presetCount,
|
||||||
filters: !Object.keys(ui["filters"]).length,
|
filters: true,
|
||||||
device: !Object.keys(ui["device"]).length,
|
device: true,
|
||||||
index: !Object.keys(ui["index"]).length,
|
index: true,
|
||||||
overview: !Object.keys(ui["overview"]).length,
|
overview: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const k of Object.keys(ui)) {
|
||||||
|
if (k.startsWith("filters.")) status.filters = false;
|
||||||
|
if (k.startsWith("device.")) status.device = false;
|
||||||
|
if (k.startsWith("index.")) status.index = false;
|
||||||
|
if (k.startsWith("overview.")) status.overview = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function seed(options: Record<string, boolean>): Promise<void> {
|
export async function seed(options: Record<string, boolean>): Promise<void> {
|
||||||
|
|||||||
+22
-20
@@ -14,6 +14,7 @@ import { flattenDevice } from "./ui/db.ts";
|
|||||||
import { getRequestOrigin } from "./forwarded.ts";
|
import { getRequestOrigin } from "./forwarded.ts";
|
||||||
import { acquireLock, releaseLock } from "./lock.ts";
|
import { acquireLock, releaseLock } from "./lock.ts";
|
||||||
import { ResourceLockedError } from "./common/errors.ts";
|
import { ResourceLockedError } from "./common/errors.ts";
|
||||||
|
import Expression from "./common/expression.ts";
|
||||||
|
|
||||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||||
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
||||||
@@ -349,6 +350,24 @@ async function handler(
|
|||||||
const lastInform = (dev["_lastInform"] as Date).getTime();
|
const lastInform = (dev["_lastInform"] as Date).getTime();
|
||||||
const device = flattenDevice(dev);
|
const device = flattenDevice(dev);
|
||||||
|
|
||||||
|
const configCallback = (e: Expression): Expression.Literal => {
|
||||||
|
if (e instanceof Expression.Literal) return e;
|
||||||
|
else if (e instanceof Expression.Parameter) {
|
||||||
|
const p = device[e.path.toString()];
|
||||||
|
if (p != null) return new Expression.Literal(p);
|
||||||
|
} else if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(Date.now());
|
||||||
|
if (e.name === "REMOTE_ADDRESS") {
|
||||||
|
for (const root of ["InternetGatewayDevice", "Device"]) {
|
||||||
|
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
||||||
|
if (p != null)
|
||||||
|
return new Expression.Literal(new URL(p as string).hostname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
};
|
||||||
|
|
||||||
let onlineThreshold: number;
|
let onlineThreshold: number;
|
||||||
if (url.searchParams.has("timeout")) {
|
if (url.searchParams.has("timeout")) {
|
||||||
onlineThreshold = parseInt(url.searchParams.get("timeout"));
|
onlineThreshold = parseInt(url.searchParams.get("timeout"));
|
||||||
@@ -357,26 +376,9 @@ async function handler(
|
|||||||
onlineThreshold = getConfig(
|
onlineThreshold = getConfig(
|
||||||
revision,
|
revision,
|
||||||
"cwmp.deviceOnlineThreshold",
|
"cwmp.deviceOnlineThreshold",
|
||||||
{},
|
4000,
|
||||||
Date.now(),
|
configCallback,
|
||||||
(exp) => {
|
);
|
||||||
if (!Array.isArray(exp)) return exp;
|
|
||||||
if (exp[0] === "PARAM") {
|
|
||||||
const p = device[exp[1]];
|
|
||||||
if (p?.value) return p.value[0];
|
|
||||||
} else if (exp[0] === "FUNC") {
|
|
||||||
if (exp[1] === "REMOTE_ADDRESS") {
|
|
||||||
for (const root of ["InternetGatewayDevice", "Device"]) {
|
|
||||||
const p =
|
|
||||||
device[`${root}.ManagementServer.ConnectionRequestURL`];
|
|
||||||
if (p?.value) return new URL(p.value[0] as string).hostname;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return exp;
|
|
||||||
},
|
|
||||||
) as number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = await apiFunctions.connectionRequest(deviceId, device);
|
let status = await apiFunctions.connectionRequest(deviceId, device);
|
||||||
|
|||||||
+147
-184
@@ -25,7 +25,6 @@ import {
|
|||||||
TransferCompleteRequest,
|
TransferCompleteRequest,
|
||||||
Operation,
|
Operation,
|
||||||
ScriptResult,
|
ScriptResult,
|
||||||
Expression,
|
|
||||||
GetParameterValues,
|
GetParameterValues,
|
||||||
GetParameterAttributes,
|
GetParameterAttributes,
|
||||||
GetParameterNames,
|
GetParameterNames,
|
||||||
@@ -42,6 +41,7 @@ import {
|
|||||||
import { getRequestOrigin } from "./forwarded.ts";
|
import { getRequestOrigin } from "./forwarded.ts";
|
||||||
import * as logger from "./logger.ts";
|
import * as logger from "./logger.ts";
|
||||||
import { encodeTag } from "./util.ts";
|
import { encodeTag } from "./util.ts";
|
||||||
|
import Expression, { Value } from "./common/expression.ts";
|
||||||
|
|
||||||
const VALID_PARAM_TYPES = new Set([
|
const VALID_PARAM_TYPES = new Set([
|
||||||
"xsd:int",
|
"xsd:int",
|
||||||
@@ -102,29 +102,25 @@ function generateRpcId(sessionContext: SessionContext): string {
|
|||||||
export function configContextCallback(
|
export function configContextCallback(
|
||||||
sessionContext: SessionContext,
|
sessionContext: SessionContext,
|
||||||
exp: Expression,
|
exp: Expression,
|
||||||
): Expression {
|
): Expression.Literal {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (exp instanceof Expression.Literal) return exp;
|
||||||
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
|
else if (exp instanceof Expression.FunctionCall) {
|
||||||
let name = exp[1];
|
if (exp.name === "NOW")
|
||||||
if (name === "id") name = "DeviceID.ID";
|
return new Expression.Literal(sessionContext.timestamp);
|
||||||
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
|
if (exp.name === "REMOTE_ADDRESS")
|
||||||
else if (name === "productClass") name = "DeviceID.ProductClass";
|
return new Expression.Literal(
|
||||||
else if (name === "oui") name = "DeviceID.OUI";
|
getRequestOrigin(sessionContext.httpRequest).remoteAddress,
|
||||||
else if (name === "remoteAddress")
|
);
|
||||||
return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
|
} else if (exp instanceof Expression.Parameter) {
|
||||||
|
|
||||||
const deviceData = sessionContext.deviceData;
|
const deviceData = sessionContext.deviceData;
|
||||||
const paths = deviceData.paths;
|
const paths = deviceData.paths;
|
||||||
const path = paths.get(Path.parse(name));
|
const path = paths.get(exp.path.toString());
|
||||||
if (path) {
|
if (path) {
|
||||||
const attrs = deviceData.attributes.get(path, 1);
|
const attrs = deviceData.attributes.get(path, 1);
|
||||||
if (attrs?.value?.[1]) return attrs.value[1][0];
|
if (attrs?.value?.[1]) return new Expression.Literal(attrs.value[1][0]);
|
||||||
}
|
}
|
||||||
} else if (exp[0] === "FUNC") {
|
|
||||||
if (exp[1] === "REMOTE_ADDRESS")
|
|
||||||
return getRequestOrigin(sessionContext.httpRequest).remoteAddress;
|
|
||||||
}
|
}
|
||||||
return exp;
|
return new Expression.Literal(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function inform(
|
export async function inform(
|
||||||
@@ -132,9 +128,9 @@ export async function inform(
|
|||||||
rpcReq: InformRequest,
|
rpcReq: InformRequest,
|
||||||
): Promise<AcsResponse> {
|
): Promise<AcsResponse> {
|
||||||
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||||
const params: [Path, number, Attributes][] = [
|
const params: [string, number, Attributes][] = [
|
||||||
[
|
[
|
||||||
Path.parse("DeviceID.Manufacturer"),
|
"DeviceID.Manufacturer",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -144,7 +140,7 @@ export async function inform(
|
|||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
Path.parse("DeviceID.OUI"),
|
"DeviceID.OUI",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -154,7 +150,7 @@ export async function inform(
|
|||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
Path.parse("DeviceID.ProductClass"),
|
"DeviceID.ProductClass",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -164,7 +160,7 @@ export async function inform(
|
|||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
Path.parse("DeviceID.SerialNumber"),
|
"DeviceID.SerialNumber",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -177,7 +173,7 @@ export async function inform(
|
|||||||
for (const p of rpcReq.parameterList) {
|
for (const p of rpcReq.parameterList) {
|
||||||
const path = p[0];
|
const path = p[0];
|
||||||
params.push([
|
params.push([
|
||||||
path,
|
path.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -187,7 +183,7 @@ export async function inform(
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.push([
|
params.push([
|
||||||
Path.parse("Events.Inform"),
|
"Events.Inform",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -198,7 +194,7 @@ export async function inform(
|
|||||||
|
|
||||||
for (const e of rpcReq.event) {
|
for (const e of rpcReq.event) {
|
||||||
params.push([
|
params.push([
|
||||||
Path.parse(`Events.${encodeTag(e.replace(/\s+/g, "_"))}`),
|
`Events.${encodeTag(e.replace(/\s+/g, "_"))}`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -210,7 +206,7 @@ export async function inform(
|
|||||||
|
|
||||||
if (sessionContext.new) {
|
if (sessionContext.new) {
|
||||||
params.push([
|
params.push([
|
||||||
Path.parse("DeviceID.ID"),
|
"DeviceID.ID",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -219,7 +215,7 @@ export async function inform(
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
params.push([
|
params.push([
|
||||||
Path.parse("Events.Registered"),
|
"Events.Registered",
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -235,7 +231,7 @@ export async function inform(
|
|||||||
let toClear = null;
|
let toClear = null;
|
||||||
for (const p of params) {
|
for (const p of params) {
|
||||||
// Don't need to clear wildcards for Events
|
// Don't need to clear wildcards for Events
|
||||||
if (p[0].segments[0] === "Events") {
|
if (p[0].startsWith("Events.")) {
|
||||||
device.set(sessionContext.deviceData, p[0], p[1], p[2]);
|
device.set(sessionContext.deviceData, p[0], p[1], p[2]);
|
||||||
} else {
|
} else {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
@@ -301,69 +297,49 @@ export async function transferComplete(
|
|||||||
let toClear = null;
|
let toClear = null;
|
||||||
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||||
|
|
||||||
let p;
|
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.LastDownload`),
|
|
||||||
);
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
`Downloads.${instance}.LastDownload`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
|
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
);
|
);
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.LastFileType`),
|
|
||||||
);
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
`Downloads.${instance}.LastFileType`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] },
|
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] },
|
||||||
toClear,
|
toClear,
|
||||||
);
|
);
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.LastFileName`),
|
|
||||||
);
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
`Downloads.${instance}.LastFileName`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] },
|
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] },
|
||||||
toClear,
|
toClear,
|
||||||
);
|
);
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.LastTargetFileName`),
|
|
||||||
);
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
`Downloads.${instance}.LastTargetFileName`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, [operation.args.targetFileName, "xsd:string"]] },
|
{ value: [timestamp, [operation.args.targetFileName, "xsd:string"]] },
|
||||||
toClear,
|
toClear,
|
||||||
);
|
);
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.StartTime`),
|
|
||||||
);
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
`Downloads.${instance}.StartTime`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
|
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
);
|
);
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.CompleteTime`),
|
|
||||||
);
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
`Downloads.${instance}.CompleteTime`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
|
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -387,22 +363,22 @@ function revertDownloadParameters(
|
|||||||
): void {
|
): void {
|
||||||
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||||
|
|
||||||
let p;
|
const lastDownloadPath = sessionContext.deviceData.paths.add(
|
||||||
|
`Downloads.${instance}.LastDownload`,
|
||||||
p = sessionContext.deviceData.paths.add(
|
|
||||||
Path.parse(`Downloads.${instance}.LastDownload`),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastDownload = sessionContext.deviceData.attributes.get(p);
|
const lastDownload =
|
||||||
|
sessionContext.deviceData.attributes.get(lastDownloadPath);
|
||||||
|
|
||||||
p = sessionContext.deviceData.paths.add(
|
const toClear = device.set(
|
||||||
Path.parse(`Downloads.${instance}.Download`),
|
sessionContext.deviceData,
|
||||||
|
`Downloads.${instance}.Download`,
|
||||||
|
timestamp,
|
||||||
|
{
|
||||||
|
value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
|
|
||||||
value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (toClear) {
|
if (toClear) {
|
||||||
for (const c of toClear)
|
for (const c of toClear)
|
||||||
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
|
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
|
||||||
@@ -426,11 +402,10 @@ export async function timeoutOperations(
|
|||||||
throw new Error(`Unknown operation name ${operation.name}`);
|
throw new Error(`Unknown operation name ${operation.name}`);
|
||||||
|
|
||||||
const DOWNLOAD_TIMEOUT =
|
const DOWNLOAD_TIMEOUT =
|
||||||
+localCache.getConfig(
|
localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.downloadTimeout",
|
"cwmp.downloadTimeout",
|
||||||
{},
|
3600,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) * 1000;
|
) * 1000;
|
||||||
|
|
||||||
@@ -443,11 +418,10 @@ export async function timeoutOperations(
|
|||||||
commandKey: commandKey,
|
commandKey: commandKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SUCCESS_ON_TIMEOUT = +localCache.getConfig(
|
const SUCCESS_ON_TIMEOUT = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.downloadSuccessOnTimeout",
|
"cwmp.downloadSuccessOnTimeout",
|
||||||
{},
|
false,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -486,16 +460,15 @@ export async function timeoutOperations(
|
|||||||
export function addProvisions(
|
export function addProvisions(
|
||||||
sessionContext: SessionContext,
|
sessionContext: SessionContext,
|
||||||
channel: string,
|
channel: string,
|
||||||
provisions: [string, ...Expression[]][],
|
provisions: [string, ...Value[]][],
|
||||||
): void {
|
): void {
|
||||||
// Multiply by two because every iteration is two
|
// Multiply by two because every iteration is two
|
||||||
// phases: read and update
|
// phases: read and update
|
||||||
const MAX_ITERATIONS =
|
const MAX_ITERATIONS =
|
||||||
+localCache.getConfig(
|
localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.maxCommitIterations",
|
"cwmp.maxCommitIterations",
|
||||||
{},
|
32,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) * 2;
|
) * 2;
|
||||||
|
|
||||||
@@ -549,8 +522,7 @@ export function clearProvisions(sessionContext: SessionContext): void {
|
|||||||
+localCache.getConfig(
|
+localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.maxCommitIterations",
|
"cwmp.maxCommitIterations",
|
||||||
{},
|
32,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) * 2;
|
) * 2;
|
||||||
|
|
||||||
@@ -848,22 +820,20 @@ function runDeclarations(
|
|||||||
(path.alias | path.wildcard) & 1 ||
|
(path.alias | path.wildcard) & 1 ||
|
||||||
path.segments[0] === "VirtualParameters"
|
path.segments[0] === "VirtualParameters"
|
||||||
) {
|
) {
|
||||||
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters"));
|
sessionContext.deviceData.paths.add("VirtualParameters");
|
||||||
if ((path.alias | path.wildcard) & 2) {
|
if ((path.alias | path.wildcard) & 2) {
|
||||||
sessionContext.deviceData.paths.add(Path.parse("VirtualParameters.*"));
|
sessionContext.deviceData.paths.add("VirtualParameters.*");
|
||||||
for (const k of Object.keys(allVirtualParameters)) {
|
for (const k of Object.keys(allVirtualParameters)) {
|
||||||
sessionContext.deviceData.paths.add(
|
sessionContext.deviceData.paths.add(`VirtualParameters.${k}`);
|
||||||
Path.parse(`VirtualParameters.${k}`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot")
|
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "Reboot")
|
||||||
sessionContext.deviceData.paths.add(Path.parse("Reboot"));
|
sessionContext.deviceData.paths.add("Reboot");
|
||||||
|
|
||||||
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset")
|
if ((path.alias | path.wildcard) & 1 || path.segments[0] === "FactoryReset")
|
||||||
sessionContext.deviceData.paths.add(Path.parse("FactoryReset"));
|
sessionContext.deviceData.paths.add("FactoryReset");
|
||||||
|
|
||||||
if (path.alias) {
|
if (path.alias) {
|
||||||
const aliasDecs = device.getAliasDeclarations(
|
const aliasDecs = device.getAliasDeclarations(
|
||||||
@@ -871,12 +841,12 @@ function runDeclarations(
|
|||||||
declaration.pathGet || 1,
|
declaration.pathGet || 1,
|
||||||
);
|
);
|
||||||
for (const ad of aliasDecs) {
|
for (const ad of aliasDecs) {
|
||||||
const p = sessionContext.deviceData.paths.add(ad.path);
|
const p = sessionContext.deviceData.paths.add(ad.path.toString());
|
||||||
allDeclareTimestamps.set(
|
allDeclareTimestamps.set(
|
||||||
p,
|
p,
|
||||||
Math.max(ad.pathGet || 1, allDeclareTimestamps.get(p) || 0),
|
Math.max(ad.pathGet || 1, allDeclareTimestamps.get(p) || 0),
|
||||||
);
|
);
|
||||||
let attrTrackers;
|
let attrTrackers: string[];
|
||||||
if (ad.attrGet) {
|
if (ad.attrGet) {
|
||||||
attrTrackers = Object.keys(ad.attrGet);
|
attrTrackers = Object.keys(ad.attrGet);
|
||||||
mergeAttributeTimestamps(p, ad.attrGet);
|
mergeAttributeTimestamps(p, ad.attrGet);
|
||||||
@@ -884,7 +854,7 @@ function runDeclarations(
|
|||||||
|
|
||||||
device.track(
|
device.track(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
p.toString(),
|
||||||
"prerequisite",
|
"prerequisite",
|
||||||
attrTrackers,
|
attrTrackers,
|
||||||
);
|
);
|
||||||
@@ -900,14 +870,14 @@ function runDeclarations(
|
|||||||
mergeAttributeTimestamps(u, declaration.attrGet);
|
mergeAttributeTimestamps(u, declaration.attrGet);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path = sessionContext.deviceData.paths.add(path);
|
path = sessionContext.deviceData.paths.add(path.toString());
|
||||||
allDeclareTimestamps.set(
|
allDeclareTimestamps.set(
|
||||||
path,
|
path,
|
||||||
Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(path) || 0),
|
Math.max(declaration.pathGet || 1, allDeclareTimestamps.get(path) || 0),
|
||||||
);
|
);
|
||||||
if (declaration.attrGet)
|
if (declaration.attrGet)
|
||||||
mergeAttributeTimestamps(path, declaration.attrGet);
|
mergeAttributeTimestamps(path, declaration.attrGet);
|
||||||
device.track(sessionContext.deviceData, path, "prerequisite");
|
device.track(sessionContext.deviceData, path.toString(), "prerequisite");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (declaration.attrSet) {
|
if (declaration.attrSet) {
|
||||||
@@ -928,7 +898,7 @@ function runDeclarations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (declaration.pathSet != null) {
|
if (declaration.pathSet != null) {
|
||||||
let minInstances, maxInstances;
|
let minInstances: number, maxInstances: number;
|
||||||
if (Array.isArray(declaration.pathSet)) {
|
if (Array.isArray(declaration.pathSet)) {
|
||||||
minInstances = declaration.pathSet[0];
|
minInstances = declaration.pathSet[0];
|
||||||
maxInstances = declaration.pathSet[1];
|
maxInstances = declaration.pathSet[1];
|
||||||
@@ -938,17 +908,19 @@ function runDeclarations(
|
|||||||
|
|
||||||
let parent = path.slice(0, -1);
|
let parent = path.slice(0, -1);
|
||||||
|
|
||||||
let keys;
|
let keys: Record<string, string>;
|
||||||
if (Array.isArray(path.segments[path.length - 1])) {
|
if (path.segments[path.length - 1] instanceof Expression) {
|
||||||
keys = {};
|
keys = {};
|
||||||
for (const [p, v] of path.segments[path.length - 1])
|
for (const [p, v] of device.expressionToAlias(
|
||||||
keys[p.toString()] = v;
|
path.segments[path.length - 1] as Expression,
|
||||||
|
))
|
||||||
|
keys[p.toString()] = v as string;
|
||||||
} else if (path.segments[path.length - 1] === "*") {
|
} else if (path.segments[path.length - 1] === "*") {
|
||||||
keys = {};
|
keys = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parent.wildcard && !parent.alias) {
|
if (!parent.wildcard && !parent.alias) {
|
||||||
parent = sessionContext.deviceData.paths.add(parent);
|
parent = sessionContext.deviceData.paths.add(parent.toString());
|
||||||
if (!unpacked)
|
if (!unpacked)
|
||||||
unpacked = device.unpack(sessionContext.deviceData, path);
|
unpacked = device.unpack(sessionContext.deviceData, path);
|
||||||
|
|
||||||
@@ -1143,8 +1115,7 @@ export async function rpcRequest(
|
|||||||
+localCache.getConfig(
|
+localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.maxCommitIterations",
|
"cwmp.maxCommitIterations",
|
||||||
{},
|
32,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) * 2;
|
) * 2;
|
||||||
|
|
||||||
@@ -1184,7 +1155,7 @@ export async function rpcRequest(
|
|||||||
);
|
);
|
||||||
const timestamp = sessionContext.timestamp + sessionContext.iteration;
|
const timestamp = sessionContext.timestamp + sessionContext.iteration;
|
||||||
|
|
||||||
let toClear;
|
let toClear: Clear[];
|
||||||
|
|
||||||
const allVirtualParameters = localCache.getVirtualParameters(
|
const allVirtualParameters = localCache.getVirtualParameters(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
@@ -1198,7 +1169,7 @@ export async function rpcRequest(
|
|||||||
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
|
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
declaration[0],
|
declaration[0].toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1211,7 +1182,7 @@ export async function rpcRequest(
|
|||||||
for (const k of Object.keys(allVirtualParameters)) {
|
for (const k of Object.keys(allVirtualParameters)) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`VirtualParameters.${k}`),
|
`VirtualParameters.${k}`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -1221,7 +1192,7 @@ export async function rpcRequest(
|
|||||||
}
|
}
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
declaration[0],
|
declaration[0].toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
null,
|
null,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1235,7 +1206,7 @@ export async function rpcRequest(
|
|||||||
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
|
if (!sessionContext.deviceData.attributes.has(declaration[0])) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
declaration[0],
|
declaration[0].toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 0] },
|
{ object: [timestamp, 0] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1247,7 +1218,7 @@ export async function rpcRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.findCompat(
|
||||||
declaration[0],
|
declaration[0],
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@@ -1290,7 +1261,7 @@ export async function rpcRequest(
|
|||||||
return rpcRequest(sessionContext, null);
|
return rpcRequest(sessionContext, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let toClear;
|
let toClear: Clear[];
|
||||||
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||||
|
|
||||||
// Update tags
|
// Update tags
|
||||||
@@ -1299,7 +1270,7 @@ export async function rpcRequest(
|
|||||||
if (v && !c) {
|
if (v && !c) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
p.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -1311,7 +1282,7 @@ export async function rpcRequest(
|
|||||||
} else if (c && !v) {
|
} else if (c && !v) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
p.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
null,
|
null,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1320,11 +1291,11 @@ export async function rpcRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Downloads
|
// Downloads
|
||||||
let index;
|
let index: number;
|
||||||
for (const instance of sessionContext.syncState.downloadsToCreate) {
|
for (const instance of sessionContext.syncState.downloadsToCreate) {
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
index = 0;
|
index = 0;
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.findCompat(
|
||||||
Path.parse("Downloads.*"),
|
Path.parse("Downloads.*"),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@@ -1341,7 +1312,7 @@ export async function rpcRequest(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse("Downloads"),
|
"Downloads",
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 1] },
|
{ object: [timestamp, 1], writable: [timestamp, 1] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1349,7 +1320,7 @@ export async function rpcRequest(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${index}`),
|
`Downloads.${index}`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 1] },
|
{ object: [timestamp, 1], writable: [timestamp, 1] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1383,7 +1354,7 @@ export async function rpcRequest(
|
|||||||
for (const [k, v] of Object.entries(params)) {
|
for (const [k, v] of Object.entries(params)) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${index}.${k}`),
|
`Downloads.${index}.${k}`,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -1399,7 +1370,7 @@ export async function rpcRequest(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${index}.*`),
|
`Downloads.${index}.*`,
|
||||||
timestamp,
|
timestamp,
|
||||||
null,
|
null,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1411,7 +1382,7 @@ export async function rpcRequest(
|
|||||||
for (const instance of sessionContext.syncState.downloadsToDelete) {
|
for (const instance of sessionContext.syncState.downloadsToDelete) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
instance,
|
instance.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
null,
|
null,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1432,7 +1403,7 @@ export async function rpcRequest(
|
|||||||
if (val[0] !== attrs.value[1][0]) {
|
if (val[0] !== attrs.value[1][0]) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p,
|
p.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ value: [timestamp, val] },
|
{ value: [timestamp, val] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1504,7 +1475,7 @@ export async function rpcRequest(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`VirtualParameters.${vparams[i][0]}`),
|
`VirtualParameters.${vparams[i][0]}`,
|
||||||
timestamp,
|
timestamp,
|
||||||
vpu,
|
vpu,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -1527,7 +1498,7 @@ function generateGetRpcRequest(
|
|||||||
|
|
||||||
for (const path of syncState.refreshAttributes.exist) {
|
for (const path of syncState.refreshAttributes.exist) {
|
||||||
let found = false;
|
let found = false;
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.findCompat(
|
||||||
path,
|
path,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@@ -1547,7 +1518,9 @@ function generateGetRpcRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
|
const p = sessionContext.deviceData.paths.add(
|
||||||
|
path.slice(0, -1).toString(),
|
||||||
|
);
|
||||||
syncState.gpn.add(p);
|
syncState.gpn.add(p);
|
||||||
const f = 1 << p.length;
|
const f = 1 << p.length;
|
||||||
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
|
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
|
||||||
@@ -1557,7 +1530,7 @@ function generateGetRpcRequest(
|
|||||||
|
|
||||||
for (const path of syncState.refreshAttributes.object) {
|
for (const path of syncState.refreshAttributes.object) {
|
||||||
let found = false;
|
let found = false;
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.findCompat(
|
||||||
path,
|
path,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@@ -1577,7 +1550,9 @@ function generateGetRpcRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
|
const p = sessionContext.deviceData.paths.add(
|
||||||
|
path.slice(0, -1).toString(),
|
||||||
|
);
|
||||||
syncState.gpn.add(p);
|
syncState.gpn.add(p);
|
||||||
const f = 1 << p.length;
|
const f = 1 << p.length;
|
||||||
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
|
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
|
||||||
@@ -1586,7 +1561,7 @@ function generateGetRpcRequest(
|
|||||||
syncState.refreshAttributes.object.clear();
|
syncState.refreshAttributes.object.clear();
|
||||||
|
|
||||||
for (const path of syncState.refreshAttributes.writable) {
|
for (const path of syncState.refreshAttributes.writable) {
|
||||||
const p = sessionContext.deviceData.paths.add(path.slice(0, -1));
|
const p = sessionContext.deviceData.paths.add(path.slice(0, -1).toString());
|
||||||
syncState.gpn.add(p);
|
syncState.gpn.add(p);
|
||||||
const f = 1 << p.length;
|
const f = 1 << p.length;
|
||||||
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
|
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
|
||||||
@@ -1597,8 +1572,7 @@ function generateGetRpcRequest(
|
|||||||
const GPN_NEXT_LEVEL = localCache.getConfig(
|
const GPN_NEXT_LEVEL = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.gpnNextLevel",
|
"cwmp.gpnNextLevel",
|
||||||
{},
|
0,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) as number;
|
) as number;
|
||||||
|
|
||||||
@@ -1609,11 +1583,10 @@ function generateGetRpcRequest(
|
|||||||
|
|
||||||
// Skip root GPN workaround
|
// Skip root GPN workaround
|
||||||
if (path && !path.length) {
|
if (path && !path.length) {
|
||||||
const SKIP_ROOT_GPN = !!localCache.getConfig(
|
const SKIP_ROOT_GPN = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.skipRootGpn",
|
"cwmp.skipRootGpn",
|
||||||
{},
|
false,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1634,7 +1607,7 @@ function generateGetRpcRequest(
|
|||||||
let est = 0;
|
let est = 0;
|
||||||
if (path.length >= GPN_NEXT_LEVEL) {
|
if (path.length >= GPN_NEXT_LEVEL) {
|
||||||
const patterns: [Path, number][] = [[path, 0]];
|
const patterns: [Path, number][] = [[path, 0]];
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.findCompat(
|
||||||
path,
|
path,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -1651,7 +1624,7 @@ function generateGetRpcRequest(
|
|||||||
syncState.gpn.delete(path);
|
syncState.gpn.delete(path);
|
||||||
} else {
|
} else {
|
||||||
nextLevel = false;
|
nextLevel = false;
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.findCompat(
|
||||||
path,
|
path,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
@@ -1672,8 +1645,7 @@ function generateGetRpcRequest(
|
|||||||
const GPV_BATCH_SIZE = localCache.getConfig(
|
const GPV_BATCH_SIZE = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.gpvBatchSize",
|
"cwmp.gpvBatchSize",
|
||||||
{},
|
32,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) as number;
|
) as number;
|
||||||
|
|
||||||
@@ -1703,8 +1675,7 @@ function generateGetRpcRequest(
|
|||||||
const GPV_BATCH_SIZE = localCache.getConfig(
|
const GPV_BATCH_SIZE = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.gpvBatchSize",
|
"cwmp.gpvBatchSize",
|
||||||
{},
|
32,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) as number;
|
) as number;
|
||||||
|
|
||||||
@@ -1764,11 +1735,10 @@ function generateSetRpcRequest(
|
|||||||
|
|
||||||
const deviceData = sessionContext.deviceData;
|
const deviceData = sessionContext.deviceData;
|
||||||
|
|
||||||
const SKIP_WRITABLE_CHECK = !!localCache.getConfig(
|
const SKIP_WRITABLE_CHECK = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.skipWritableCheck",
|
"cwmp.skipWritableCheck",
|
||||||
{},
|
false,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1810,24 +1780,21 @@ function generateSetRpcRequest(
|
|||||||
const GPV_BATCH_SIZE = localCache.getConfig(
|
const GPV_BATCH_SIZE = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.gpvBatchSize",
|
"cwmp.gpvBatchSize",
|
||||||
{},
|
32,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
) as number;
|
) as number;
|
||||||
|
|
||||||
const DATETIME_MILLISECONDS = !!localCache.getConfig(
|
const DATETIME_MILLISECONDS = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.datetimeMilliseconds",
|
"cwmp.datetimeMilliseconds",
|
||||||
{},
|
true,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
const BOOLEAN_LITERAL = !!localCache.getConfig(
|
const BOOLEAN_LITERAL = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.booleanLiteral",
|
"cwmp.booleanLiteral",
|
||||||
{},
|
true,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1904,15 +1871,13 @@ function generateSetRpcRequest(
|
|||||||
const t2 = attrs?.value?.[1]?.[0] as number;
|
const t2 = attrs?.value?.[1]?.[0] as number;
|
||||||
if (!(t <= t2)) {
|
if (!(t <= t2)) {
|
||||||
const fileTypeAttrs = deviceData.attributes.get(
|
const fileTypeAttrs = deviceData.attributes.get(
|
||||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType"))),
|
deviceData.paths.get(p.slice(0, -1).toString() + ".FileType"),
|
||||||
);
|
);
|
||||||
const fileNameAttrs = deviceData.attributes.get(
|
const fileNameAttrs = deviceData.attributes.get(
|
||||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileName"))),
|
deviceData.paths.get(p.slice(0, -1).toString() + ".FileName"),
|
||||||
);
|
);
|
||||||
const targetFileNameAttrs = deviceData.attributes.get(
|
const targetFileNameAttrs = deviceData.attributes.get(
|
||||||
deviceData.paths.get(
|
deviceData.paths.get(p.slice(0, -1).toString() + ".TargetFileName"),
|
||||||
p.slice(0, -1).concat(Path.parse("TargetFileName")),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1928,7 +1893,7 @@ function generateSetRpcRequest(
|
|||||||
|
|
||||||
// Reboot
|
// Reboot
|
||||||
if (syncState.reboot > 0 && syncState.reboot <= sessionContext.timestamp) {
|
if (syncState.reboot > 0 && syncState.reboot <= sessionContext.timestamp) {
|
||||||
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
|
const p = sessionContext.deviceData.paths.get("Reboot");
|
||||||
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
||||||
const t = attrs?.value?.[1][0] as number;
|
const t = attrs?.value?.[1][0] as number;
|
||||||
if (!(t >= syncState.reboot)) {
|
if (!(t >= syncState.reboot)) {
|
||||||
@@ -1942,7 +1907,7 @@ function generateSetRpcRequest(
|
|||||||
syncState.factoryReset > 0 &&
|
syncState.factoryReset > 0 &&
|
||||||
syncState.factoryReset <= sessionContext.timestamp
|
syncState.factoryReset <= sessionContext.timestamp
|
||||||
) {
|
) {
|
||||||
const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset"));
|
const p = sessionContext.deviceData.paths.get("FactoryReset");
|
||||||
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
||||||
const t = attrs?.value?.[1][0] as number;
|
const t = attrs?.value?.[1][0] as number;
|
||||||
if (!(t >= syncState.factoryReset)) {
|
if (!(t >= syncState.factoryReset)) {
|
||||||
@@ -2064,8 +2029,7 @@ function processDeclarations(
|
|||||||
const deviceData = sessionContext.deviceData;
|
const deviceData = sessionContext.deviceData;
|
||||||
const syncState = sessionContext.syncState;
|
const syncState = sessionContext.syncState;
|
||||||
|
|
||||||
const root = sessionContext.deviceData.paths.add(Path.parse(""));
|
const paths = deviceData.paths.findCompat(Path.root, false, true, 99);
|
||||||
const paths = deviceData.paths.find(root, false, true, 99);
|
|
||||||
paths.sort((a, b): number =>
|
paths.sort((a, b): number =>
|
||||||
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard,
|
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard,
|
||||||
);
|
);
|
||||||
@@ -2358,7 +2322,7 @@ function processDeclarations(
|
|||||||
allDeclareAttributeTimestamps.size ||
|
allDeclareAttributeTimestamps.size ||
|
||||||
allDeclareAttributeValues.size
|
allDeclareAttributeValues.size
|
||||||
)
|
)
|
||||||
func(root, 1, 0, paths);
|
func(Path.root, 1, 0, [Path.root, ...paths]);
|
||||||
|
|
||||||
return virtualParameterDeclarations;
|
return virtualParameterDeclarations;
|
||||||
}
|
}
|
||||||
@@ -2372,7 +2336,7 @@ function processInstances(
|
|||||||
maxInstances: number,
|
maxInstances: number,
|
||||||
defer: boolean,
|
defer: boolean,
|
||||||
): void {
|
): void {
|
||||||
parent = sessionContext.deviceData.paths.add(parent);
|
parent = sessionContext.deviceData.paths.add(parent.toString());
|
||||||
let instancesToCreate: InstanceSet, instancesToDelete: Set<Path>;
|
let instancesToCreate: InstanceSet, instancesToDelete: Set<Path>;
|
||||||
if (parent.segments[0] === "Downloads") {
|
if (parent.segments[0] === "Downloads") {
|
||||||
if (parent.length !== 1) return;
|
if (parent.length !== 1) return;
|
||||||
@@ -2491,19 +2455,17 @@ export async function rpcResponse(
|
|||||||
if (!parameterList.length) {
|
if (!parameterList.length) {
|
||||||
sessionContext.rpcRequest = null;
|
sessionContext.rpcRequest = null;
|
||||||
} else {
|
} else {
|
||||||
const DATETIME_MILLISECONDS = !!localCache.getConfig(
|
const DATETIME_MILLISECONDS = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.datetimeMilliseconds",
|
"cwmp.datetimeMilliseconds",
|
||||||
{},
|
true,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
const BOOLEAN_LITERAL = !!localCache.getConfig(
|
const BOOLEAN_LITERAL = localCache.getConfig(
|
||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.booleanLiteral",
|
"cwmp.booleanLiteral",
|
||||||
{},
|
true,
|
||||||
sessionContext.timestamp,
|
|
||||||
(e) => configContextCallback(sessionContext, e),
|
(e) => configContextCallback(sessionContext, e),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2543,7 +2505,7 @@ export async function rpcResponse(
|
|||||||
}
|
}
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
path,
|
path.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -2562,7 +2524,7 @@ export async function rpcResponse(
|
|||||||
});
|
});
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(p),
|
p,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -2589,7 +2551,7 @@ export async function rpcResponse(
|
|||||||
}
|
}
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
path,
|
path.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
notification: [timestamp, notification],
|
notification: [timestamp, notification],
|
||||||
@@ -2608,7 +2570,7 @@ export async function rpcResponse(
|
|||||||
});
|
});
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(p),
|
p,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
notification: [timestamp, 0],
|
notification: [timestamp, 0],
|
||||||
@@ -2623,7 +2585,8 @@ export async function rpcResponse(
|
|||||||
return invalidResponse("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
let root: Path;
|
let root: Path;
|
||||||
if (rpcReq.parameterPath.endsWith("."))
|
if (!rpcReq.parameterPath) root = Path.root;
|
||||||
|
else if (rpcReq.parameterPath.endsWith("."))
|
||||||
root = Path.parse(rpcReq.parameterPath.slice(0, -1));
|
root = Path.parse(rpcReq.parameterPath.slice(0, -1));
|
||||||
else root = Path.parse(rpcReq.parameterPath);
|
else root = Path.parse(rpcReq.parameterPath);
|
||||||
|
|
||||||
@@ -2667,7 +2630,7 @@ export async function rpcResponse(
|
|||||||
"VirtualParameters",
|
"VirtualParameters",
|
||||||
"Downloads",
|
"Downloads",
|
||||||
]) {
|
]) {
|
||||||
const p = sessionContext.deviceData.paths.get(Path.parse(n));
|
const p = sessionContext.deviceData.paths.get(n);
|
||||||
if (p && sessionContext.deviceData.attributes.has(p))
|
if (p && sessionContext.deviceData.attributes.has(p))
|
||||||
sessionContext.deviceData.timestamps.set(p, timestamp);
|
sessionContext.deviceData.timestamps.set(p, timestamp);
|
||||||
}
|
}
|
||||||
@@ -2693,7 +2656,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
path,
|
path.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, object ? 1 : 0],
|
object: [timestamp, object ? 1 : 0],
|
||||||
@@ -2706,7 +2669,7 @@ export async function rpcResponse(
|
|||||||
for (const path of wildcardParams) {
|
for (const path of wildcardParams) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
path,
|
path.toString(),
|
||||||
timestamp,
|
timestamp,
|
||||||
null,
|
null,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2719,7 +2682,7 @@ export async function rpcResponse(
|
|||||||
for (const p of rpcReq.parameterList) {
|
for (const p of rpcReq.parameterList) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(p[0]),
|
p[0],
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{
|
{
|
||||||
object: [timestamp + 1, 0],
|
object: [timestamp + 1, 0],
|
||||||
@@ -2755,7 +2718,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(p[0]),
|
p[0],
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
attrs,
|
attrs,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2767,7 +2730,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(rpcReq.objectName + rpcRes.instanceNumber),
|
rpcReq.objectName + rpcRes.instanceNumber,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ object: [timestamp + 1, 1] },
|
{ object: [timestamp + 1, 1] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2778,7 +2741,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(rpcReq.objectName.slice(0, -1)),
|
rpcReq.objectName.slice(0, -1),
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
null,
|
null,
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2789,7 +2752,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse("Reboot"),
|
"Reboot",
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2800,7 +2763,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse("FactoryReset"),
|
"FactoryReset",
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2811,7 +2774,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.Download`),
|
`Downloads.${rpcReq.instance}.Download`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2820,7 +2783,7 @@ export async function rpcResponse(
|
|||||||
if (rpcRes.status === 0) {
|
if (rpcRes.status === 0) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.LastDownload`),
|
`Downloads.${rpcReq.instance}.LastDownload`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{
|
{
|
||||||
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]],
|
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]],
|
||||||
@@ -2830,7 +2793,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.LastFileType`),
|
`Downloads.${rpcReq.instance}.LastFileType`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
|
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2838,7 +2801,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.LastFileName`),
|
`Downloads.${rpcReq.instance}.LastFileName`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
|
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2846,7 +2809,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.LastTargetFileName`),
|
`Downloads.${rpcReq.instance}.LastTargetFileName`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
|
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2854,7 +2817,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.StartTime`),
|
`Downloads.${rpcReq.instance}.StartTime`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
|
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2862,7 +2825,7 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
Path.parse(`Downloads.${rpcReq.instance}.CompleteTime`),
|
`Downloads.${rpcReq.instance}.CompleteTime`,
|
||||||
timestamp + 1,
|
timestamp + 1,
|
||||||
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
|
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
|
||||||
toClear,
|
toClear,
|
||||||
@@ -2977,7 +2940,7 @@ export async function deserialize(
|
|||||||
|
|
||||||
const deviceData = initDeviceData();
|
const deviceData = initDeviceData();
|
||||||
for (const r of sessionContext.deviceData as unknown as any[]) {
|
for (const r of sessionContext.deviceData as unknown as any[]) {
|
||||||
const path = deviceData.paths.add(Path.parse(r[0]));
|
const path = deviceData.paths.add(r[0]);
|
||||||
|
|
||||||
if (r[1]) deviceData.trackers.set(path, r[1]);
|
if (r[1]) deviceData.trackers.set(path, r[1]);
|
||||||
|
|
||||||
@@ -2999,8 +2962,8 @@ export async function serialize(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const deviceData = [];
|
const deviceData = [];
|
||||||
|
|
||||||
for (const path of sessionContext.deviceData.paths.find(
|
for (const path of sessionContext.deviceData.paths.findCompat(
|
||||||
Path.parse(""),
|
Path.root,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
99,
|
99,
|
||||||
|
|||||||
+5
-15
@@ -4,8 +4,7 @@ import Path from "./common/path.ts";
|
|||||||
import PathSet from "./common/path-set.ts";
|
import PathSet from "./common/path-set.ts";
|
||||||
import VersionedMap from "./versioned-map.ts";
|
import VersionedMap from "./versioned-map.ts";
|
||||||
import InstanceSet from "./instance-set.ts";
|
import InstanceSet from "./instance-set.ts";
|
||||||
|
import Expression, { Value } from "./common/expression.ts";
|
||||||
export type Expression = string | number | boolean | null | any[];
|
|
||||||
|
|
||||||
export interface Fault {
|
export interface Fault {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -160,7 +159,7 @@ export interface Task {
|
|||||||
fileName?: string;
|
fileName?: string;
|
||||||
targetFileName?: string;
|
targetFileName?: string;
|
||||||
expiry?: number;
|
expiry?: number;
|
||||||
provisions?: [string, ...Expression[]][];
|
provisions?: [string, ...Value[]][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Operation {
|
export interface Operation {
|
||||||
@@ -470,7 +469,7 @@ export interface Permissions {
|
|||||||
[resource: string]: {
|
[resource: string]: {
|
||||||
access: number;
|
access: number;
|
||||||
filter: Expression;
|
filter: Expression;
|
||||||
validate?: Expression;
|
validate: Expression;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -479,7 +478,7 @@ export interface Permissions {
|
|||||||
export type PermissionSet = {
|
export type PermissionSet = {
|
||||||
[resource: string]: {
|
[resource: string]: {
|
||||||
access: number;
|
access: number;
|
||||||
validate?: Expression;
|
validate: Expression;
|
||||||
filter: Expression;
|
filter: Expression;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
@@ -488,16 +487,7 @@ export interface Config {
|
|||||||
[name: string]: Expression;
|
[name: string]: Expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UiConfig {
|
export type UiConfig = Record<string, string>;
|
||||||
filters: Record<string, unknown>;
|
|
||||||
device: Record<string, unknown>;
|
|
||||||
index: Record<string, unknown>;
|
|
||||||
overview: {
|
|
||||||
charts?: Record<string, unknown>;
|
|
||||||
groups?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
pageSize?: Expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SoapMessage {
|
export interface SoapMessage {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -214,8 +214,17 @@ router.get("/", async (ctx) => {
|
|||||||
// be problematic when using relatives asset paths in HTML
|
// be problematic when using relatives asset paths in HTML
|
||||||
if (ctx.path.endsWith("//")) return;
|
if (ctx.path.endsWith("//")) return;
|
||||||
|
|
||||||
const permissionSets: PermissionSet[] =
|
const ps: PermissionSet[] = ctx.state.authorizer.getPermissionSets();
|
||||||
ctx.state.authorizer.getPermissionSets();
|
const permissionSets = ps.map((p) =>
|
||||||
|
p.map((s) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(s).map(([resource, { access, validate, filter }]) => [
|
||||||
|
resource,
|
||||||
|
{ access, validate: validate.toString(), filter: filter.toString() },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let wizard = "";
|
let wizard = "";
|
||||||
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
|
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
|
||||||
@@ -231,9 +240,7 @@ router.get("/", async (ctx) => {
|
|||||||
<body class="h-full bg-stone-100">
|
<body class="h-full bg-stone-100">
|
||||||
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
|
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
|
||||||
<script>
|
<script>
|
||||||
window.clientConfig = ${JSON.stringify({
|
window.clientConfig = ${JSON.stringify(localCache.getUiConfig(ctx.state.configSnapshot))};
|
||||||
ui: localCache.getUiConfig(ctx.state.configSnapshot),
|
|
||||||
})};
|
|
||||||
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
|
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
|
||||||
window.genieacsVersion = ${JSON.stringify(VERSION)};
|
window.genieacsVersion = ${JSON.stringify(VERSION)};
|
||||||
window.username = ${JSON.stringify(
|
window.username = ${JSON.stringify(
|
||||||
|
|||||||
+213
-141
@@ -3,11 +3,11 @@ import Router from "@koa/router";
|
|||||||
import { ObjectId } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import * as db from "./db.ts";
|
import * as db from "./db.ts";
|
||||||
import * as apiFunctions from "../api-functions.ts";
|
import * as apiFunctions from "../api-functions.ts";
|
||||||
import { evaluate, and, extractParams } from "../common/expression/util.ts";
|
import Expression, { extractPaths } from "../common/expression.ts";
|
||||||
import { parse } from "../common/expression/parser.ts";
|
import Path from "../common/path.ts";
|
||||||
import * as logger from "../logger.ts";
|
import * as logger from "../logger.ts";
|
||||||
import { getConfig } from "../ui/local-cache.ts";
|
import { getConfig } from "../ui/local-cache.ts";
|
||||||
import { Expression, Task } from "../types.ts";
|
import { Task } from "../types.ts";
|
||||||
import { generateSalt, hashPassword } from "../auth.ts";
|
import { generateSalt, hashPassword } from "../auth.ts";
|
||||||
import { del } from "../cache.ts";
|
import { del } from "../cache.ts";
|
||||||
import Authorizer from "../common/authorizer.ts";
|
import Authorizer from "../common/authorizer.ts";
|
||||||
@@ -67,11 +67,14 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
|||||||
id: ctx.params.id,
|
id: ctx.params.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter("devices", 2), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter("devices", 2),
|
||||||
["PARAM", RESOURCE_IDS.devices],
|
new Expression.Binary(
|
||||||
ctx.params.id,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS.devices)),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (!authorizer.hasAccess("devices", 2)) {
|
if (!authorizer.hasAccess("devices", 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
@@ -89,36 +92,51 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp",
|
"Parameter,Object,Writable,Value,Value type,Timestamp,Value timestamp,Notification,Access list,Attributes timestamp",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const k of Object.keys(device).sort()) {
|
const keys = Object.keys(device).sort();
|
||||||
const p = device[k];
|
let prevParam = "";
|
||||||
let value = "";
|
let attrs: Record<string, string | number | boolean | null> = {};
|
||||||
let type = "";
|
|
||||||
if (p.value) {
|
function flushRow(): void {
|
||||||
value = p.value[0];
|
if (!prevParam) return;
|
||||||
type = p.value[1];
|
let value: string | number | boolean | null = attrs["value"] ?? "";
|
||||||
if (type === "xsd:dateTime" && typeof value === "number")
|
if (attrs["type"] === "xsd:dateTime" && typeof value === "number")
|
||||||
value = new Date(value).toJSON();
|
value = new Date(value).toJSON();
|
||||||
}
|
|
||||||
|
|
||||||
const row = [
|
const row = [
|
||||||
k,
|
prevParam,
|
||||||
p.object,
|
attrs["object"] ?? "",
|
||||||
new Date(p.objectTimestamp).toJSON(),
|
attrs["writable"] ?? "",
|
||||||
p.writable,
|
|
||||||
new Date(p.writableTimestamp).toJSON(),
|
|
||||||
`"${String(value).replace(/"/g, '""')}"`,
|
`"${String(value).replace(/"/g, '""')}"`,
|
||||||
type,
|
attrs["type"] ?? "",
|
||||||
new Date(p.valueTimestamp).toJSON(),
|
attrs["timestamp"] != null ? new Date(+attrs["timestamp"]).toJSON() : "",
|
||||||
p.notification,
|
attrs["valueTimestamp"] != null
|
||||||
new Date(p.notificationTimestamp).toJSON(),
|
? new Date(+attrs["valueTimestamp"]).toJSON()
|
||||||
p.accessList ? p.accessList.join(", ") : "",
|
: "",
|
||||||
new Date(p.accessListTimestamp).toJSON(),
|
attrs["notification"] ?? "",
|
||||||
|
attrs["accessList"] ?? "",
|
||||||
|
attrs["attributesTimestamp"] != null
|
||||||
|
? new Date(+attrs["attributesTimestamp"]).toJSON()
|
||||||
|
: "",
|
||||||
];
|
];
|
||||||
lines.push(row.map((r) => (r != null ? r : "")).join(","));
|
lines.push(row.map((r) => (r != null ? r : "")).join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const k of keys) {
|
||||||
|
const colonIdx = k.lastIndexOf(":");
|
||||||
|
const param = colonIdx === -1 ? k : k.slice(0, colonIdx);
|
||||||
|
const attr = colonIdx === -1 ? "value" : k.slice(colonIdx + 1);
|
||||||
|
|
||||||
|
if (param !== prevParam) {
|
||||||
|
flushRow();
|
||||||
|
prevParam = param;
|
||||||
|
attrs = {};
|
||||||
|
}
|
||||||
|
attrs[attr] = device[k];
|
||||||
|
}
|
||||||
|
flushRow();
|
||||||
ctx.body = lines.join("\n");
|
ctx.body = lines.join("\n");
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
});
|
});
|
||||||
@@ -128,7 +146,10 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
let filter: Expression = authorizer.getFilter(resource, 1);
|
let filter: Expression = authorizer.getFilter(resource, 1);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
filter = Expression.and(
|
||||||
|
filter,
|
||||||
|
Expression.parse(singleParam(ctx.request.query.filter)),
|
||||||
|
);
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: `Count ${resource}`,
|
message: `Count ${resource}`,
|
||||||
@@ -144,11 +165,18 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
|
|
||||||
// Exclude temporary tasks and faults
|
// Exclude temporary tasks and faults
|
||||||
if (resource === "tasks" || resource === "faults") {
|
if (resource === "tasks" || resource === "faults") {
|
||||||
filter = and(filter, [
|
const p = new Expression.Parameter(Path.parse("expiry"));
|
||||||
"OR",
|
filter = Expression.and(
|
||||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
filter,
|
||||||
["IS NULL", ["PARAM", "expiry"]],
|
Expression.or(
|
||||||
]);
|
new Expression.Binary(
|
||||||
|
">=",
|
||||||
|
p,
|
||||||
|
new Expression.Literal(Date.now() + 60000),
|
||||||
|
),
|
||||||
|
new Expression.Unary("IS NULL", p),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = await db.count(resource, filter);
|
const count = await db.count(resource, filter);
|
||||||
@@ -165,7 +193,10 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const options: Parameters<typeof db.query>[2] = {};
|
const options: Parameters<typeof db.query>[2] = {};
|
||||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
filter = Expression.and(
|
||||||
|
filter,
|
||||||
|
Expression.parse(singleParam(ctx.request.query.filter)),
|
||||||
|
);
|
||||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||||
if (ctx.request.query.sort)
|
if (ctx.request.query.sort)
|
||||||
@@ -193,11 +224,18 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
|
|
||||||
// Exclude temporary tasks and faults
|
// Exclude temporary tasks and faults
|
||||||
if (resource === "tasks" || resource === "faults") {
|
if (resource === "tasks" || resource === "faults") {
|
||||||
filter = and(filter, [
|
const p = new Expression.Parameter(Path.parse("expiry"));
|
||||||
"OR",
|
filter = Expression.and(
|
||||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
filter,
|
||||||
["IS NULL", ["PARAM", "expiry"]],
|
Expression.or(
|
||||||
]);
|
new Expression.Binary(
|
||||||
|
">=",
|
||||||
|
p,
|
||||||
|
new Expression.Literal(Date.now() + 60000),
|
||||||
|
),
|
||||||
|
new Expression.Unary("IS NULL", p),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
@@ -219,7 +257,10 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const options: Parameters<typeof db.query>[2] = { projection: {} };
|
const options: Parameters<typeof db.query>[2] = { projection: {} };
|
||||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
filter = Expression.and(
|
||||||
|
filter,
|
||||||
|
Expression.parse(singleParam(ctx.request.query.filter)),
|
||||||
|
);
|
||||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||||
if (ctx.request.query.sort)
|
if (ctx.request.query.sort)
|
||||||
@@ -239,25 +280,38 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: Record<string, Expression> = JSON.parse(
|
const columnsStr: Record<string, string> = JSON.parse(
|
||||||
singleParam(ctx.request.query.columns),
|
singleParam(ctx.request.query.columns),
|
||||||
);
|
);
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(columns)) {
|
const now = Date.now();
|
||||||
const e = evaluate(parse(v as string), null, now);
|
const columns: Record<string, Expression> = Object.fromEntries(
|
||||||
columns[k] = e;
|
Object.entries(columnsStr).map(([k, v]) => {
|
||||||
for (const p of extractParams(e))
|
let exp = Expression.parse(v);
|
||||||
if (typeof p === "string") options.projection[p] = 1;
|
exp = exp.evaluate((e) => {
|
||||||
}
|
if (e instanceof Expression.FunctionCall && e.name === "NOW")
|
||||||
|
return new Expression.Literal(now);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
for (const p of extractPaths(exp)) options.projection[p.toString()] = 1;
|
||||||
|
return [k, exp];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Exclude temporary tasks and faults
|
// Exclude temporary tasks and faults
|
||||||
if (resource === "tasks" || resource === "faults") {
|
if (resource === "tasks" || resource === "faults") {
|
||||||
filter = and(filter, [
|
const p = new Expression.Parameter(Path.parse("expiry"));
|
||||||
"OR",
|
filter = Expression.and(
|
||||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
filter,
|
||||||
["IS NULL", ["PARAM", "expiry"]],
|
Expression.or(
|
||||||
]);
|
new Expression.Binary(
|
||||||
|
">=",
|
||||||
|
p,
|
||||||
|
new Expression.Literal(Date.now() + 60000),
|
||||||
|
),
|
||||||
|
new Expression.Unary("IS NULL", p),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
@@ -272,45 +326,40 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
"\n";
|
"\n";
|
||||||
for await (const obj of db.query(resource, filter, options)) {
|
for await (const obj of db.query(resource, filter, options)) {
|
||||||
const arr = Object.values(columns).map((exp) => {
|
const arr = Object.values(columns).map((exp) => {
|
||||||
const v = evaluate(exp, obj, null, (e) => {
|
return exp.evaluate((e) => {
|
||||||
if (Array.isArray(e)) {
|
if (e instanceof Expression.Literal) return e;
|
||||||
if (e[0] === "PARAM") {
|
else if (e instanceof Expression.FunctionCall) {
|
||||||
if (resource === "devices") {
|
if (e.name === "NOW") return new Expression.Literal(now);
|
||||||
if (e[1] === "Tags") {
|
if (e.name === "DATE_STRING") {
|
||||||
const tags = [];
|
if (e.args[0] instanceof Expression.Literal)
|
||||||
for (const p in obj)
|
return new Expression.Literal(
|
||||||
if (p.startsWith("Tags."))
|
new Date(e.args[0].value as number).toJSON(),
|
||||||
tags.push(decodeTag(p.slice(5)));
|
);
|
||||||
|
|
||||||
return tags.join(", ");
|
|
||||||
}
|
|
||||||
if (e === exp) {
|
|
||||||
const p = obj[e[1]];
|
|
||||||
if (
|
|
||||||
p &&
|
|
||||||
p.value &&
|
|
||||||
p.value[1] === "xsd:dateTime" &&
|
|
||||||
typeof p.value[0] === "number"
|
|
||||||
)
|
|
||||||
return new Date(p.value[0]).toJSON();
|
|
||||||
}
|
|
||||||
} else if (resource === "faults") {
|
|
||||||
if (e[1] === "detail") return yamlStringify(obj["detail"]);
|
|
||||||
}
|
|
||||||
} else if (e[0] === "FUNC") {
|
|
||||||
if (e[1] === "DATE_STRING") {
|
|
||||||
if (e[2] && !Array.isArray(e[2]))
|
|
||||||
return new Date(e[2]).toJSON();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (e instanceof Expression.Parameter) {
|
||||||
|
let v = obj[e.path.toString()];
|
||||||
|
if (resource === "devices") {
|
||||||
|
if (e.path.toString() === "Tags") {
|
||||||
|
const tags = [];
|
||||||
|
for (const p in obj)
|
||||||
|
if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
|
||||||
|
tags.push(decodeTag(p.slice(5)));
|
||||||
|
v = tags.join(", ");
|
||||||
|
}
|
||||||
|
if (e === exp) {
|
||||||
|
const type = obj[e.path.toString() + ":type"];
|
||||||
|
if (type === "xsd:dateTime" && typeof v === "number")
|
||||||
|
v = new Date(v).toJSON();
|
||||||
|
}
|
||||||
|
} else if (resource === "faults") {
|
||||||
|
if (e.path.toString() === "detail") v = yamlStringify(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "string") v = `"${v.replace(/"/g, '""')}"`;
|
||||||
|
if (v != null) return new Expression.Literal(v);
|
||||||
}
|
}
|
||||||
|
return new Expression.Literal(null);
|
||||||
return e;
|
}).value;
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(v) || v == null) return "";
|
|
||||||
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
|
|
||||||
return v;
|
|
||||||
});
|
});
|
||||||
yield arr.join(",") + "\n";
|
yield arr.join(",") + "\n";
|
||||||
}
|
}
|
||||||
@@ -326,11 +375,15 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter(resource, 2), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter(resource, 2),
|
||||||
["PARAM", RESOURCE_IDS[resource]],
|
new Expression.Binary(
|
||||||
ctx.params.id,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (!authorizer.hasAccess(resource, 2)) {
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
@@ -352,11 +405,14 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter(resource, 2), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter(resource, 2),
|
||||||
["PARAM", RESOURCE_IDS[resource]],
|
new Expression.Binary(
|
||||||
ctx.params.id,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
if (!authorizer.hasAccess(resource, 2)) {
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
@@ -379,11 +435,14 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
id: ctx.params.id,
|
id: ctx.params.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter(resource, 3), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter(resource, 3),
|
||||||
["PARAM", RESOURCE_IDS[resource]],
|
new Expression.Binary(
|
||||||
ctx.params.id,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
if (!authorizer.hasAccess(resource, 3)) {
|
if (!authorizer.hasAccess(resource, 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
@@ -467,11 +526,14 @@ router.get("/blob/files/:id", async (ctx) => {
|
|||||||
id: id,
|
id: id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter(resource, 2), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter(resource, 2),
|
||||||
["PARAM", RESOURCE_IDS[resource]],
|
new Expression.Binary(
|
||||||
ctx.params.id,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS[resource])),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (!authorizer.hasAccess(resource, 2)) {
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
@@ -566,11 +628,14 @@ router.post("/devices/:id/tasks", async (ctx) => {
|
|||||||
let statuses: { _id: string; status: string }[];
|
let statuses: { _id: string; status: string }[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filter = and(authorizer.getFilter("devices", 3), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter("devices", 3),
|
||||||
["PARAM", "DeviceID.ID"],
|
new Expression.Binary(
|
||||||
deviceId,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
device = (await db.query("devices", filter).next()).value;
|
device = (await db.query("devices", filter).next()).value;
|
||||||
if (!device) return void (ctx.status = 404);
|
if (!device) return void (ctx.status = 404);
|
||||||
|
|
||||||
@@ -596,30 +661,31 @@ router.post("/devices/:id/tasks", async (ctx) => {
|
|||||||
await releaseLock(`cwmp_session_${deviceId}`, token);
|
await releaseLock(`cwmp_session_${deviceId}`, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
const onlineThreshold = getConfig(
|
const onlineThreshold = getConfig(
|
||||||
ctx.state.configSnapshot,
|
ctx.state.configSnapshot,
|
||||||
"cwmp.deviceOnlineThreshold",
|
"cwmp.deviceOnlineThreshold",
|
||||||
{},
|
4000,
|
||||||
Date.now(),
|
|
||||||
(exp) => {
|
(exp) => {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (exp instanceof Expression.Literal) return exp;
|
||||||
if (exp[0] === "PARAM") {
|
else if (exp instanceof Expression.Parameter) {
|
||||||
const p = device[exp[1]];
|
const p = device[exp.path.toString()];
|
||||||
if (p?.value) return p.value[0];
|
if (p != null) return new Expression.Literal(p);
|
||||||
} else if (exp[0] === "FUNC") {
|
} else if (exp instanceof Expression.FunctionCall) {
|
||||||
if (exp[1] === "REMOTE_ADDRESS") {
|
if (exp.name === "NOW") return new Expression.Literal(now);
|
||||||
|
if (exp.name === "REMOTE_ADDRESS") {
|
||||||
for (const root of ["InternetGatewayDevice", "Device"]) {
|
for (const root of ["InternetGatewayDevice", "Device"]) {
|
||||||
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
||||||
if (p?.value) return new URL(p.value[0]).hostname;
|
if (p != null) return new Expression.Literal(new URL(p).hostname);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exp;
|
return new Expression.Literal(null);
|
||||||
},
|
},
|
||||||
) as number;
|
);
|
||||||
|
|
||||||
const lastInform = device["Events.Inform"].value[0] as number;
|
const lastInform = device["Events.Inform"] as number;
|
||||||
|
|
||||||
let status = await apiFunctions.connectionRequest(deviceId, device);
|
let status = await apiFunctions.connectionRequest(deviceId, device);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -667,11 +733,14 @@ router.post("/devices/:id/tags", async (ctx) => {
|
|||||||
tags: ctx.request.body,
|
tags: ctx.request.body,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = and(authorizer.getFilter("devices", 3), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter("devices", 3),
|
||||||
["PARAM", "DeviceID.ID"],
|
new Expression.Binary(
|
||||||
ctx.params.id,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS["devices"])),
|
||||||
|
new Expression.Literal(ctx.params.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
if (!authorizer.hasAccess("devices", 3)) {
|
if (!authorizer.hasAccess("devices", 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
@@ -745,11 +814,14 @@ router.put("/users/:id/password", async (ctx) => {
|
|||||||
const newPassword = ctx.request.body.newPassword;
|
const newPassword = ctx.request.body.newPassword;
|
||||||
|
|
||||||
if (ctx.state.user) {
|
if (ctx.state.user) {
|
||||||
const filter = and(authorizer.getFilter("users", 3), [
|
const filter = Expression.and(
|
||||||
"=",
|
authorizer.getFilter("users", 3),
|
||||||
["PARAM", RESOURCE_IDS.users],
|
new Expression.Binary(
|
||||||
username,
|
"=",
|
||||||
]);
|
new Expression.Parameter(Path.parse(RESOURCE_IDS["users"])),
|
||||||
|
new Expression.Literal(username),
|
||||||
|
),
|
||||||
|
);
|
||||||
const { value: res } = await db.query("users", filter).next();
|
const { value: res } = await db.query("users", filter).next();
|
||||||
if (!res) return void (ctx.status = 404);
|
if (!res) return void (ctx.status = 404);
|
||||||
const validate = authorizer.getValidator("users", res);
|
const validate = authorizer.getValidator("users", res);
|
||||||
|
|||||||
+66
-136
@@ -2,12 +2,11 @@ import { Script } from "node:vm";
|
|||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { Collection, ObjectId, WithoutId } from "mongodb";
|
import { Collection, ObjectId, WithoutId } from "mongodb";
|
||||||
import { encodeTag } from "../util.ts";
|
import { encodeTag } from "../util.ts";
|
||||||
import { evaluate } from "../common/expression/util.ts";
|
import { Fault, Task } from "../types.ts";
|
||||||
import { Expression, Fault, Task } from "../types.ts";
|
|
||||||
import { collections, filesBucket } from "../db/db.ts";
|
import { collections, filesBucket } from "../db/db.ts";
|
||||||
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
|
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
|
||||||
import * as MongoTypes from "../db/types.ts";
|
import * as MongoTypes from "../db/types.ts";
|
||||||
import { parse, parseList, stringify } from "../common/expression/parser.ts";
|
import Expression, { parseList, Value } from "../common/expression.ts";
|
||||||
import { toMongoQuery } from "../db/synth.ts";
|
import { toMongoQuery } from "../db/synth.ts";
|
||||||
|
|
||||||
function processDeviceProjection(
|
function processDeviceProjection(
|
||||||
@@ -61,21 +60,8 @@ function parseDate(d: Date): number | string {
|
|||||||
return isNaN(n) ? "" + d : n;
|
return isNaN(n) ? "" + d : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlatAttributes {
|
|
||||||
object?: boolean;
|
|
||||||
objectTimestamp?: number;
|
|
||||||
writable?: boolean;
|
|
||||||
writableTimestamp?: number;
|
|
||||||
value?: [string | number | boolean, string];
|
|
||||||
valueTimestamp?: number;
|
|
||||||
notification?: number;
|
|
||||||
notificationTimestamp?: number;
|
|
||||||
accessList?: string[];
|
|
||||||
accessListTimestamp?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlatDevice {
|
export interface FlatDevice {
|
||||||
[param: string]: FlatAttributes;
|
[param: string]: Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
||||||
@@ -88,100 +74,38 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
|||||||
for (const [name, tree] of Object.entries(input)) {
|
for (const [name, tree] of Object.entries(input)) {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
if (name === "_lastInform") {
|
if (name === "_lastInform") {
|
||||||
output["Events.Inform"] = {
|
output["Events.Inform"] = parseDate(tree as Date);
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
output["Events.Inform:type"] = "xsd:dateTime";
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_registered") {
|
} else if (name === "_registered") {
|
||||||
output["Events.Registered"] = {
|
output["Events.Registered"] = parseDate(tree as Date);
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
output["Events.Registered:type"] = "xsd:dateTime";
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_lastBoot") {
|
} else if (name === "_lastBoot") {
|
||||||
output["Events.1_BOOT"] = {
|
output["Events.1_BOOT"] = parseDate(tree as Date);
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
output["Events.1_BOOT:type"] = "xsd:dateTime";
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_lastBootstrap") {
|
} else if (name === "_lastBootstrap") {
|
||||||
output["Events.0_BOOTSTRAP"] = {
|
output["Events.0_BOOTSTRAP"] = parseDate(tree as Date);
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
output["Events.0_BOOTSTRAP:type"] = "xsd:dateTime";
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_id") {
|
} else if (name === "_id") {
|
||||||
output["DeviceID.ID"] = {
|
output["DeviceID.ID"] = tree as string;
|
||||||
value: [tree as string, "xsd:string"],
|
output["DeviceID.ID:type"] = "xsd:dateTime";
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_deviceId") {
|
} else if (name === "_deviceId") {
|
||||||
output["DeviceID.Manufacturer"] = {
|
output["DeviceID.Manufacturer"] = tree["_Manufacturer"];
|
||||||
value: [tree["_Manufacturer"], "xsd:string"],
|
output["DeviceID.Manufacturer:type"] = "xsd:string";
|
||||||
valueTimestamp: timestamp,
|
output["DeviceID.OUI"] = tree["_OUI"];
|
||||||
writable: false,
|
output["DeviceID.OUI:type"] = "xsd:string";
|
||||||
writableTimestamp: timestamp,
|
output["DeviceID.ProductClass"] = tree["_ProductClass"];
|
||||||
object: false,
|
output["DeviceID.ProductClass:type"] = "xsd:string";
|
||||||
objectTimestamp: timestamp,
|
output["DeviceID.SerialNumber"] = tree["_SerialNumber"];
|
||||||
};
|
output["DeviceID.SerialNumber:type"] = "xsd:string";
|
||||||
output["DeviceID.OUI"] = {
|
|
||||||
value: [tree["_OUI"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
output["DeviceID.ProductClass"] = {
|
|
||||||
value: [tree["_ProductClass"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
output["DeviceID.SerialNumber"] = {
|
|
||||||
value: [tree["_SerialNumber"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_tags") {
|
} else if (name === "_tags") {
|
||||||
output["Tags"] = {
|
output["Tags:object"] = true;
|
||||||
writable: false,
|
output["Tags:writable"] = true;
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: true,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const t of tree as string[]) {
|
for (const t of tree as string[]) {
|
||||||
output[`Tags.${encodeTag(t)}`] = {
|
const et = encodeTag(t);
|
||||||
value: [true, "xsd:boolean"],
|
output[`Tags.${et}`] = true;
|
||||||
valueTimestamp: timestamp,
|
output[`Tags.${et}:type`] = "xsd:boolean";
|
||||||
writable: true,
|
output[`Tags.${et}:writable`] = true;
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,40 +118,37 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
|||||||
else if (+input["_timestamp"] > timestamp)
|
else if (+input["_timestamp"] > timestamp)
|
||||||
childrenTimestamp = +input["_timestamp"];
|
childrenTimestamp = +input["_timestamp"];
|
||||||
|
|
||||||
const attrs: FlatAttributes = {};
|
const r = root ? `${root}.${name}` : name;
|
||||||
|
|
||||||
if (tree["_value"] != null) {
|
if (tree["_value"] != null) {
|
||||||
attrs.value = [
|
output[r] =
|
||||||
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"];
|
||||||
tree["_type"],
|
output[`${r}:type`] = tree["_type"];
|
||||||
];
|
output[`${r}:timestamp`] = childrenTimestamp;
|
||||||
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
|
output[`${r}:valueTimestamp`] = +(
|
||||||
attrs.object = false;
|
tree["_timestamp"] || childrenTimestamp
|
||||||
attrs.objectTimestamp = childrenTimestamp;
|
);
|
||||||
} else if (tree["_object"] != null) {
|
} else if (tree["_object"] != null) {
|
||||||
attrs.object = tree["_object"];
|
output[`${r}:object`] = tree["_object"];
|
||||||
attrs.objectTimestamp = childrenTimestamp;
|
output[`${r}:timestamp`] = childrenTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tree["_writable"] != null) {
|
if (tree["_writable"] != null) {
|
||||||
attrs.writable = tree["_writable"];
|
output[`${r}:writable`] = tree["_writable"];
|
||||||
attrs.writableTimestamp = childrenTimestamp;
|
output[`${r}:timestamp`] = childrenTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tree["_notification"] != null) {
|
if (tree["_notification"] != null) {
|
||||||
attrs.notification = tree["_notification"];
|
output[`${r}:notification`] = tree["_notification"];
|
||||||
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
|
output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tree["_accessList"] != null) {
|
if (tree["_accessList"] != null) {
|
||||||
attrs.accessList = tree["_accessList"];
|
output[`${r}:accessList`] = tree["_accessList"].join(",");
|
||||||
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
|
output[`${r}:attributesTimestamp`] = +tree["_attributesTimestamp"] || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = root ? `${root}.${name}` : name;
|
recursive(tree, r, output, childrenTimestamp);
|
||||||
output[r] = attrs;
|
|
||||||
|
|
||||||
if (attrs.object || tree["object"] == null)
|
|
||||||
recursive(tree, r, output, childrenTimestamp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,14 +180,12 @@ function flattenPreset(
|
|||||||
if (p.precondition) {
|
if (p.precondition) {
|
||||||
try {
|
try {
|
||||||
// Try parse to check expression validity
|
// Try parse to check expression validity
|
||||||
parse(p.precondition as string);
|
Expression.parse(p.precondition as string);
|
||||||
} catch {
|
} catch {
|
||||||
p.precondition = convertOldPrecondition(
|
const e = convertOldPrecondition(JSON.parse(p.precondition as string));
|
||||||
JSON.parse(p.precondition as string),
|
if (e instanceof Expression.Literal && e.value)
|
||||||
);
|
p.precondition = e.toString();
|
||||||
p.precondition = (p.precondition as string).length
|
else p.precondition = "";
|
||||||
? stringify(p.precondition as Expression)
|
|
||||||
: "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +204,7 @@ function flattenPreset(
|
|||||||
) {
|
) {
|
||||||
p.provision = provision.name;
|
p.provision = provision.name;
|
||||||
p.provisionArgs = provision.args
|
p.provisionArgs = provision.args
|
||||||
? provision.args.map((a) => stringify(a)).join(", ")
|
? provision.args.map((a) => a.toString()).join(", ")
|
||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,8 +228,7 @@ function preProcessPreset(data: Record<string, unknown>): MongoTypes.Preset {
|
|||||||
const preset = Object.assign({}, data);
|
const preset = Object.assign({}, data);
|
||||||
|
|
||||||
if (!preset.precondition) preset.precondition = "";
|
if (!preset.precondition) preset.precondition = "";
|
||||||
// Try parse to check expression validity
|
else Expression.parse(preset.precondition as string); // Try parse to check validity
|
||||||
parse(preset.precondition as string);
|
|
||||||
|
|
||||||
preset.weight = parseInt(preset.weight as string) || 0;
|
preset.weight = parseInt(preset.weight as string) || 0;
|
||||||
|
|
||||||
@@ -361,7 +279,13 @@ export async function* query(
|
|||||||
options?: QueryOptions,
|
options?: QueryOptions,
|
||||||
): AsyncGenerator<any, void, undefined> {
|
): AsyncGenerator<any, void, undefined> {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
filter = evaluate(filter, null, Date.now());
|
const now = Date.now();
|
||||||
|
filter = filter.evaluate((e) => {
|
||||||
|
if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(now);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
const q = toMongoQuery(filter, resource);
|
const q = toMongoQuery(filter, resource);
|
||||||
if (!q) return;
|
if (!q) return;
|
||||||
|
|
||||||
@@ -408,7 +332,13 @@ export async function* query(
|
|||||||
|
|
||||||
export function count(resource: string, filter: Expression): Promise<number> {
|
export function count(resource: string, filter: Expression): Promise<number> {
|
||||||
const collection = collections[resource] as Collection<any>;
|
const collection = collections[resource] as Collection<any>;
|
||||||
filter = evaluate(filter, null, Date.now());
|
const now = Date.now();
|
||||||
|
filter = filter.evaluate((e) => {
|
||||||
|
if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(now);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
const q = toMongoQuery(filter, resource);
|
const q = toMongoQuery(filter, resource);
|
||||||
if (!q) return Promise.resolve(0);
|
if (!q) return Promise.resolve(0);
|
||||||
return collection.countDocuments(q);
|
return collection.countDocuments(q);
|
||||||
|
|||||||
+39
-66
@@ -1,9 +1,7 @@
|
|||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
import * as config from "../config.ts";
|
|
||||||
import { collections } from "../db/db.ts";
|
import { collections } from "../db/db.ts";
|
||||||
import * as expression from "../common/expression/util.ts";
|
import Expression, { Value } from "../common/expression.ts";
|
||||||
import { parse } from "../common/expression/parser.ts";
|
import { Users, Permissions, Config, UiConfig } from "../types.ts";
|
||||||
import { Expression, Users, Permissions, Config, UiConfig } from "../types.ts";
|
|
||||||
import { LocalCache } from "../local-cache.ts";
|
import { LocalCache } from "../local-cache.ts";
|
||||||
|
|
||||||
interface Snapshot {
|
interface Snapshot {
|
||||||
@@ -26,12 +24,14 @@ async function fetchPermissions(): Promise<[string, Permissions]> {
|
|||||||
if (!permissions[p.role]) permissions[p.role] = {};
|
if (!permissions[p.role]) permissions[p.role] = {};
|
||||||
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
|
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
|
||||||
|
|
||||||
|
let validate: Expression;
|
||||||
|
if (p.validate) validate = Expression.parse(p.validate);
|
||||||
|
else validate = new Expression.Literal(true);
|
||||||
permissions[p.role][p.access][p.resource] = {
|
permissions[p.role][p.access][p.resource] = {
|
||||||
access: p.access,
|
access: p.access,
|
||||||
filter: parse(p.filter || "true"),
|
filter: Expression.parse(p.filter || "true"),
|
||||||
|
validate,
|
||||||
};
|
};
|
||||||
if (p.validate)
|
|
||||||
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [h, permissions];
|
return [h, permissions];
|
||||||
@@ -62,33 +62,18 @@ async function fetchConfig(): Promise<[string, Config, UiConfig]> {
|
|||||||
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
|
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
|
||||||
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
|
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
|
||||||
|
|
||||||
const ui = {
|
const ui: UiConfig = {};
|
||||||
filters: {},
|
|
||||||
device: {},
|
|
||||||
index: {},
|
|
||||||
overview: {},
|
|
||||||
pageSize: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _config = {};
|
const _config = {};
|
||||||
|
|
||||||
for (const c of conf) {
|
for (const c of conf) {
|
||||||
// Evaluate expressions to simplify them
|
|
||||||
const val = expression.evaluate(parse(c.value));
|
|
||||||
_config[c._id] = val;
|
|
||||||
if (c._id.startsWith("ui.")) {
|
if (c._id.startsWith("ui.")) {
|
||||||
const keys = c._id.split(".");
|
ui[c._id.slice(3)] = c.value;
|
||||||
if (!(keys[1] in ui)) continue;
|
continue;
|
||||||
// remove the first key(ui)
|
|
||||||
keys.shift();
|
|
||||||
let ref = ui;
|
|
||||||
while (keys.length > 1) {
|
|
||||||
const k = keys.shift();
|
|
||||||
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
|
||||||
ref = ref[k];
|
|
||||||
}
|
|
||||||
ref[keys[0]] = val;
|
|
||||||
}
|
}
|
||||||
|
// Evaluate expressions to simplify them
|
||||||
|
const val = Expression.parse(c.value).evaluate((e) => e);
|
||||||
|
_config[c._id] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [h, _config, ui];
|
return [h, _config, ui];
|
||||||
@@ -123,47 +108,35 @@ export async function getRevision(): Promise<string> {
|
|||||||
export function getConfig(
|
export function getConfig(
|
||||||
revision: string,
|
revision: string,
|
||||||
key: string,
|
key: string,
|
||||||
context: Record<string, unknown>,
|
dflt: string,
|
||||||
now: number,
|
fn: (e: Expression) => Expression.Literal,
|
||||||
cb?: (e: Expression) => Expression,
|
): string;
|
||||||
): string | number | boolean | null {
|
export function getConfig(
|
||||||
|
revision: string,
|
||||||
|
key: string,
|
||||||
|
dflt: number,
|
||||||
|
fn: (e: Expression) => Expression.Literal,
|
||||||
|
): number;
|
||||||
|
export function getConfig(
|
||||||
|
revision: string,
|
||||||
|
key: string,
|
||||||
|
dflt: boolean,
|
||||||
|
fn: (e: Expression) => Expression.Literal,
|
||||||
|
): boolean;
|
||||||
|
export function getConfig(
|
||||||
|
revision: string,
|
||||||
|
key: string,
|
||||||
|
dflt: Value,
|
||||||
|
fn: (e: Expression) => Expression.Literal,
|
||||||
|
): Value {
|
||||||
const snapshot = localCache.get(revision);
|
const snapshot = localCache.get(revision);
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
const oldOpts = {
|
const e = snapshot.config[key];
|
||||||
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
|
if (!e) return dflt;
|
||||||
"cwmp.debug": "DEBUG",
|
const v = e.evaluate(fn).value;
|
||||||
"cwmp.retryDelay": "RETRY_DELAY",
|
if (typeof v !== typeof dflt) return dflt;
|
||||||
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
|
return v;
|
||||||
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
|
|
||||||
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
|
|
||||||
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
|
|
||||||
"cwmp.cookiesPath": "COOKIES_PATH",
|
|
||||||
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
|
|
||||||
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
|
|
||||||
"cwmp.connectionRequestAllowBasicAuth":
|
|
||||||
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
|
||||||
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
|
||||||
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
|
||||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(key in snapshot.config)) {
|
|
||||||
if (key in oldOpts) {
|
|
||||||
let id;
|
|
||||||
if (context?.["id"]) {
|
|
||||||
id = context["id"];
|
|
||||||
} else if (cb) {
|
|
||||||
id = cb(["PARAM", "DeviceID.ID"]);
|
|
||||||
if (Array.isArray(id)) id = null;
|
|
||||||
}
|
|
||||||
return config.get(oldOpts[key], id);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = expression.evaluate(snapshot.config[key], context, now, cb);
|
|
||||||
return Array.isArray(v) ? null : v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfigExpression(revision: string, key: string): Expression {
|
export function getConfigExpression(revision: string, key: string): Expression {
|
||||||
|
|||||||
@@ -183,4 +183,12 @@ export default class VersionedMap<K, V> {
|
|||||||
this.map.delete(k);
|
this.map.delete(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public *[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||||
|
for (const [key, revisions] of this.map) {
|
||||||
|
const last = revisions[revisions.length - 1];
|
||||||
|
if (last === NONEXISTENT) continue;
|
||||||
|
yield [key, last as V];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Generated
-24
@@ -22,7 +22,6 @@
|
|||||||
"koa-jwt": "^4.0.3",
|
"koa-jwt": "^4.0.3",
|
||||||
"koa-send": "^5.0.1",
|
"koa-send": "^5.0.1",
|
||||||
"mongodb": "^4.16.0",
|
"mongodb": "^4.16.0",
|
||||||
"parsimmon": "^1.18.1",
|
|
||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -46,7 +45,6 @@
|
|||||||
"@types/koa-compress": "^4.0.7",
|
"@types/koa-compress": "^4.0.7",
|
||||||
"@types/mithril": "^2.2.7",
|
"@types/mithril": "^2.2.7",
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.3.5",
|
||||||
"@types/parsimmon": "^1.10.9",
|
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.0.3",
|
||||||
@@ -3028,12 +3026,6 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/parsimmon": {
|
|
||||||
"version": "1.10.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.9.tgz",
|
|
||||||
"integrity": "sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
||||||
@@ -5323,11 +5315,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parsimmon": {
|
|
||||||
"version": "1.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
|
|
||||||
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
|
|
||||||
},
|
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -8317,12 +8304,6 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/parsimmon": {
|
|
||||||
"version": "1.10.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.9.tgz",
|
|
||||||
"integrity": "sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
||||||
@@ -9871,11 +9852,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||||
},
|
},
|
||||||
"parsimmon": {
|
|
||||||
"version": "1.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
|
|
||||||
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
|
|
||||||
},
|
|
||||||
"path-exists": {
|
"path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"koa-jwt": "^4.0.3",
|
"koa-jwt": "^4.0.3",
|
||||||
"koa-send": "^5.0.1",
|
"koa-send": "^5.0.1",
|
||||||
"mongodb": "^4.16.0",
|
"mongodb": "^4.16.0",
|
||||||
"parsimmon": "^1.18.1",
|
|
||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
"@types/koa-compress": "^4.0.7",
|
"@types/koa-compress": "^4.0.7",
|
||||||
"@types/mithril": "^2.2.7",
|
"@types/mithril": "^2.2.7",
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.3.5",
|
||||||
"@types/parsimmon": "^1.10.9",
|
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.0.3",
|
||||||
|
|||||||
+6
-9
@@ -2,13 +2,13 @@ import test from "node:test";
|
|||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { EJSON } from "bson";
|
import { EJSON } from "bson";
|
||||||
import { Filter } from "mongodb";
|
import { Filter } from "mongodb";
|
||||||
import { stringify, parse } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { convertOldPrecondition } from "../lib/db/util.ts";
|
import { convertOldPrecondition } from "../lib/db/util.ts";
|
||||||
import { toMongoQuery } from "../lib/db/synth.ts";
|
import { toMongoQuery } from "../lib/db/synth.ts";
|
||||||
|
|
||||||
void test("convertOldPrecondition", () => {
|
void test("convertOldPrecondition", () => {
|
||||||
const tests = [
|
const tests = [
|
||||||
[{}, "true"],
|
[{}, "TRUE"],
|
||||||
[{ test: "test" }, 'test = "test"'],
|
[{ test: "test" }, 'test = "test"'],
|
||||||
[{ test: { $eq: "test" } }, 'test = "test"'],
|
[{ test: { $eq: "test" } }, 'test = "test"'],
|
||||||
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
|
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
|
||||||
@@ -31,7 +31,7 @@ void test("convertOldPrecondition", () => {
|
|||||||
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
||||||
[
|
[
|
||||||
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||||
'test = "test" OR (test <> "test" OR test IS NULL)',
|
'test = "test" OR test <> "test" OR test IS NULL',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ test: { $gte: "test1", $ne: "test2" } },
|
{ test: { $gte: "test1", $ne: "test2" } },
|
||||||
@@ -54,7 +54,7 @@ void test("convertOldPrecondition", () => {
|
|||||||
|
|
||||||
for (const t of tests) {
|
for (const t of tests) {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)),
|
convertOldPrecondition(t[0] as Record<string, unknown>).toString(),
|
||||||
t[1],
|
t[1],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,6 @@ void test("toMongoQuery", async () => {
|
|||||||
"Param1 <> 1657844103524",
|
"Param1 <> 1657844103524",
|
||||||
{
|
{
|
||||||
"Param1._value": { $ne: 1657844103524 },
|
"Param1._value": { $ne: 1657844103524 },
|
||||||
|
|
||||||
$and: [
|
$and: [
|
||||||
{
|
{
|
||||||
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
|
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
|
||||||
@@ -114,7 +113,6 @@ void test("toMongoQuery", async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
"LOWER(Param1) LIKE 'value'",
|
"LOWER(Param1) LIKE 'value'",
|
||||||
{
|
{
|
||||||
@@ -146,7 +144,7 @@ void test("toMongoQuery", async () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const [expStr, expect] of queries) {
|
for (const [expStr, expect] of queries) {
|
||||||
const exp = parse(expStr);
|
const exp = Expression.parse(expStr);
|
||||||
let query = toMongoQuery(exp, "devices");
|
let query = toMongoQuery(exp, "devices");
|
||||||
if (query) query = EJSON.serialize(query);
|
if (query) query = EJSON.serialize(query);
|
||||||
assert.deepStrictEqual(query, expect);
|
assert.deepStrictEqual(query, expect);
|
||||||
@@ -156,11 +154,10 @@ void test("toMongoQuery", async () => {
|
|||||||
["Param1 = Param2", "Right-hand operand must be a literal value"],
|
["Param1 = Param2", "Right-hand operand must be a literal value"],
|
||||||
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
|
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
|
||||||
["NOW() = 1", "Left-hand operand must be a parameter"],
|
["NOW() = 1", "Left-hand operand must be a parameter"],
|
||||||
["param{param2} = 1", "Left-hand operand must be a parameter"],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [expStr, err] of failQueries) {
|
for (const [expStr, err] of failQueries) {
|
||||||
const exp = parse(expStr);
|
const exp = Expression.parse(expStr);
|
||||||
assert.throws(() => toMongoQuery(exp, "devices"), {
|
assert.throws(() => toMongoQuery(exp, "devices"), {
|
||||||
message: err,
|
message: err,
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-19
@@ -7,7 +7,7 @@ import VersionedMap from "../lib/versioned-map.ts";
|
|||||||
import { Attributes } from "../lib/types.ts";
|
import { Attributes } from "../lib/types.ts";
|
||||||
|
|
||||||
void test("getAliasDeclarations", () => {
|
void test("getAliasDeclarations", () => {
|
||||||
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b");
|
const path = Path.parse("a.[aa = 10 AND bb.[aaa = 100].cc = 1].b");
|
||||||
const decs = device.getAliasDeclarations(path, 99);
|
const decs = device.getAliasDeclarations(path, 99);
|
||||||
|
|
||||||
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
|
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
|
||||||
@@ -32,66 +32,69 @@ void test("unpack", () => {
|
|||||||
changes: new Set<string>(),
|
changes: new Set<string>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
device.set(deviceData, Path.parse("a.1.b"), now, {
|
device.set(deviceData, "a.1.b", now, {
|
||||||
value: [now, ["b", "xsd:string"]],
|
value: [now, ["b", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.c"), now, {
|
device.set(deviceData, "a.1.c", now, {
|
||||||
value: [now, ["c", "xsd:string"]],
|
value: [now, ["c", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.a.1.a"), now, {
|
device.set(deviceData, "a.1.a.1.a", now, {
|
||||||
value: [now, ["", "xsd:string"]],
|
value: [now, ["", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.a.1.b"), now, {
|
device.set(deviceData, "a.1.a.1.b", now, {
|
||||||
value: [now, ["b1", "xsd:string"]],
|
value: [now, ["b1", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.a.1.c"), now, {
|
device.set(deviceData, "a.1.a.1.c", now, {
|
||||||
value: [now, ["c1", "xsd:string"]],
|
value: [now, ["c1", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.a.2.a"), now, {
|
device.set(deviceData, "a.1.a.2.a", now, {
|
||||||
value: [now, ["", "xsd:string"]],
|
value: [now, ["", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.a.2.b"), now, {
|
device.set(deviceData, "a.1.a.2.b", now, {
|
||||||
value: [now, ["b2", "xsd:string"]],
|
value: [now, ["b2", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.1.a.2.c"), now, {
|
device.set(deviceData, "a.1.a.2.c", now, {
|
||||||
value: [now, ["c2", "xsd:string"]],
|
value: [now, ["c2", "xsd:string"]],
|
||||||
});
|
});
|
||||||
|
|
||||||
device.set(deviceData, Path.parse("a.2.b"), now, {
|
device.set(deviceData, "a.2.b", now, {
|
||||||
value: [now, ["b", "xsd:string"]],
|
value: [now, ["b", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.c"), now, {
|
device.set(deviceData, "a.2.c", now, {
|
||||||
value: [now, ["c", "xsd:string"]],
|
value: [now, ["c", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.a.1.a"), now, {
|
device.set(deviceData, "a.2.a.1.a", now, {
|
||||||
value: [now, ["", "xsd:string"]],
|
value: [now, ["", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.a.1.b"), now, {
|
device.set(deviceData, "a.2.a.1.b", now, {
|
||||||
value: [now, ["b1", "xsd:string"]],
|
value: [now, ["b1", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.a.1.c"), now, {
|
device.set(deviceData, "a.2.a.1.c", now, {
|
||||||
value: [now, ["c1", "xsd:string"]],
|
value: [now, ["c1", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.a.2.a"), now, {
|
device.set(deviceData, "a.2.a.2.a", now, {
|
||||||
value: [now, ["", "xsd:string"]],
|
value: [now, ["", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.a.2.b"), now, {
|
device.set(deviceData, "a.2.a.2.b", now, {
|
||||||
value: [now, ["c1", "xsd:string"]],
|
value: [now, ["c1", "xsd:string"]],
|
||||||
});
|
});
|
||||||
device.set(deviceData, Path.parse("a.2.a.2.c"), now, {
|
device.set(deviceData, "a.2.a.2.c", now, {
|
||||||
value: [now, ["b1", "xsd:string"]],
|
value: [now, ["b1", "xsd:string"]],
|
||||||
});
|
});
|
||||||
|
|
||||||
let unpacked: Path[];
|
let unpacked: Path[];
|
||||||
unpacked = device.unpack(
|
unpacked = device.unpack(
|
||||||
deviceData,
|
deviceData,
|
||||||
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a"),
|
Path.parse("a.[b='b' AND c='c'].a.[b='b1' AND c='c1'].a"),
|
||||||
);
|
);
|
||||||
assert.strictEqual(unpacked.length, 2);
|
assert.strictEqual(unpacked.length, 2);
|
||||||
assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a");
|
assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a");
|
||||||
assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a");
|
assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a");
|
||||||
|
|
||||||
unpacked = device.unpack(deviceData, Path.parse("a.*.a.[b:c1,c:b1].a"));
|
unpacked = device.unpack(
|
||||||
|
deviceData,
|
||||||
|
Path.parse("a.*.a.[b='c1' AND c='b1'].a"),
|
||||||
|
);
|
||||||
assert.strictEqual(unpacked.length, 1);
|
assert.strictEqual(unpacked.length, 1);
|
||||||
assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a");
|
assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a");
|
||||||
});
|
});
|
||||||
+10
-10
@@ -1,13 +1,12 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import initSqlJs from "sql.js/dist/sql-asm.js";
|
import initSqlJs from "sql.js/dist/sql-asm.js";
|
||||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
|
||||||
import {
|
import {
|
||||||
bookmarkToExpression,
|
bookmarkToExpression,
|
||||||
paginate,
|
paginate,
|
||||||
toBookmark,
|
toBookmark,
|
||||||
} from "../lib/common/expression/pagination.ts";
|
} from "../lib/common/expression/pagination.ts";
|
||||||
import { Expression } from "../lib/types.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { covers, minimize } from "../lib/common/expression/synth.ts";
|
import { covers, minimize } from "../lib/common/expression/synth.ts";
|
||||||
|
|
||||||
const VALUES = [null, -1, false, "a"];
|
const VALUES = [null, -1, false, "a"];
|
||||||
@@ -79,15 +78,15 @@ async function testPaginate(
|
|||||||
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
|
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
const allMatches = await query(`${stringify(q2)} ORDER BY ${orderBy}`);
|
const allMatches = await query(`${q2.toString()} ORDER BY ${orderBy}`);
|
||||||
const [fulfilled, diff] = paginate(q1, q2, sort);
|
const [fulfilled, diff] = paginate(q1, q2, sort);
|
||||||
assert.ok(covers(q1, fulfilled));
|
assert.ok(covers(q1, fulfilled));
|
||||||
|
|
||||||
const fulfilledMatches = await query(
|
const fulfilledMatches = await query(
|
||||||
`${stringify(fulfilled)} ORDER BY ${orderBy}`,
|
`${fulfilled.toString()} ORDER BY ${orderBy}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const diffMatches = await query(`${stringify(diff)} ORDER BY ${orderBy}`);
|
const diffMatches = await query(`${diff.toString()} ORDER BY ${orderBy}`);
|
||||||
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
|
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
|
||||||
|
|
||||||
if (allMatches.length === fulfilledMatches.length) return;
|
if (allMatches.length === fulfilledMatches.length) return;
|
||||||
@@ -98,14 +97,14 @@ async function testPaginate(
|
|||||||
);
|
);
|
||||||
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
|
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
|
||||||
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
|
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
|
||||||
const capped = ["AND", q2, bookmarkFilter];
|
const capped = Expression.and(q2, bookmarkFilter);
|
||||||
|
|
||||||
assert.ok(!covers(q1, capped));
|
assert.ok(!covers(q1, capped));
|
||||||
const cappedMatches = await query(
|
const cappedMatches = await query(
|
||||||
`(${stringify(capped)}) ORDER BY ${orderBy}`,
|
`(${capped.toString()}) ORDER BY ${orderBy}`,
|
||||||
);
|
);
|
||||||
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
|
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
|
||||||
const min = minimize(["OR", q1, capped]);
|
const min = minimize(Expression.or(q1, capped));
|
||||||
await testPaginate(min, q2, sort);
|
await testPaginate(min, q2, sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +114,7 @@ void test("paginate", async () => {
|
|||||||
const sortOrders = getAllSortOrders(params);
|
const sortOrders = getAllSortOrders(params);
|
||||||
|
|
||||||
for (const [q1, q2] of cases)
|
for (const [q1, q2] of cases)
|
||||||
for (const sort of sortOrders)
|
for (const sort of sortOrders) {
|
||||||
await testPaginate(parse(q1), parse(q2), sort);
|
await testPaginate(Expression.parse(q1), Expression.parse(q2), sort);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
+24
-28
@@ -3,67 +3,63 @@ import assert from "node:assert";
|
|||||||
import Path from "../lib/common/path.ts";
|
import Path from "../lib/common/path.ts";
|
||||||
import PathSet from "../lib/common/path-set.ts";
|
import PathSet from "../lib/common/path-set.ts";
|
||||||
|
|
||||||
void test("depth", () => {
|
|
||||||
const pathSet = new PathSet();
|
|
||||||
assert.strictEqual(pathSet.depth, 0);
|
|
||||||
pathSet.add(Path.parse(""));
|
|
||||||
assert.strictEqual(pathSet.depth, 1);
|
|
||||||
pathSet.add(Path.parse("a"));
|
|
||||||
assert.strictEqual(pathSet.depth, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
void test("add", () => {
|
void test("add", () => {
|
||||||
const pathSet = new PathSet();
|
const pathSet = new PathSet();
|
||||||
pathSet.add(Path.parse("a"));
|
pathSet.add("a");
|
||||||
pathSet.add(Path.parse("a"));
|
pathSet.add("a");
|
||||||
assert.strictEqual(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
|
assert.strictEqual(
|
||||||
|
pathSet.findCompat(Path.parse("a"), true, true, 99).length,
|
||||||
|
1,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("get", () => {
|
void test("get", () => {
|
||||||
const pathSet = new PathSet();
|
const pathSet = new PathSet();
|
||||||
pathSet.add(Path.parse("a.*"));
|
pathSet.add("a.*");
|
||||||
pathSet.add(Path.parse("a.a"));
|
pathSet.add("a.a");
|
||||||
pathSet.add(Path.parse("*.*"));
|
pathSet.add("*.*");
|
||||||
|
|
||||||
assert.strictEqual(pathSet.get(Path.parse("a.*")).toString(), "a.*");
|
assert.strictEqual(pathSet.get("a.*").toString(), "a.*");
|
||||||
assert.strictEqual(pathSet.get(Path.parse("*.a")), null);
|
assert.equal(pathSet.get("*.a"), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("find", () => {
|
void test("find", () => {
|
||||||
const pathSet = new PathSet();
|
const pathSet = new PathSet();
|
||||||
pathSet.add(Path.parse("a"));
|
pathSet.add("a");
|
||||||
pathSet.add(Path.parse("a.*"));
|
pathSet.add("a.*");
|
||||||
pathSet.add(Path.parse("a.a"));
|
pathSet.add("a.a");
|
||||||
pathSet.add(Path.parse("*.a"));
|
pathSet.add("*.a");
|
||||||
pathSet.add(Path.parse("*.*"));
|
pathSet.add("*.*");
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pathSet.find(Path.parse(""), true, true, 1).map((p) => p.toString()),
|
pathSet.findCompat(Path.root, true, true, 1).map((p) => p.toString()),
|
||||||
["a"],
|
["a"],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pathSet.find(Path.parse(""), false, false, 2).map((p) => p.toString()),
|
pathSet.findCompat(Path.root, false, false, 2).map((p) => p.toString()),
|
||||||
["a", "a.*", "a.a", "*.a", "*.*"],
|
["a", "a.*", "a.a", "*.a", "*.*"],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pathSet.find(Path.parse("a.*"), false, false).map((p) => p.toString()),
|
pathSet
|
||||||
|
.findCompat(Path.parse("a.*"), false, false)
|
||||||
|
.map((p) => p.toString()),
|
||||||
["a.*"],
|
["a.*"],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pathSet.find(Path.parse("a.*"), false, true).map((p) => p.toString()),
|
pathSet.findCompat(Path.parse("a.*"), false, true).map((p) => p.toString()),
|
||||||
["a.*", "a.a"],
|
["a.*", "a.a"],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pathSet.find(Path.parse("a.*"), true, false).map((p) => p.toString()),
|
pathSet.findCompat(Path.parse("a.*"), true, false).map((p) => p.toString()),
|
||||||
["a.*", "*.*"],
|
["a.*", "*.*"],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
pathSet.find(Path.parse("a.*"), true, true).map((p) => p.toString()),
|
pathSet.findCompat(Path.parse("a.*"), true, true).map((p) => p.toString()),
|
||||||
["a.*", "a.a", "*.a", "*.*"],
|
["a.*", "a.a", "*.a", "*.*"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
+138
-15
@@ -8,38 +8,161 @@ void test("parse", () => {
|
|||||||
assert.throws(() => Path.parse(".a"));
|
assert.throws(() => Path.parse(".a"));
|
||||||
assert.throws(() => Path.parse("a."));
|
assert.throws(() => Path.parse("a."));
|
||||||
assert.throws(() => Path.parse("a..b"));
|
assert.throws(() => Path.parse("a..b"));
|
||||||
assert.throws(() => Path.parse("b*"));
|
assert.doesNotThrow(() => Path.parse("b*"));
|
||||||
assert.throws(() => Path.parse("*b"));
|
assert.doesNotThrow(() => Path.parse("*b"));
|
||||||
assert.throws(() => Path.parse("a.b c.d"));
|
assert.throws(() => Path.parse("a.b c.d"));
|
||||||
assert.throws(() => Path.parse("a["));
|
assert.throws(() => Path.parse("a["));
|
||||||
assert.throws(() => Path.parse("a[b"));
|
assert.throws(() => Path.parse("a[b"));
|
||||||
assert.throws(() => Path.parse("a[b:"));
|
assert.throws(() => Path.parse("a[b:"));
|
||||||
assert.throws(() => Path.parse('a[b:"waef]'));
|
assert.throws(() => Path.parse('a[b:"waef]'));
|
||||||
assert.doesNotThrow(() => Path.parse("[]"));
|
|
||||||
assert.doesNotThrow(() => Path.parse("[a:]"));
|
|
||||||
assert.doesNotThrow(() => Path.parse("*"));
|
assert.doesNotThrow(() => Path.parse("*"));
|
||||||
assert.doesNotThrow(() => Path.parse(""));
|
assert.throws(() => Path.parse(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("toString", () => {
|
void test("toString", () => {
|
||||||
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
const path1 = Path.parse('abc.[ abc=123 and def=" abc " ].123');
|
||||||
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
const path2 = Path.parse('abc.[abc = 123 AND def = " abc "].123');
|
||||||
assert.strictEqual(path1.toString(), path2.toString());
|
assert.strictEqual(path1.toString(), path2.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("slice", () => {
|
void test("slice", () => {
|
||||||
const path = Path.parse("a.*.b.[x:y].c");
|
const path = Path.parse(`a.*.b.[x = "y"].c`);
|
||||||
const sliced = path.slice(1, -1);
|
const sliced = path.slice(1, -1);
|
||||||
assert.strictEqual(sliced.toString(), '*.b.[x:"y"]');
|
assert.strictEqual(sliced.toString(), '*.b.[x = "y"]');
|
||||||
assert.strictEqual(sliced.alias, 0b100);
|
assert.strictEqual(sliced.alias, 0b100);
|
||||||
assert.strictEqual(sliced.wildcard, 0b1);
|
assert.strictEqual(sliced.wildcard, 0b1);
|
||||||
|
|
||||||
|
const path2 = Path.parse("a.b:c.d");
|
||||||
|
|
||||||
|
// Trim from right into colon region
|
||||||
|
assert.strictEqual(path2.slice(0, 3).toString(), "a.b:c");
|
||||||
|
assert.strictEqual(path2.slice(0, 3).colon, 1);
|
||||||
|
|
||||||
|
// Trim from right past colon region
|
||||||
|
assert.strictEqual(path2.slice(0, 2).toString(), "a.b");
|
||||||
|
assert.strictEqual(path2.slice(0, 2).colon, 0);
|
||||||
|
|
||||||
|
// Start exactly at colon boundary (all-colon result)
|
||||||
|
assert.strictEqual(path2.slice(2, 4).toString(), ":c.d");
|
||||||
|
assert.strictEqual(path2.slice(2, 4).colon, 2);
|
||||||
|
|
||||||
|
// Start past colon boundary (colon dropped)
|
||||||
|
assert.strictEqual(path2.slice(3, 4).toString(), "d");
|
||||||
|
assert.strictEqual(path2.slice(3, 4).colon, 0);
|
||||||
|
|
||||||
|
// Span across boundary from both sides
|
||||||
|
assert.strictEqual(path2.slice(1, 3).toString(), "b:c");
|
||||||
|
assert.strictEqual(path2.slice(1, 3).colon, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("concat", () => {
|
void test("concat", () => {
|
||||||
const path1 = Path.parse("a");
|
// Alias and wildcard propagation
|
||||||
const path2 = Path.parse("*.[a:b]");
|
const c0 = Path.parse("a").concat(Path.parse('*.[a = "b"]'));
|
||||||
const concat = path1.concat(path2);
|
assert.strictEqual(c0.toString(), 'a.*.[a = "b"]');
|
||||||
assert.strictEqual(concat.toString(), 'a.*.[a:"b"]');
|
assert.strictEqual(c0.alias, 0b100);
|
||||||
assert.strictEqual(concat.alias, 0b100);
|
assert.strictEqual(c0.wildcard, 0b10);
|
||||||
assert.strictEqual(concat.wildcard, 0b10);
|
|
||||||
|
// Left plain, right all-colon
|
||||||
|
const allColon = Path.parse("a:b.c").slice(1, 3);
|
||||||
|
const c1 = Path.parse("a.b").concat(allColon);
|
||||||
|
assert.strictEqual(c1.toString(), "a.b:b.c");
|
||||||
|
assert.strictEqual(c1.colon, 2);
|
||||||
|
|
||||||
|
// Left colon, right all-colon
|
||||||
|
const c2 = Path.parse("a:b").concat(Path.parse("a.b:c.d").slice(2, 4));
|
||||||
|
assert.strictEqual(c2.toString(), "a:b.c.d");
|
||||||
|
assert.strictEqual(c2.colon, 3);
|
||||||
|
|
||||||
|
// Both have mixed colon — should throw
|
||||||
|
assert.throws(() => Path.parse("a:b").concat(Path.parse("c:d")));
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("old alias format", () => {
|
||||||
|
// Empty brackets
|
||||||
|
const empty = Path.parse("a.[].b");
|
||||||
|
assert.strictEqual(empty.alias, 0b10);
|
||||||
|
assert.strictEqual(empty.toString(), "a.[TRUE].b");
|
||||||
|
|
||||||
|
// Single key-value
|
||||||
|
const single = Path.parse("a.[b:c].d");
|
||||||
|
assert.strictEqual(single.alias, 0b10);
|
||||||
|
assert.strictEqual(single.toString(), 'a.[b = "c"].d');
|
||||||
|
|
||||||
|
// Multiple key-value pairs
|
||||||
|
const multi = Path.parse("a.[b:1,c:2].d");
|
||||||
|
assert.strictEqual(multi.alias, 0b10);
|
||||||
|
assert.strictEqual(multi.toString(), 'a.[b = "1" AND c = "2"].d');
|
||||||
|
|
||||||
|
// Key with empty value
|
||||||
|
const emptyVal = Path.parse("a.[b:].d");
|
||||||
|
assert.strictEqual(emptyVal.toString(), 'a.[b = ""].d');
|
||||||
|
|
||||||
|
// Value containing colons (split on first : only)
|
||||||
|
const colonVal = Path.parse("a.[b:c:d].e");
|
||||||
|
assert.strictEqual(colonVal.toString(), 'a.[b = "c:d"].e');
|
||||||
|
|
||||||
|
// Value containing spaces (trimmed)
|
||||||
|
const spaceVal = Path.parse("a.[b:hello world].c");
|
||||||
|
assert.strictEqual(spaceVal.toString(), 'a.[b = "hello world"].c');
|
||||||
|
|
||||||
|
// Unquoted values are trimmed
|
||||||
|
const trimmed = Path.parse("a.[b: hello ].c");
|
||||||
|
assert.strictEqual(trimmed.toString(), 'a.[b = "hello"].c');
|
||||||
|
|
||||||
|
// Whitespace around keys is trimmed
|
||||||
|
const keySpace = Path.parse("a.[ b : c ].d");
|
||||||
|
assert.strictEqual(keySpace.toString(), 'a.[b = "c"].d');
|
||||||
|
|
||||||
|
// Equivalence with new format
|
||||||
|
const oldFmt = Path.parse("x.[a:1,b:2].y");
|
||||||
|
const newFmt = Path.parse('x.[a = "1" AND b = "2"].y');
|
||||||
|
assert.strictEqual(oldFmt.toString(), newFmt.toString());
|
||||||
|
|
||||||
|
// Nested old-format alias
|
||||||
|
const nested = Path.parse("a.[b.[x:1].c:2].d");
|
||||||
|
assert.strictEqual(nested.toString(), 'a.[b.[x = "1"].c = "2"].d');
|
||||||
|
|
||||||
|
// New SQL format still works
|
||||||
|
const sql = Path.parse("a.[b = 1 AND c = 2].d");
|
||||||
|
assert.strictEqual(sql.toString(), "a.[b = 1 AND c = 2].d");
|
||||||
|
|
||||||
|
// Double-quoted value containing closing bracket
|
||||||
|
const quotedBracket = Path.parse('a.[b:"hello]world"].c');
|
||||||
|
assert.strictEqual(quotedBracket.toString(), 'a.[b = "hello]world"].c');
|
||||||
|
|
||||||
|
// Double-quoted value containing comma
|
||||||
|
const quotedComma = Path.parse('a.[b:"hello,world"].c');
|
||||||
|
assert.strictEqual(quotedComma.toString(), 'a.[b = "hello,world"].c');
|
||||||
|
|
||||||
|
// Double-quoted value with escape sequences (JSON semantics)
|
||||||
|
const escaped = Path.parse('a.[b:"hello\\"world"].c');
|
||||||
|
assert.strictEqual(escaped.toString(), 'a.[b = "hello\\"world"].c');
|
||||||
|
|
||||||
|
// Single-quoted value
|
||||||
|
const singleQuoted = Path.parse("a.[b:'value'].c");
|
||||||
|
assert.strictEqual(singleQuoted.toString(), 'a.[b = "value"].c');
|
||||||
|
|
||||||
|
// Invalid old format (no colon) still throws
|
||||||
|
assert.throws(() => Path.parse("[abc]"));
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("slice concat round-trip", () => {
|
||||||
|
const path = Path.parse("a.b:c.d");
|
||||||
|
|
||||||
|
// Round-trips at every split position up to and including the boundary
|
||||||
|
for (let i = 1; i <= path.paramLength; i++) {
|
||||||
|
const rejoined = path.slice(0, i).concat(path.slice(i));
|
||||||
|
assert.strictEqual(rejoined.toString(), path.toString());
|
||||||
|
assert.strictEqual(rejoined.colon, path.colon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split inside the colon region: right half loses colon,
|
||||||
|
// but concat still extends the left's attr region
|
||||||
|
const left = path.slice(0, 3);
|
||||||
|
const right = path.slice(3);
|
||||||
|
assert.strictEqual(left.colon, 1);
|
||||||
|
assert.strictEqual(right.colon, 0);
|
||||||
|
const rejoined = left.concat(right);
|
||||||
|
assert.strictEqual(rejoined.toString(), "a.b:c.d");
|
||||||
|
assert.strictEqual(rejoined.colon, 2);
|
||||||
});
|
});
|
||||||
+34
-14
@@ -2,7 +2,7 @@ import test from "node:test";
|
|||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import initSqlJs from "sql.js/dist/sql-asm.js";
|
import initSqlJs from "sql.js/dist/sql-asm.js";
|
||||||
import { covers, minimize, unionDiff } from "../lib/common/expression/synth.ts";
|
import { covers, minimize, unionDiff } from "../lib/common/expression/synth.ts";
|
||||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
|
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
|
||||||
const DECIMAL_VALUES = [null, 0, -10, 10];
|
const DECIMAL_VALUES = [null, 0, -10, 10];
|
||||||
@@ -86,7 +86,7 @@ void test("minimize", async () => {
|
|||||||
|
|
||||||
for (const c of cases) {
|
for (const c of cases) {
|
||||||
const res1 = await query(c);
|
const res1 = await query(c);
|
||||||
const min = stringify(minimize(parse(c), true));
|
const min = minimize(Expression.parse(c), true).toString();
|
||||||
const res2 = await query(min);
|
const res2 = await query(min);
|
||||||
assert.strictEqual(setsEqual(res1, res2), true);
|
assert.strictEqual(setsEqual(res1, res2), true);
|
||||||
}
|
}
|
||||||
@@ -104,9 +104,9 @@ void test("unionDiff", async () => {
|
|||||||
for (const [c1, c2] of getPermutations(cases, cases)) {
|
for (const [c1, c2] of getPermutations(cases, cases)) {
|
||||||
const res1 = await query(c1);
|
const res1 = await query(c1);
|
||||||
const res2 = await query(c2);
|
const res2 = await query(c2);
|
||||||
const [union, diff] = unionDiff(parse(c1), parse(c2));
|
const [union, diff] = unionDiff(Expression.parse(c1), Expression.parse(c2));
|
||||||
const res3 = await query(stringify(union));
|
const res3 = await query(union.toString());
|
||||||
const res4 = await query(stringify(diff));
|
const res4 = await query(diff.toString());
|
||||||
|
|
||||||
const unionSet = new Set([...res1, ...res2]);
|
const unionSet = new Set([...res1, ...res2]);
|
||||||
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
|
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
|
||||||
@@ -117,15 +117,35 @@ void test("unionDiff", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void test("covers", async () => {
|
void test("covers", async () => {
|
||||||
assert.strictEqual(covers(false, false), true);
|
|
||||||
assert.strictEqual(covers(false, parse("decimal > 5 AND decimal < 3")), true);
|
|
||||||
assert.strictEqual(covers(parse("true"), parse("decimal > 0")), true);
|
|
||||||
assert.strictEqual(covers(parse("true"), parse("false")), true);
|
|
||||||
assert.strictEqual(covers(parse("false"), parse("false")), true);
|
|
||||||
assert.strictEqual(covers(parse("false"), parse("decimal > 0")), false);
|
|
||||||
assert.strictEqual(covers(parse("decimal >= 0"), parse("decimal > 0")), true);
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
covers(parse("decimal > 0"), parse("decimal >= 0")),
|
covers(Expression.parse("false"), Expression.parse("false")),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
covers(
|
||||||
|
Expression.parse("false"),
|
||||||
|
Expression.parse("decimal > 5 AND decimal < 3"),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
covers(Expression.parse("true"), Expression.parse("decimal > 0")),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
covers(Expression.parse("true"), Expression.parse("false")),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
covers(Expression.parse("false"), Expression.parse("decimal > 0")),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
covers(Expression.parse("decimal >= 0"), Expression.parse("decimal > 0")),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
covers(Expression.parse("decimal > 0"), Expression.parse("decimal >= 0")),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -139,7 +159,7 @@ void test("covers", async () => {
|
|||||||
for (const [c1, c2] of cases) {
|
for (const [c1, c2] of cases) {
|
||||||
const res1 = await query(c1);
|
const res1 = await query(c1);
|
||||||
const res2 = await query(c2);
|
const res2 = await query(c2);
|
||||||
const coversResult = covers(parse(c1), parse(c2));
|
const coversResult = covers(Expression.parse(c1), Expression.parse(c2));
|
||||||
const actuallyCovers = Array.from(res2).every((r) => res1.has(r));
|
const actuallyCovers = Array.from(res2).every((r) => res1.has(r));
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
|
|||||||
@@ -20,19 +20,37 @@ import { contextifyComponent } from "./components.ts";
|
|||||||
import { PermissionSet, UiConfig } from "../lib/types.ts";
|
import { PermissionSet, UiConfig } from "../lib/types.ts";
|
||||||
import drawerComponent from "./drawer-component.ts";
|
import drawerComponent from "./drawer-component.ts";
|
||||||
import { render as renderOverlay } from "./overlay.ts";
|
import { render as renderOverlay } from "./overlay.ts";
|
||||||
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
authorizer: Authorizer;
|
authorizer: Authorizer;
|
||||||
permissionSets: PermissionSet[];
|
permissionSets: {
|
||||||
|
[resource: string]: { access: number; validate: string; filter: string };
|
||||||
|
}[][];
|
||||||
username: string;
|
username: string;
|
||||||
clientConfig: { ui: UiConfig };
|
clientConfig: UiConfig;
|
||||||
configSnapshot: string;
|
configSnapshot: string;
|
||||||
genieacsVersion: string;
|
genieacsVersion: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.authorizer = new Authorizer(window.permissionSets);
|
const permissionSets: PermissionSet[] = window.permissionSets.map((p) =>
|
||||||
|
p.map((s) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(s).map(([resource, { access, validate, filter }]) => [
|
||||||
|
resource,
|
||||||
|
{
|
||||||
|
access,
|
||||||
|
validate: Expression.parse(validate),
|
||||||
|
filter: Expression.parse(filter),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.authorizer = new Authorizer(permissionSets);
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import { ClosureComponent } from "mithril";
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as taskQueue from "../task-queue.ts";
|
import * as taskQueue from "../task-queue.ts";
|
||||||
import { parse } from "../../lib/common/expression/parser.ts";
|
|
||||||
import memoize from "../../lib/common/memoize.ts";
|
import memoize from "../../lib/common/memoize.ts";
|
||||||
import { icon } from "../tailwind-utility-components.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||||
import debounce from "../../lib/common/debounce.ts";
|
import debounce from "../../lib/common/debounce.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import Expression, { Value } from "../../lib/common/expression.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
|
import Path from "../../lib/common/path.ts";
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
|
|
||||||
function escapeRegExp(str): string {
|
function escapeRegExp(str): string {
|
||||||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
const keysByDepth: WeakMap<Record<string, unknown>, string[][]> = new WeakMap();
|
interface Parameter {
|
||||||
|
path: Expression.Parameter;
|
||||||
|
value?: Value;
|
||||||
|
writable?: boolean;
|
||||||
|
object?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
const prepareParams = memoize((device: FlatDevice): Parameter[][] => {
|
||||||
if (keysByDepth.has(device)) return keysByDepth.get(device);
|
const map = new Map<string, Parameter>();
|
||||||
const res: string[][] = [];
|
|
||||||
for (const key of Object.keys(device)) {
|
for (const [k, v] of Object.entries(device)) {
|
||||||
|
const [param, attr] = k.split(":");
|
||||||
|
let attrs = map.get(param);
|
||||||
|
if (!attrs)
|
||||||
|
map.set(
|
||||||
|
param,
|
||||||
|
(attrs = { path: new Expression.Parameter(Path.parse(param)) }),
|
||||||
|
);
|
||||||
|
attrs[attr || "value"] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: Parameter[][] = [];
|
||||||
|
for (const [key, attrs] of map) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (
|
for (
|
||||||
let i = key.lastIndexOf(".", key.length - 2);
|
let i = key.lastIndexOf(".", key.length - 2);
|
||||||
@@ -29,11 +44,10 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
|||||||
)
|
)
|
||||||
++count;
|
++count;
|
||||||
while (res.length <= count) res.push([]);
|
while (res.length <= count) res.push([]);
|
||||||
res[count].push(key);
|
res[count].push(attrs);
|
||||||
}
|
}
|
||||||
keysByDepth.set(device, res);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
});
|
||||||
|
|
||||||
interface Attrs {
|
interface Attrs {
|
||||||
device: FlatDevice;
|
device: FlatDevice;
|
||||||
@@ -51,9 +65,13 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs.device;
|
const device = vnode.attrs.device;
|
||||||
|
const allParams = prepareParams(device);
|
||||||
|
|
||||||
const limit =
|
let limit = 100;
|
||||||
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
|
if (vnode.attrs.limit) {
|
||||||
|
const l = evaluateExpression(vnode.attrs.limit, device);
|
||||||
|
if (typeof l.value === "number") limit = l.value;
|
||||||
|
}
|
||||||
|
|
||||||
const search = m(
|
const search = m(
|
||||||
"input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2",
|
"input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2",
|
||||||
@@ -75,35 +93,36 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredKeys: string[] = [];
|
const filteredParams: Parameter[] = [];
|
||||||
const allKeys = orderKeysByDepth(device);
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const keys of allKeys) {
|
for (const keys of allParams) {
|
||||||
let c = 0;
|
let c = 0;
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
const p = device[k];
|
const str = k.value ? `${k.path.toString()} ${k.value}` : k;
|
||||||
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
|
|
||||||
if (re && !re.test(str)) continue;
|
if (re && !re.test(str)) continue;
|
||||||
++c;
|
++c;
|
||||||
if (count < limit) filteredKeys.push(k);
|
if (count < limit) filteredParams.push(k);
|
||||||
}
|
}
|
||||||
count += c;
|
count += c;
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredKeys.sort();
|
filteredParams.sort((a, b) => {
|
||||||
|
if (a.path < b.path) return -1;
|
||||||
|
if (a.path > b.path) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
const rows = filteredKeys.map((k) => {
|
const rows = filteredParams.map((p) => {
|
||||||
const p = device[k];
|
|
||||||
const val = [];
|
const val = [];
|
||||||
if (p.object === false) {
|
if (p.value) {
|
||||||
val.push(
|
val.push(
|
||||||
m(
|
m(
|
||||||
"parameter",
|
"parameter",
|
||||||
Object.assign({ device: device, parameter: memoizedParse(k) }),
|
Object.assign({ device: device, parameter: p.path }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (p.object && p.writable) {
|
} else if (p.object && p.writable) {
|
||||||
if (instanceRegex.test(k)) {
|
if (instanceRegex.test(p.path.toString())) {
|
||||||
val.push(
|
val.push(
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
@@ -112,8 +131,8 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "deleteObject",
|
name: "deleteObject",
|
||||||
device: device["DeviceID.ID"].value[0] as string,
|
device: device["DeviceID.ID"] as string,
|
||||||
objectName: k,
|
objectName: p.path.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -133,8 +152,8 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "addObject",
|
name: "addObject",
|
||||||
device: device["DeviceID.ID"].value[0] as string,
|
device: device["DeviceID.ID"] as string,
|
||||||
objectName: k,
|
objectName: p.path.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -156,8 +175,8 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "getParameterValues",
|
name: "getParameterValues",
|
||||||
device: device["DeviceID.ID"].value[0] as string,
|
device: device["DeviceID.ID"] as string,
|
||||||
parameterNames: [k],
|
parameterNames: [p.path.toString()],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -170,7 +189,10 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
|
|
||||||
return m(
|
return m(
|
||||||
"tr",
|
"tr",
|
||||||
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: k })),
|
m(
|
||||||
|
"td.pl-4 pr-2 py-2 truncate",
|
||||||
|
m("long-text", { text: p.path.toString() }),
|
||||||
|
),
|
||||||
m("td.pr-4 py-2 text-right flex justify-end", val),
|
m("td.pr-4 py-2 text-right flex justify-end", val),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -195,7 +217,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
m(
|
m(
|
||||||
"span.text-xs",
|
"span.text-xs",
|
||||||
"Displaying ",
|
"Displaying ",
|
||||||
m("span.font-medium", "" + filteredKeys.length),
|
m("span.font-medium", "" + filteredParams.length),
|
||||||
" out of ",
|
" out of ",
|
||||||
m("span.font-medium", "" + count),
|
m("span.font-medium", "" + count),
|
||||||
" parameters",
|
" parameters",
|
||||||
@@ -204,7 +226,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
|
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
|
||||||
{
|
{
|
||||||
href: `api/devices/${encodeURIComponent(
|
href: `api/devices/${encodeURIComponent(
|
||||||
device["DeviceID.ID"].value[0],
|
device["DeviceID.ID"],
|
||||||
)}.csv`,
|
)}.csv`,
|
||||||
download: "",
|
download: "",
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-15
@@ -1,7 +1,6 @@
|
|||||||
import { Attributes, ClosureComponent } from "mithril";
|
import { Attributes, ClosureComponent } from "mithril";
|
||||||
import { map } from "../../lib/common/expression/parser.ts";
|
|
||||||
import memoize from "../../lib/common/memoize.ts";
|
import memoize from "../../lib/common/memoize.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import Expression from "../../lib/common/expression.ts";
|
||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import { evaluateExpression, getTimestamp } from "../store.ts";
|
import { evaluateExpression, getTimestamp } from "../store.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
@@ -14,15 +13,16 @@ const evaluateAttributes = memoize(
|
|||||||
): Attributes => {
|
): Attributes => {
|
||||||
const res: Attributes = {};
|
const res: Attributes = {};
|
||||||
for (const [k, v] of Object.entries(attrs)) {
|
for (const [k, v] of Object.entries(attrs)) {
|
||||||
const vv = map(v, (e) => {
|
const vv = v.evaluate((e) => {
|
||||||
if (
|
if (e instanceof Expression.Literal) return e;
|
||||||
Array.isArray(e) &&
|
else if (e instanceof Expression.FunctionCall) {
|
||||||
e[0] === "FUNC" &&
|
if (e.name === "ENCODEURICOMPONENT") {
|
||||||
e[1] === "ENCODEURICOMPONENT"
|
const a = evaluateExpression(e.args[0], obj);
|
||||||
) {
|
if (a instanceof Expression.Literal) {
|
||||||
const a = evaluateExpression(e[2], obj);
|
if (a.value == null) return new Expression.Literal(null);
|
||||||
if (a == null) return null;
|
return new Expression.Literal(encodeURIComponent(a.value));
|
||||||
return encodeURIComponent(a as string);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
@@ -47,9 +47,10 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const children = Object.values(vnode.attrs.components).map((c) => {
|
const children = Object.values(vnode.attrs.components).map((c) => {
|
||||||
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
if (c instanceof Expression)
|
||||||
|
c = evaluateExpression(c, device || {}).value;
|
||||||
if (typeof c !== "object") return `${c}`;
|
if (typeof c !== "object") return `${c}`;
|
||||||
const type = evaluateExpression(c["type"], device || {});
|
const type = evaluateExpression(c["type"], device || {}).value;
|
||||||
if (!type) return null;
|
if (!type) return null;
|
||||||
return m(type as string, c);
|
return m(type as string, c);
|
||||||
});
|
});
|
||||||
@@ -59,8 +60,8 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
if (el == null) return children;
|
if (el == null) return children;
|
||||||
|
|
||||||
let attrs: Attributes;
|
let attrs: Attributes;
|
||||||
if (Array.isArray(el)) {
|
if (el instanceof Expression) {
|
||||||
el = evaluateExpression(el, device || {});
|
el = evaluateExpression(el, device || {}).value;
|
||||||
} else if (typeof el === "object") {
|
} else if (typeof el === "object") {
|
||||||
if (el["attributes"] != null) {
|
if (el["attributes"] != null) {
|
||||||
attrs = evaluateAttributes(
|
attrs = evaluateAttributes(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "reboot",
|
name: "reboot",
|
||||||
device: device["DeviceID.ID"].value[0],
|
device: device["DeviceID.ID"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "factoryReset",
|
name: "factoryReset",
|
||||||
device: device["DeviceID.ID"].value[0],
|
device: device["DeviceID.ID"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.stageDownload({
|
taskQueue.stageDownload({
|
||||||
name: "download",
|
name: "download",
|
||||||
devices: [device["DeviceID.ID"].value[0]],
|
devices: [device["DeviceID.ID"]],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
title: "Delete device",
|
title: "Delete device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
if (!confirm("Deleting this device. Are you sure?")) return;
|
if (!confirm("Deleting this device. Are you sure?")) return;
|
||||||
const deviceId = device["DeviceID.ID"].value[0];
|
const deviceId = device["DeviceID.ID"];
|
||||||
|
|
||||||
store
|
store
|
||||||
.deleteResource("devices", deviceId)
|
.deleteResource("devices", deviceId)
|
||||||
|
|||||||
@@ -3,17 +3,24 @@ import { m } from "../components.ts";
|
|||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import * as notifications from "../notifications.ts";
|
import * as notifications from "../notifications.ts";
|
||||||
import { stringify } from "../../lib/common/yaml.ts";
|
import { stringify } from "../../lib/common/yaml.ts";
|
||||||
|
import Expression from "../../lib/common/expression.ts";
|
||||||
|
import Path from "../../lib/common/path.ts";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
const deviceId = device["DeviceID.ID"].value[0];
|
const deviceId = device["DeviceID.ID"];
|
||||||
const faults = store.fetch("faults", [
|
const p = new Expression.Parameter(Path.parse("_id"));
|
||||||
"AND",
|
const exp = Expression.and(
|
||||||
[">", ["PARAM", "_id"], `${deviceId}:`],
|
new Expression.Binary(">", p, new Expression.Literal(`${deviceId}:`)),
|
||||||
["<", ["PARAM", "_id"], `${deviceId}:zzzz`],
|
new Expression.Binary(
|
||||||
]);
|
"<",
|
||||||
|
p,
|
||||||
|
new Expression.Literal(`${deviceId}:zzzz`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const faults = store.fetch("faults", exp);
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
"Channel",
|
"Channel",
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import { evaluateExpression } from "../store.ts";
|
import { evaluateExpression } from "../store.ts";
|
||||||
|
import Expression from "../../lib/common/expression.ts";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
let deviceId;
|
let deviceId;
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
if (device) deviceId = device["DeviceID.ID"].value[0];
|
if (device) deviceId = device["DeviceID.ID"];
|
||||||
|
|
||||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||||
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
if (c instanceof Expression)
|
||||||
|
c = evaluateExpression(c, device ?? {}).value;
|
||||||
if (typeof c !== "object") return `${c}`;
|
if (typeof c !== "object") return `${c}`;
|
||||||
const type = evaluateExpression(c["type"], device || {});
|
const type = evaluateExpression(c["type"], device ?? {}).value;
|
||||||
if (!type) return null;
|
if (!type) return null;
|
||||||
const attrs = Object.assign({}, vnode.attrs, c);
|
const attrs = Object.assign({}, vnode.attrs, c);
|
||||||
return m(type as string, attrs);
|
return m(type as string, attrs);
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import config from "../config.ts";
|
import { overview } from "../config.ts";
|
||||||
import { evaluateExpression } from "../store.ts";
|
import { evaluateExpression } from "../store.ts";
|
||||||
|
|
||||||
const CHARTS = config.ui.overview.charts;
|
const CHARTS = overview.charts;
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
const chartName = evaluateExpression(vnode.attrs["chart"], device || {});
|
const chartName = evaluateExpression(
|
||||||
const chart = CHARTS[chartName as string] as Record<string, unknown>;
|
vnode.attrs["chart"],
|
||||||
|
device ?? {},
|
||||||
|
).value;
|
||||||
|
const chart = CHARTS[chartName as string];
|
||||||
if (!chart) return null;
|
if (!chart) return null;
|
||||||
for (const slice of Object.values(chart.slices)) {
|
for (const slice of chart.slices) {
|
||||||
const filter = slice["filter"];
|
const filter = slice.filter;
|
||||||
if (evaluateExpression(filter, device || {})) {
|
if (evaluateExpression(filter, device ?? {}).value) {
|
||||||
const dot = m(
|
const dot = m(
|
||||||
"svg.inline",
|
"svg.inline",
|
||||||
{
|
{
|
||||||
@@ -28,10 +31,10 @@ const component: ClosureComponent = (): Component => {
|
|||||||
cx: "0.5em",
|
cx: "0.5em",
|
||||||
cy: "0.5em",
|
cy: "0.5em",
|
||||||
r: "0.4em",
|
r: "0.4em",
|
||||||
fill: evaluateExpression(slice["color"], null),
|
fill: slice.color,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return m("span", dot, evaluateExpression(slice["label"], null));
|
return m("span", dot, slice.label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ClosureComponent, VnodeDOM } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import Expression from "../../lib/common/expression.ts";
|
||||||
|
|
||||||
interface Attrs {
|
interface Attrs {
|
||||||
device: FlatDevice;
|
device: FlatDevice;
|
||||||
@@ -19,7 +19,11 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
const device = vnode.attrs.device;
|
const device = vnode.attrs.device;
|
||||||
|
|
||||||
const rows = Object.values(vnode.attrs.parameters).map((parameter) => {
|
const rows = Object.values(vnode.attrs.parameters).map((parameter) => {
|
||||||
const type = evaluateExpression(parameter.type, device);
|
let type = "parameter";
|
||||||
|
if (parameter.type) {
|
||||||
|
const t = evaluateExpression(parameter.type, device);
|
||||||
|
if (typeof t.value === "string") type = t.value;
|
||||||
|
}
|
||||||
const p = m.context(
|
const p = m.context(
|
||||||
{
|
{
|
||||||
device: device,
|
device: device,
|
||||||
@@ -45,7 +49,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
},
|
},
|
||||||
m(
|
m(
|
||||||
"dt.text-sm font-medium text-stone-500",
|
"dt.text-sm font-medium text-stone-500",
|
||||||
evaluateExpression(parameter["label"], device),
|
evaluateExpression(parameter["label"], device).value,
|
||||||
),
|
),
|
||||||
m("dd.text-sm text-stone-900 col-span-2", p),
|
m("dd.text-sm text-stone-900 col-span-2", p),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,47 +2,45 @@ import { ClosureComponent } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as taskQueue from "../task-queue.ts";
|
import * as taskQueue from "../task-queue.ts";
|
||||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||||
import * as expressionParser from "../../lib/common/expression/parser.ts";
|
|
||||||
import { icon } from "../tailwind-utility-components.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import Expression from "../../lib/common/expression.ts";
|
||||||
|
import Path from "../../lib/common/path.ts";
|
||||||
|
|
||||||
interface Attrs {
|
interface Attrs {
|
||||||
device: FlatDevice;
|
device: FlatDevice;
|
||||||
parameter: Expression;
|
parameter: Expression;
|
||||||
label: Expression;
|
label: Expression;
|
||||||
childParameters: Record<string, { label: string; parameter: Expression }>;
|
childParameters: Record<string, { label: Expression; parameter: Expression }>;
|
||||||
filter?: Expression;
|
filter?: Expression;
|
||||||
deviceQuery: QueryResponse;
|
deviceQuery: QueryResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent<Attrs> = () => {
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
|
let object: Path;
|
||||||
|
let parameters: { label: Expression; parameter: Expression }[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oninit: (vnode) => {
|
oninit: (vnode) => {
|
||||||
const obj = vnode.attrs.parameter;
|
const obj = vnode.attrs.parameter;
|
||||||
if (!Array.isArray(obj) || obj[0] !== "PARAM")
|
if (!(obj instanceof Expression.Parameter))
|
||||||
throw new Error("Object must be a parameter path");
|
throw new Error("Object must be a parameter path");
|
||||||
vnode.state["object"] = obj[1];
|
object = obj.path;
|
||||||
vnode.state["parameters"] = Object.values(vnode.attrs.childParameters);
|
parameters = Object.values(vnode.attrs.childParameters);
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs.device;
|
const device = vnode.attrs.device;
|
||||||
const object = evaluateExpression(vnode.state["object"], device);
|
|
||||||
const parameters = vnode.state["parameters"];
|
|
||||||
|
|
||||||
if (typeof object !== "string" || !device[object]) return null;
|
|
||||||
|
|
||||||
const instances: Set<string> = new Set();
|
const instances: Set<string> = new Set();
|
||||||
const prefix = `${object}.`;
|
const prefix = `${object.toString()}.`;
|
||||||
for (const p in device) {
|
for (const p in device) {
|
||||||
if (p.startsWith(prefix)) {
|
if (!p.startsWith(prefix)) continue;
|
||||||
const i = p.indexOf(".", prefix.length);
|
if (p.lastIndexOf(":") !== -1) continue;
|
||||||
if (i === -1) instances.add(p);
|
const i = p.indexOf(".", prefix.length);
|
||||||
else instances.add(p.slice(0, i));
|
if (i === -1) instances.add(p);
|
||||||
}
|
else instances.add(p.slice(0, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = Object.values(parameters).map((p, i) => {
|
const headers = parameters.map((p, i) => {
|
||||||
const padding = i ? "px-3" : "pl-6 pr-3";
|
const padding = i ? "px-3" : "pl-6 pr-3";
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
@@ -53,7 +51,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||||
padding,
|
padding,
|
||||||
},
|
},
|
||||||
evaluateExpression(p["label"], device),
|
evaluateExpression(p.label, device).value,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,24 +61,33 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const i of instances) {
|
for (const i of instances) {
|
||||||
let filter = "filter" in vnode.attrs ? vnode.attrs.filter : true;
|
let filter: Expression =
|
||||||
|
"filter" in vnode.attrs
|
||||||
|
? vnode.attrs.filter
|
||||||
|
: new Expression.Literal(true);
|
||||||
|
|
||||||
filter = expressionParser.map(filter, (e) => {
|
const root = Path.parse(i);
|
||||||
if (Array.isArray(e) && e[0] === "PARAM")
|
filter = filter.evaluate((e) => {
|
||||||
return ["PARAM", ["||", i, ".", e[1]]];
|
if (e instanceof Expression.Parameter)
|
||||||
|
return new Expression.Parameter(root.concat(e.path));
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!evaluateExpression(filter, device)) continue;
|
if (!evaluateExpression(filter, device).value) continue;
|
||||||
|
|
||||||
const row = parameters.map((p, j) => {
|
const row = parameters.map((p, j) => {
|
||||||
const padding = j ? "px-3" : "pl-6 pr-3";
|
const padding = j ? "px-3" : "pl-6 pr-3";
|
||||||
|
|
||||||
const param = expressionParser.map(p.parameter, (e) => {
|
const param = p.parameter.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "PARAM")
|
if (e instanceof Expression.Parameter)
|
||||||
return ["PARAM", ["||", i, ".", e[1]]];
|
return new Expression.Parameter(root.concat(e.path));
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let type = "parameter";
|
||||||
|
if (p["type"] instanceof Expression)
|
||||||
|
type = evaluateExpression(p["type"], device).value + "";
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"td",
|
"td",
|
||||||
{
|
{
|
||||||
@@ -91,7 +98,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
device: device,
|
device: device,
|
||||||
parameter: param,
|
parameter: param,
|
||||||
},
|
},
|
||||||
p.type || "parameter",
|
type,
|
||||||
Object.assign({}, p, {
|
Object.assign({}, p, {
|
||||||
device: device,
|
device: device,
|
||||||
parameter: param,
|
parameter: param,
|
||||||
@@ -101,7 +108,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (device[i].writable === true) {
|
if (device[i + ":writable"]) {
|
||||||
row.push(
|
row.push(
|
||||||
m(
|
m(
|
||||||
"td",
|
"td",
|
||||||
@@ -116,7 +123,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "deleteObject",
|
name: "deleteObject",
|
||||||
device: device["DeviceID.ID"].value[0] as string,
|
device: device["DeviceID.ID"] as string,
|
||||||
objectName: i,
|
objectName: i,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -147,7 +154,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device[object].writable === true) {
|
if (device[object.toString() + ":writable"]) {
|
||||||
rows.push(
|
rows.push(
|
||||||
m(
|
m(
|
||||||
"tr",
|
"tr",
|
||||||
@@ -161,8 +168,8 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "addObject",
|
name: "addObject",
|
||||||
device: device["DeviceID.ID"].value[0] as string,
|
device: device["DeviceID.ID"] as string,
|
||||||
objectName: object,
|
objectName: object.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+42
-48
@@ -2,54 +2,52 @@ import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as taskQueue from "../task-queue.ts";
|
import * as taskQueue from "../task-queue.ts";
|
||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import { evaluate } from "../../lib/common/expression/util.ts";
|
import Expression, { Value } from "../../lib/common/expression.ts";
|
||||||
import memoize from "../../lib/common/memoize.ts";
|
import memoize from "../../lib/common/memoize.ts";
|
||||||
import timeAgo from "../timeago.ts";
|
import timeAgo from "../timeago.ts";
|
||||||
import { icon } from "../tailwind-utility-components.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
|
|
||||||
const evaluateParam = memoize((exp, obj, now: number) => {
|
const evaluateParam = memoize(
|
||||||
let timestamp = now;
|
(
|
||||||
exp = evaluate(exp, null, now, (e) => {
|
exp: Expression,
|
||||||
if (!Array.isArray(e)) return e;
|
obj: any,
|
||||||
for (let i = 1; i < e.length; ++i) {
|
now: number,
|
||||||
if (
|
): { value: Value; timestamp: number; parameter: string } => {
|
||||||
Array.isArray(e[i]) &&
|
let timestamp = now;
|
||||||
e[i][0] === "PARAM" &&
|
const valueMap: Map<Expression.Literal, string> = new Map();
|
||||||
!Array.isArray(e[i][1])
|
const lit = exp.evaluate((e): Expression.Literal => {
|
||||||
) {
|
if (e instanceof Expression.Literal) return e;
|
||||||
let v = null;
|
if (e instanceof Expression.Parameter) {
|
||||||
const p = obj[e[i][1]];
|
let v = obj[e.path.toString()];
|
||||||
if (p?.value) {
|
if (v) {
|
||||||
v = p.value[0];
|
timestamp = Math.min(
|
||||||
timestamp = Math.min(timestamp, p.valueTimestamp);
|
timestamp,
|
||||||
|
obj[e.path.toString() + ":valueTimestamp"] ?? 0,
|
||||||
|
);
|
||||||
|
const t = obj[e.path.toString() + ":type"];
|
||||||
|
if (t === "xsd:dateTime" && typeof v === "number")
|
||||||
|
v = new Date(v).toLocaleString();
|
||||||
|
const val = new Expression.Literal(v);
|
||||||
|
valueMap.set(val, e.path.toString());
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
} else if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(now);
|
||||||
|
else if (e.name === "DATE_STRING") {
|
||||||
|
const v = e.args[0];
|
||||||
|
if (v instanceof Expression.Literal) {
|
||||||
|
return new Expression.Literal(
|
||||||
|
new Date(v.value as string | number).toLocaleString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
e = e.slice();
|
|
||||||
e[i] = v;
|
|
||||||
}
|
}
|
||||||
}
|
return new Expression.Literal(null);
|
||||||
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2]))
|
});
|
||||||
return new Date(e[2]).toLocaleString();
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
|
|
||||||
let parameter = null;
|
return { value: lit.value, timestamp, parameter: valueMap.get(lit) };
|
||||||
let value = null;
|
},
|
||||||
|
);
|
||||||
if (!Array.isArray(exp)) {
|
|
||||||
value = exp;
|
|
||||||
} else if (exp[0] === "PARAM") {
|
|
||||||
const p = obj[exp[1]];
|
|
||||||
if (p?.value) {
|
|
||||||
timestamp = p.valueTimestamp;
|
|
||||||
value = p.value[0];
|
|
||||||
parameter = exp[1];
|
|
||||||
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
|
|
||||||
value = new Date(value).toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value, timestamp, parameter };
|
|
||||||
});
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
@@ -65,7 +63,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
|
|
||||||
let edit;
|
let edit;
|
||||||
if (device[parameter]?.writable) {
|
if (device[parameter + ":writable"]) {
|
||||||
edit = m(
|
edit = m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
@@ -73,13 +71,9 @@ const component: ClosureComponent = (): Component => {
|
|||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.stageSpv({
|
taskQueue.stageSpv({
|
||||||
name: "setParameterValues",
|
name: "setParameterValues",
|
||||||
devices: [device["DeviceID.ID"].value[0]],
|
devices: [device["DeviceID.ID"]],
|
||||||
parameterValues: [
|
parameterValues: [
|
||||||
[
|
[parameter, device[parameter], device[parameter + ":type"]],
|
||||||
parameter,
|
|
||||||
device[parameter].value[0],
|
|
||||||
device[parameter].value[1],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as taskQueue from "../task-queue.ts";
|
import * as taskQueue from "../task-queue.ts";
|
||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import * as notifications from "../notifications.ts";
|
import * as notifications from "../notifications.ts";
|
||||||
|
import Expression from "../../lib/common/expression.ts";
|
||||||
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
device: FlatDevice;
|
||||||
|
parameters: Record<string, Expression>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs.device;
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"button",
|
"button",
|
||||||
@@ -19,15 +26,16 @@ const component: ClosureComponent = (): Component => {
|
|||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
const params = Object.values(vnode.attrs["parameters"])
|
const params = Object.values(vnode.attrs["parameters"])
|
||||||
.map((exp) => {
|
.map((exp) => {
|
||||||
if (!Array.isArray(exp) || exp[0] !== "PARAM") return null;
|
if (exp instanceof Expression.Parameter)
|
||||||
return store.evaluateExpression(exp[1], device) as string;
|
return exp.path.toString();
|
||||||
|
return null;
|
||||||
})
|
})
|
||||||
.filter((exp) => !!exp);
|
.filter((exp) => !!exp);
|
||||||
|
|
||||||
const task = {
|
const task = {
|
||||||
name: "getParameterValues",
|
name: "getParameterValues",
|
||||||
parameterNames: params,
|
parameterNames: params,
|
||||||
device: device["DeviceID.ID"].value[0],
|
device: device["DeviceID.ID"] as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
taskQueue
|
taskQueue
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as notifications from "../notifications.ts";
|
|||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import { icon } from "../tailwind-utility-components.ts";
|
import { icon } from "../tailwind-utility-components.ts";
|
||||||
import { decodeTag } from "../../lib/util.ts";
|
import { decodeTag } from "../../lib/util.ts";
|
||||||
import { Expression } from "../../lib/types.ts";
|
import Expression from "../../lib/common/expression.ts";
|
||||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||||
|
|
||||||
interface Attrs {
|
interface Attrs {
|
||||||
@@ -22,7 +22,8 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
|
|
||||||
const tags = [];
|
const tags = [];
|
||||||
for (const p of Object.keys(device))
|
for (const p of Object.keys(device))
|
||||||
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
|
if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
|
||||||
|
tags.push(decodeTag(p.slice(5)));
|
||||||
|
|
||||||
tags.sort();
|
tags.sort();
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
"flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
"flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
const deviceId = device["DeviceID.ID"].value[0] as string;
|
const deviceId = device["DeviceID.ID"] as string;
|
||||||
store
|
store
|
||||||
.updateTags(deviceId, { [tag]: false })
|
.updateTags(deviceId, { [tag]: false })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -103,7 +104,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
"flex-shrink-0 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
"flex-shrink-0 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
e.target.disabled = true;
|
e.target.disabled = true;
|
||||||
const deviceId = device["DeviceID.ID"].value[0] as string;
|
const deviceId = device["DeviceID.ID"] as string;
|
||||||
const tag = prompt(`Enter tag to assign to device:`);
|
const tag = prompt(`Enter tag to assign to device:`);
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ import * as notifications from "./notifications.ts";
|
|||||||
import putFormComponent from "./put-form-component.ts";
|
import putFormComponent from "./put-form-component.ts";
|
||||||
import uiConfigComponent from "./ui-config-component.ts";
|
import uiConfigComponent from "./ui-config-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts";
|
import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts";
|
||||||
import { icon } from "./tailwind-utility-components.ts";
|
import { icon } from "./tailwind-utility-components.ts";
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
if (!id.match(regex)) return void resolve({ _id: "Invalid ID" });
|
if (!id.match(regex)) return void resolve({ _id: "Invalid ID" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
object.value = stringify(parse(object.value || ""));
|
object.value = Expression.parse(object.value || "").toString();
|
||||||
} catch {
|
} catch {
|
||||||
return void resolve({
|
return void resolve({
|
||||||
value: "Config value must be valid expression",
|
value: "Config value must be valid expression",
|
||||||
@@ -232,7 +232,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const confs = store.fetch("config", true);
|
const confs = store.fetch("config", new Expression.Literal(true));
|
||||||
|
|
||||||
let newConfig;
|
let newConfig;
|
||||||
const subs = [];
|
const subs = [];
|
||||||
|
|||||||
+111
-1
@@ -1,3 +1,113 @@
|
|||||||
export default window.clientConfig;
|
import Expression from "../lib/common/expression.ts";
|
||||||
export const configSnapshot = window.configSnapshot;
|
export const configSnapshot = window.configSnapshot;
|
||||||
export const genieacsVersion = window.genieacsVersion;
|
export const genieacsVersion = window.genieacsVersion;
|
||||||
|
|
||||||
|
type Filters = { label: string; parameter: Expression; type: string }[];
|
||||||
|
type pageSize = number;
|
||||||
|
type overview = {
|
||||||
|
charts: {
|
||||||
|
[name: string]: {
|
||||||
|
label: string;
|
||||||
|
slices: {
|
||||||
|
label: string;
|
||||||
|
filter: Expression;
|
||||||
|
color: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
groups: {
|
||||||
|
label: string;
|
||||||
|
charts: string[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Index = {
|
||||||
|
label: string;
|
||||||
|
type?: string;
|
||||||
|
parameter: Expression;
|
||||||
|
unsortable: boolean;
|
||||||
|
raw: NestedRecord;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
type NestedRecord = { [k: string]: Expression | NestedRecord };
|
||||||
|
|
||||||
|
const conf: NestedRecord = {};
|
||||||
|
for (const [key, value] of Object.entries(window.clientConfig)) {
|
||||||
|
const exp = Expression.parse(value).evaluate((e) => e);
|
||||||
|
let ref = conf;
|
||||||
|
const keyParts = key.split(".");
|
||||||
|
while (keyParts.length > 1) {
|
||||||
|
const k = keyParts.shift();
|
||||||
|
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
||||||
|
ref = ref[k] as NestedRecord;
|
||||||
|
}
|
||||||
|
ref[keyParts[0]] = exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filters: Filters = [];
|
||||||
|
export let pageSize: number = 10;
|
||||||
|
export const overview: overview = { charts: {}, groups: [] };
|
||||||
|
export const index: Index = [];
|
||||||
|
export let device: NestedRecord = {};
|
||||||
|
|
||||||
|
for (const obj of Object.values(conf["filters"] || {})) {
|
||||||
|
let label = "";
|
||||||
|
let parameter: Expression = new Expression.Literal(false);
|
||||||
|
let type = "string";
|
||||||
|
if (obj["label"] instanceof Expression.Literal)
|
||||||
|
label = obj["label"].value as string;
|
||||||
|
if (obj["parameter"] instanceof Expression) parameter = obj["parameter"];
|
||||||
|
if (obj["type"] instanceof Expression.Literal)
|
||||||
|
type = obj["type"].value as string;
|
||||||
|
filters.push({ label, parameter, type });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of Object.values(conf["index"] || {})) {
|
||||||
|
let label = "";
|
||||||
|
let parameter: Expression = new Expression.Literal(null);
|
||||||
|
let unsortable = false;
|
||||||
|
let type = "";
|
||||||
|
if (obj["label"] instanceof Expression.Literal)
|
||||||
|
label = obj["label"].value as string;
|
||||||
|
if (obj["type"] instanceof Expression.Literal)
|
||||||
|
type = obj["type"].value as string;
|
||||||
|
if (obj["parameter"] instanceof Expression) parameter = obj["parameter"];
|
||||||
|
if (obj["unsortable"] instanceof Expression.Literal)
|
||||||
|
unsortable = obj["unsortable"].value as boolean;
|
||||||
|
index.push({ label, type, parameter, unsortable, raw: obj });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of Object.values(conf["overview"]?.["groups"] || {})) {
|
||||||
|
let label = "";
|
||||||
|
const charts: string[] = [];
|
||||||
|
if (obj["label"] instanceof Expression.Literal)
|
||||||
|
label = obj["label"].value as string;
|
||||||
|
for (const chart of Object.values(obj["charts"] || {})) {
|
||||||
|
if (chart instanceof Expression.Literal) charts.push(chart.value as string);
|
||||||
|
}
|
||||||
|
overview.groups.push({ label, charts });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, obj] of Object.entries(conf["overview"]?.["charts"] || {})) {
|
||||||
|
const slices: { label: string; filter: Expression; color: string }[] = [];
|
||||||
|
for (const slice of Object.values(obj["slices"] || {})) {
|
||||||
|
let label = "";
|
||||||
|
let filter: Expression = new Expression.Literal(false);
|
||||||
|
let color = "";
|
||||||
|
if (slice["label"] instanceof Expression.Literal)
|
||||||
|
label = slice["label"].value as string;
|
||||||
|
if (slice["filter"] instanceof Expression) filter = slice["filter"];
|
||||||
|
if (slice["color"] instanceof Expression.Literal)
|
||||||
|
color = slice["color"].value as string;
|
||||||
|
slices.push({ label, filter, color });
|
||||||
|
}
|
||||||
|
let label = "";
|
||||||
|
if (obj["label"] instanceof Expression.Literal)
|
||||||
|
label = obj["label"].value as string;
|
||||||
|
overview.charts[name] = { label, slices };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conf["pageSize"] instanceof Expression.Literal)
|
||||||
|
pageSize = +conf["pageSize"].value || 10;
|
||||||
|
|
||||||
|
device = conf["device"] as NestedRecord;
|
||||||
+20
-9
@@ -1,7 +1,9 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { device as deviceConfig } from "./config.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
import Path from "../lib/common/path.ts";
|
||||||
|
|
||||||
export function init(
|
export function init(
|
||||||
args: Record<string, unknown>,
|
args: Record<string, unknown>,
|
||||||
@@ -14,16 +16,25 @@ export function init(
|
|||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
deviceId: args.id,
|
deviceId: args.id,
|
||||||
deviceFilter: ["=", ["PARAM", "DeviceID.ID"], args.id],
|
deviceFilter: new Expression.Binary(
|
||||||
|
"=",
|
||||||
|
new Expression.Parameter(Path.parse("DeviceID.ID")),
|
||||||
|
new Expression.Literal(args.id as string),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
deviceId: string;
|
||||||
|
deviceFilter: Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
document.title = `${vnode.attrs.deviceId} - Devices - GenieACS`;
|
||||||
|
|
||||||
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
|
const dev = store.fetch("devices", vnode.attrs.deviceFilter);
|
||||||
if (!dev.value.length) {
|
if (!dev.value.length) {
|
||||||
if (!dev.fulfilling) {
|
if (!dev.fulfilling) {
|
||||||
return m(
|
return m(
|
||||||
@@ -38,15 +49,15 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const conf = config.ui.device;
|
const conf = deviceConfig;
|
||||||
const cmps = [];
|
const cmps = [];
|
||||||
|
|
||||||
for (const c of Object.values(conf)) {
|
for (const c of Object.values(conf)) {
|
||||||
cmps.push(
|
cmps.push(
|
||||||
m.context(
|
m.context(
|
||||||
{ device: dev.value[0], deviceQuery: dev },
|
{ device: dev.value[0], deviceQuery: dev },
|
||||||
store.evaluateExpression(c["type"], null) as string,
|
store.evaluateExpression(c["type"], {}).value as string,
|
||||||
c,
|
c as any,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-56
@@ -1,63 +1,72 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE, index as indexConfig } from "./config.ts";
|
||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import { queueTask, stageDownload } from "./task-queue.ts";
|
import { queueTask, stageDownload } from "./task-queue.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
|
import Expression, { extractPaths } from "../lib/common/expression.ts";
|
||||||
import { evaluate, extractParams } from "../lib/common/expression/util.ts";
|
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
const memoizedGetSortable = memoize((p: Expression) => {
|
||||||
|
const expressionParams = extractPaths(p);
|
||||||
const memoizedParse = memoize(parse);
|
if (expressionParams.length === 1) return expressionParams[0];
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
|
||||||
const memoizedGetSortable = memoize((p) => {
|
|
||||||
const expressionParams = extractParams(p);
|
|
||||||
if (expressionParams.length === 1) {
|
|
||||||
const param = evaluate(expressionParams[0]);
|
|
||||||
if (typeof param === "string") return param;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDownloadUrl = memoize((filter, indexParameters) => {
|
const getDownloadUrl = memoize(
|
||||||
const columns = {};
|
(
|
||||||
for (const p of indexParameters)
|
filter: Expression,
|
||||||
columns[store.evaluateExpression(p.label, null) as string] = stringify(
|
indexParameters: { label: string; parameter: Expression }[],
|
||||||
p.parameter,
|
) => {
|
||||||
);
|
const columns = {};
|
||||||
return `api/devices.csv?${m.buildQueryString({
|
for (const p of indexParameters) columns[p.label] = p.parameter.toString();
|
||||||
filter: stringify(filter),
|
return `api/devices.csv?${m.buildQueryString({
|
||||||
columns: JSON.stringify(columns),
|
filter: filter.toString(),
|
||||||
})}`;
|
columns: JSON.stringify(columns),
|
||||||
});
|
})}`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("devices", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"devices",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function init(
|
export function init(args: Record<string, unknown>): Promise<Attrs> {
|
||||||
args: Record<string, unknown>,
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!window.authorizer.hasAccess("devices", 2))
|
if (!window.authorizer.hasAccess("devices", 2))
|
||||||
return void reject(new Error("You are not authorized to view this page"));
|
return void reject(new Error("You are not authorized to view this page"));
|
||||||
|
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let filter: Expression = null;
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let sort: Record<string, number> = null;
|
||||||
const indexParameters = Object.values(config.ui.index);
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
|
const indexParameters = indexConfig;
|
||||||
if (!indexParameters.length) {
|
if (!indexParameters.length) {
|
||||||
indexParameters.push({
|
indexParameters.push({
|
||||||
label: "ID",
|
label: "ID",
|
||||||
parameter: ["PARAM", "DeviceID.ID"],
|
parameter: Expression.parse("DeviceID.ID"),
|
||||||
|
unsortable: false,
|
||||||
|
raw: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resolve({ filter, indexParameters, sort });
|
resolve({ filter, indexParameters, sort });
|
||||||
@@ -220,11 +229,17 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
indexParameters: typeof indexConfig;
|
||||||
|
filter?: Expression;
|
||||||
|
sort?: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = "Devices - GenieACS";
|
document.title = "Devices - GenieACS";
|
||||||
const attributes = vnode.attrs["indexParameters"];
|
const attributes = vnode.attrs.indexParameters;
|
||||||
|
|
||||||
function showMore(): void {
|
function showMore(): void {
|
||||||
vnode.state["showCount"] =
|
vnode.state["showCount"] =
|
||||||
@@ -232,22 +247,22 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter: Expression): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
|
if (vnode.attrs.sort) ops["sort"] = vnode.attrs.sort;
|
||||||
m.route.set("/devices", ops);
|
m.route.set("/devices", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = vnode.attrs["sort"]
|
const sort = vnode.attrs.sort || {};
|
||||||
? memoizedJsonParse(vnode.attrs["sort"])
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const sortAttributes = {};
|
const sortAttributes = {};
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
const attr = attributes[i];
|
const attr = attributes[i];
|
||||||
if (attr.unsortable) continue;
|
if (attr.unsortable) continue;
|
||||||
const param = memoizedGetSortable(attr.parameter);
|
const param = memoizedGetSortable(attr.parameter);
|
||||||
if (param) sortAttributes[i] = sort[param] || 0;
|
if (param) sortAttributes[i] = sort[param.toString()] || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSortChange(sortedAttrs): void {
|
function onSortChange(sortedAttrs): void {
|
||||||
@@ -256,17 +271,16 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
const param = memoizedGetSortable(
|
const param = memoizedGetSortable(
|
||||||
attributes[Math.abs(index) - 1].parameter,
|
attributes[Math.abs(index) - 1].parameter,
|
||||||
);
|
);
|
||||||
_sort[param] = Math.sign(index);
|
_sort[param.toString()] = Math.sign(index);
|
||||||
}
|
}
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/devices", ops);
|
m.route.set("/devices", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const devs = store.fetch("devices", filter, {
|
const devs = store.fetch("devices", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
@@ -280,15 +294,15 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return m.context(
|
return m.context(
|
||||||
{ device: device, parameter: attr.parameter },
|
{ device: device, parameter: attr.parameter },
|
||||||
attr.type || "parameter",
|
attr.type || "parameter",
|
||||||
attr,
|
attr.raw,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
attrs["attributes"] = attributes.map((a) => ({
|
attrs["attributes"] = attributes.map((a) => ({
|
||||||
...a,
|
...a,
|
||||||
label: store.evaluateExpression(a.label, null),
|
label: a.label,
|
||||||
type: store.evaluateExpression(a.type, null),
|
type: a.type,
|
||||||
}));
|
}));
|
||||||
attrs["data"] = devs.value;
|
attrs["data"] = devs.value;
|
||||||
attrs["total"] = count.value;
|
attrs["total"] = count.value;
|
||||||
@@ -301,9 +315,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return m(
|
return m(
|
||||||
"a.text-cyan-700 hover:text-cyan-900",
|
"a.text-cyan-700 hover:text-cyan-900",
|
||||||
{
|
{
|
||||||
href: `#!/devices/${encodeURIComponent(
|
href: `#!/devices/${encodeURIComponent(device["DeviceID.ID"])}`,
|
||||||
device["DeviceID.ID"].value[0],
|
|
||||||
)}`,
|
|
||||||
},
|
},
|
||||||
"Show",
|
"Show",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
queueTask,
|
queueTask,
|
||||||
StageTask,
|
StageTask,
|
||||||
} from "./task-queue.ts";
|
} from "./task-queue.ts";
|
||||||
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
const invalid: WeakSet<StageTask> = new WeakSet();
|
const invalid: WeakSet<StageTask> = new WeakSet();
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
|||||||
function renderStagingDownload(task: StageTask): Children {
|
function renderStagingDownload(task: StageTask): Children {
|
||||||
if (!task.fileName || !task.fileType) invalid.add(task);
|
if (!task.fileName || !task.fileType) invalid.add(task);
|
||||||
else invalid.delete(task);
|
else invalid.delete(task);
|
||||||
const files = store.fetch("files", true);
|
const files = store.fetch("files", new Expression.Literal(true));
|
||||||
let oui = "";
|
let oui = "";
|
||||||
let productClass = "";
|
let productClass = "";
|
||||||
for (const d of task.devices) {
|
for (const d of task.devices) {
|
||||||
|
|||||||
+30
-18
@@ -1,18 +1,15 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
|
||||||
import { stringify as yamlStringify } from "../lib/common/yaml.ts";
|
import { stringify as yamlStringify } from "../lib/common/yaml.ts";
|
||||||
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
@@ -33,15 +30,27 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `api/faults.csv?${m.buildQueryString({
|
return `api/faults.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("faults", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"faults",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -66,9 +75,11 @@ export function init(
|
|||||||
new Error("You are not authorized to view this page"),
|
new Error("You are not authorized to view this page"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let filter: Expression = null;
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let sort: Record<string, number> = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
return Promise.resolve({ filter, sort });
|
return Promise.resolve({ filter, sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +95,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/faults", ops);
|
m.route.set("/faults", ops);
|
||||||
}
|
}
|
||||||
@@ -108,10 +121,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/faults", ops);
|
m.route.set("/faults", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const faults = store.fetch("faults", filter, {
|
const faults = store.fetch("faults", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
|||||||
+30
-17
@@ -1,6 +1,6 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
@@ -9,11 +9,8 @@ import putFormComponent from "./put-form-component.ts";
|
|||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes: {
|
const attributes: {
|
||||||
@@ -46,10 +43,22 @@ const formData = {
|
|||||||
.concat([{ id: "file", label: "File", type: "file" }]),
|
.concat([{ id: "file", label: "File", type: "file" }]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("files", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"files",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -87,7 +96,7 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
const cols = {};
|
const cols = {};
|
||||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
return `api/files.csv?${m.buildQueryString({
|
return `api/files.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
@@ -101,8 +110,11 @@ export function init(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let filter: Expression = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let sort: Record<string, number> = null;
|
||||||
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
return Promise.resolve({ filter, sort });
|
return Promise.resolve({ filter, sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +130,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/files", ops);
|
m.route.set("/files", ops);
|
||||||
}
|
}
|
||||||
@@ -140,10 +154,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/files", ops);
|
m.route.set("/files", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const files = store.fetch("files", filter, {
|
const files = store.fetch("files", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
|||||||
+51
-33
@@ -1,10 +1,9 @@
|
|||||||
import m, { ClosureComponent } from "mithril";
|
import m, { ClosureComponent } from "mithril";
|
||||||
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
|
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
import Autocomplete from "./autocomplete-compnent.ts";
|
import Autocomplete from "./autocomplete-compnent.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { validQuery } from "../lib/db/synth.ts";
|
import { validQuery } from "../lib/db/synth.ts";
|
||||||
import { Expression } from "../lib/types.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
const getAutocomplete = memoize((resource) => {
|
const getAutocomplete = memoize((resource) => {
|
||||||
const labels = smartQuery.getLabels(resource);
|
const labels = smartQuery.getLabels(resource);
|
||||||
@@ -22,20 +21,35 @@ const getAutocomplete = memoize((resource) => {
|
|||||||
return autocomplete;
|
return autocomplete;
|
||||||
});
|
});
|
||||||
|
|
||||||
function parseFilter(resource, f): Expression {
|
function parseFilter(resource: string, f: string): Expression {
|
||||||
let exp;
|
let exp;
|
||||||
if (/^[\s0-9a-zA-Z]+:/.test(f)) {
|
if (/^[\s0-9a-zA-Z]+:/.test(f)) {
|
||||||
const k = f.split(":", 1)[0];
|
const k = f.split(":", 1)[0];
|
||||||
const v = f.slice(k.length + 1).trim();
|
const v = f.slice(k.length + 1).trim();
|
||||||
exp = ["FUNC", "Q", k.trim(), v];
|
exp = new Expression.FunctionCall("Q", [
|
||||||
|
new Expression.Literal(k.trim()),
|
||||||
|
new Expression.Literal(v),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
exp = parse(f);
|
exp = Expression.parse(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpacked = map(exp, (e) => {
|
const unpacked = exp.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC") {
|
if (e instanceof Expression.FunctionCall) {
|
||||||
if (e[1] === "Q") return smartQuery.unpack(resource, e[2], e[3]);
|
if (e.name === "NOW") return new Expression.Literal(Date.now());
|
||||||
else if (e[1] === "NOW") return Date.now();
|
else if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
const r = smartQuery.unpack(
|
||||||
|
resource,
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
@@ -46,28 +60,33 @@ function parseFilter(resource, f): Expression {
|
|||||||
return exp;
|
return exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyFilter(f: Expression): string {
|
function splitFilter(filter: Expression): string[] {
|
||||||
if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q")
|
|
||||||
return `${f[2]}: ${f[3]}`;
|
|
||||||
return stringify(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitFilter(filter: string): string[] {
|
|
||||||
if (!filter) return [""];
|
if (!filter) return [""];
|
||||||
const list: string[] = [];
|
if (filter instanceof Expression.Literal && filter.value) return [""];
|
||||||
const f = parse(filter);
|
const list: Expression[] = [filter];
|
||||||
if (Array.isArray(f) && f[0] === "AND")
|
const res: string[] = [];
|
||||||
for (const ff of f.slice(1)) list.push(stringifyFilter(ff));
|
while (list.length) {
|
||||||
else list.push(stringifyFilter(f));
|
const f = list.pop();
|
||||||
|
if (f instanceof Expression.Binary && f.operator === "AND") {
|
||||||
|
list.push(f.right);
|
||||||
|
list.push(f.left);
|
||||||
|
} else if (f instanceof Expression.FunctionCall && f.name === "Q") {
|
||||||
|
const l = f.args[0] as Expression.Literal;
|
||||||
|
const r = f.args[1] as Expression.Literal;
|
||||||
|
res.push(`${l.value}: ${r.value}`);
|
||||||
|
} else {
|
||||||
|
res.push(f.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list.push("");
|
res.push("");
|
||||||
return list;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Attrs {
|
interface Attrs {
|
||||||
resource: string;
|
resource: string;
|
||||||
filter: string;
|
filter?: Expression;
|
||||||
onChange: (filter: string) => void;
|
onChange: (filter: Expression) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent<Attrs> = (initialVnode) => {
|
const component: ClosureComponent<Attrs> = (initialVnode) => {
|
||||||
@@ -80,23 +99,22 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
|
|||||||
filterTouched = false;
|
filterTouched = false;
|
||||||
filterInvalid = 0;
|
filterInvalid = 0;
|
||||||
filterList = filterList.filter((f) => f);
|
filterList = filterList.filter((f) => f);
|
||||||
const list = filterList.map((f, idx) => {
|
let filter: Expression = new Expression.Literal(true);
|
||||||
|
for (const [idx, f] of filterList.entries()) {
|
||||||
try {
|
try {
|
||||||
return parseFilter(attrs.resource, f);
|
filter = Expression.and(filter, parseFilter(attrs.resource, f));
|
||||||
} catch {
|
} catch {
|
||||||
filterInvalid |= 1 << idx;
|
filterInvalid |= 1 << idx;
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
});
|
|
||||||
filterList.push("");
|
filterList.push("");
|
||||||
|
|
||||||
if (filterInvalid) {
|
if (filterInvalid) {
|
||||||
m.redraw();
|
m.redraw();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (list.length === 0) attrs.onChange("");
|
if (!filterList.length) attrs.onChange(null);
|
||||||
else if (list.length > 1) attrs.onChange(stringify(["AND", ...list]));
|
else attrs.onChange(filter);
|
||||||
else attrs.onChange(stringify(list[0]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -120,7 +138,7 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
|
|||||||
"appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm";
|
"appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm";
|
||||||
if (idx === 0) classNames += " rounded-t-md";
|
if (idx === 0) classNames += " rounded-t-md";
|
||||||
if (idx === filterList.length - 1) classNames += " rounded-b-md";
|
if (idx === filterList.length - 1) classNames += " rounded-b-md";
|
||||||
if (filterInvalid & (1 << idx)) classNames += " text-red-700";
|
if (filterInvalid & (1 << idx)) classNames += " !text-red-700";
|
||||||
|
|
||||||
return m(`input`, {
|
return m(`input`, {
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function renderTable(
|
|||||||
checked: records.length && selected.size === records.length,
|
checked: records.length && selected.size === records.length,
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
const id = record["_id"] ?? record["DeviceID.ID"];
|
||||||
if (e.target.checked) selected.add(id);
|
if (e.target.checked) selected.add(id);
|
||||||
else selected.delete(id);
|
else selected.delete(id);
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ function renderTable(
|
|||||||
// Table rows
|
// Table rows
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
const id = record["_id"] ?? record["DeviceID.ID"];
|
||||||
const tds = [];
|
const tds = [];
|
||||||
const isSelected = selected.has(id);
|
const isSelected = selected.has(id);
|
||||||
if (buttons.length) {
|
if (buttons.length) {
|
||||||
@@ -224,7 +224,7 @@ function renderTable(
|
|||||||
{
|
{
|
||||||
class: isSelected ? "bg-stone-50" : "",
|
class: isSelected ? "bg-stone-50" : "",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
|
if (e.target.closest("input, button, a")) {
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
const _selected = new Set<string>();
|
const _selected = new Set<string>();
|
||||||
for (const record of data) {
|
for (const record of data) {
|
||||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
const id = record["_id"] ?? record["DeviceID.ID"];
|
||||||
if (selected.has(id)) _selected.add(id);
|
if (selected.has(id)) _selected.add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-28
@@ -1,31 +1,29 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { overview } from "./config.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import pieChartComponent from "./pie-chart-component.ts";
|
import pieChartComponent from "./pie-chart-component.ts";
|
||||||
|
|
||||||
const GROUPS = config.ui.overview.groups || {};
|
const GROUPS = overview.groups;
|
||||||
const CHARTS = {};
|
const CHARTS: typeof overview.charts = {};
|
||||||
for (const group of Object.values(GROUPS)) {
|
for (const group of GROUPS) {
|
||||||
for (const chartName of Object.values(group["charts"]) as string[])
|
for (const chartName of group.charts)
|
||||||
CHARTS[chartName] = config.ui.overview.charts[chartName];
|
CHARTS[chartName] = overview.charts[chartName];
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryCharts(charts: Record<string, unknown>): Record<string, unknown> {
|
function queryCharts(charts: typeof overview.charts): typeof charts {
|
||||||
charts = Object.assign({}, charts);
|
charts = Object.assign({}, charts);
|
||||||
for (let [chartName, chart] of Object.entries(charts)) {
|
for (let [chartName, chart] of Object.entries(charts)) {
|
||||||
charts[chartName] = chart = Object.assign({}, chart);
|
charts[chartName] = chart = { ...chart };
|
||||||
chart["slices"] = Object.assign({}, chart["slices"]);
|
chart.slices = chart.slices.map((s) => ({ ...s }));
|
||||||
for (let [sliceName, slice] of Object.entries(chart["slices"])) {
|
for (const slice of chart.slices) {
|
||||||
const filter = slice["filter"];
|
slice["count"] = store.count("devices", slice.filter);
|
||||||
chart["slices"][sliceName] = slice = Object.assign({}, slice);
|
|
||||||
slice["count"] = store.count("devices", filter);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init(): Promise<Record<string, unknown>> {
|
export function init(): Promise<{ charts: typeof overview.charts }> {
|
||||||
if (!window.authorizer.hasAccess("devices", 1)) {
|
if (!window.authorizer.hasAccess("devices", 1)) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error("You are not authorized to view this page"),
|
new Error("You are not authorized to view this page"),
|
||||||
@@ -35,37 +33,36 @@ export function init(): Promise<Record<string, unknown>> {
|
|||||||
return Promise.resolve({ charts: queryCharts(CHARTS) });
|
return Promise.resolve({ charts: queryCharts(CHARTS) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
charts: typeof overview.charts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = "Overview - GenieACS";
|
document.title = "Overview - GenieACS";
|
||||||
const children = [];
|
const children = [];
|
||||||
for (const group of Object.values(GROUPS)) {
|
for (const group of GROUPS) {
|
||||||
if (group["label"]) {
|
if (group.label) {
|
||||||
children.push(
|
children.push(
|
||||||
m(
|
m("h1.text-xl font-medium text-stone-900 mb-5", group["label"]),
|
||||||
"h1.text-xl font-medium text-stone-900 mb-5",
|
|
||||||
store.evaluateExpression(group["label"], null),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupChildren = [];
|
const groupChildren = [];
|
||||||
for (const chartName of Object.values(group["charts"]) as string[]) {
|
for (const chartName of group.charts) {
|
||||||
const chart = vnode.attrs["charts"][chartName];
|
const chart = vnode.attrs.charts[chartName];
|
||||||
const chartChildren = [];
|
const chartChildren = [];
|
||||||
if (chart.label) {
|
if (chart.label) {
|
||||||
chartChildren.push(
|
chartChildren.push(
|
||||||
m(
|
m(
|
||||||
"h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center",
|
"h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center",
|
||||||
store.evaluateExpression(chart.label, null),
|
chart.label,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs = {};
|
chartChildren.push(m(pieChartComponent, { chart }));
|
||||||
attrs["chart"] = chart;
|
|
||||||
chartChildren.push(m(pieChartComponent, attrs));
|
|
||||||
|
|
||||||
groupChildren.push(
|
groupChildren.push(
|
||||||
m(
|
m(
|
||||||
|
|||||||
+33
-19
@@ -1,6 +1,6 @@
|
|||||||
import { Children, ClosureComponent, Component } from "mithril";
|
import { Children, ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
@@ -8,12 +8,10 @@ import putFormComponent from "./put-form-component.ts";
|
|||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
const memoizedParse = memoize((str) => Expression.parse(str));
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
@@ -60,10 +58,22 @@ function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("permissions", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"permissions",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -89,7 +99,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
|
|
||||||
if (object.filter) {
|
if (object.filter) {
|
||||||
try {
|
try {
|
||||||
object.filter = stringify(memoizedParse(object.filter));
|
object.filter = memoizedParse(object.filter).toString();
|
||||||
} catch {
|
} catch {
|
||||||
return void resolve({
|
return void resolve({
|
||||||
filter: "Filter must be valid expression",
|
filter: "Filter must be valid expression",
|
||||||
@@ -99,7 +109,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
|
|
||||||
if (object.validate) {
|
if (object.validate) {
|
||||||
try {
|
try {
|
||||||
object.validate = stringify(memoizedParse(object.validate));
|
object.validate = memoizedParse(object.validate).toString();
|
||||||
} catch {
|
} catch {
|
||||||
return void resolve({
|
return void resolve({
|
||||||
validate: "Validate must be valid expression",
|
validate: "Validate must be valid expression",
|
||||||
@@ -163,7 +173,7 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
const cols = {};
|
const cols = {};
|
||||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
return `api/permissions.csv?${m.buildQueryString({
|
return `api/permissions.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
@@ -176,8 +186,11 @@ export function init(
|
|||||||
new Error("You are not authorized to view this page"),
|
new Error("You are not authorized to view this page"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let filter: Expression = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let sort: Record<string, number> = null;
|
||||||
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
return Promise.resolve({ filter, sort });
|
return Promise.resolve({ filter, sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +206,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/permissions", ops);
|
m.route.set("/permissions", ops);
|
||||||
}
|
}
|
||||||
@@ -218,10 +233,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/permissions", ops);
|
m.route.set("/permissions", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const permissions = store.fetch("permissions", filter, {
|
const permissions = store.fetch("permissions", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
|||||||
+21
-14
@@ -1,12 +1,8 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import { stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import memoize from "../lib/common/memoize.ts";
|
|
||||||
import * as store from "./store.ts";
|
|
||||||
|
|
||||||
const memoizedStringify = memoize(stringify);
|
function drawChart(chartData: Attrs["chart"]): Children {
|
||||||
|
|
||||||
function drawChart(chartData): Children {
|
|
||||||
const slices = chartData.slices;
|
const slices = chartData.slices;
|
||||||
const total: number = Array.from(Object.values(chartData.slices)).reduce(
|
const total: number = Array.from(Object.values(chartData.slices)).reduce(
|
||||||
(a: number, s) => a + (s["count"]["value"] || 0),
|
(a: number, s) => a + (s["count"]["value"] || 0),
|
||||||
@@ -27,10 +23,10 @@ function drawChart(chartData): Children {
|
|||||||
m(
|
m(
|
||||||
"td",
|
"td",
|
||||||
m("span.inline-block w-3 h-3 border border-stone-200 mr-1", {
|
m("span.inline-block w-3 h-3 border border-stone-200 mr-1", {
|
||||||
style: `background-color: ${store.evaluateExpression(slice["color"], null)} !important;`,
|
style: `background-color: ${slice.color} !important;`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
m("td.w-full", store.evaluateExpression(slice["label"], null)),
|
m("td.w-full", slice.label),
|
||||||
m(
|
m(
|
||||||
"td.text-stone-500 text-right tabular-nums",
|
"td.text-stone-500 text-right tabular-nums",
|
||||||
`${Math.round(percent * 100)}%`,
|
`${Math.round(percent * 100)}%`,
|
||||||
@@ -41,7 +37,7 @@ function drawChart(chartData): Children {
|
|||||||
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
|
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
|
||||||
{
|
{
|
||||||
href: `#!/devices/?${m.buildQueryString({
|
href: `#!/devices/?${m.buildQueryString({
|
||||||
filter: memoizedStringify(slice["filter"]),
|
filter: slice.filter.toString(),
|
||||||
})}`,
|
})}`,
|
||||||
},
|
},
|
||||||
slice["count"]["value"] || 0,
|
slice["count"]["value"] || 0,
|
||||||
@@ -67,7 +63,7 @@ function drawChart(chartData): Children {
|
|||||||
paths.push(
|
paths.push(
|
||||||
m("path.stroke-white stroke-1", {
|
m("path.stroke-white stroke-1", {
|
||||||
d: sketch,
|
d: sketch,
|
||||||
fill: store.evaluateExpression(slice["color"], null),
|
fill: slice.color,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -81,7 +77,7 @@ function drawChart(chartData): Children {
|
|||||||
"a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden",
|
"a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden",
|
||||||
{
|
{
|
||||||
"xlink:href": `#!/devices/?${m.buildQueryString({
|
"xlink:href": `#!/devices/?${m.buildQueryString({
|
||||||
filter: memoizedStringify(slice["filter"]),
|
filter: slice.filter.toString(),
|
||||||
})}`,
|
})}`,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -137,10 +133,21 @@ function drawChart(chartData): Children {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
chart: {
|
||||||
|
label: string;
|
||||||
|
slices: {
|
||||||
|
label: string;
|
||||||
|
filter: Expression;
|
||||||
|
color: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return drawChart(vnode.attrs["chart"]);
|
return drawChart(vnode.attrs.chart);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-19
@@ -1,6 +1,6 @@
|
|||||||
import { ClosureComponent, Component, Children, Vnode } from "mithril";
|
import { ClosureComponent, Component, Children, Vnode } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
@@ -9,11 +9,9 @@ import putFormComponent from "./put-form-component.ts";
|
|||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
const memoizedParse = memoize((str) => Expression.parse(str));
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
@@ -27,10 +25,22 @@ const attributes = [
|
|||||||
{ id: "provisionArgs", label: "Arguments", type: "textarea" },
|
{ id: "provisionArgs", label: "Arguments", type: "textarea" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("presets", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"presets",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -71,7 +81,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
|||||||
|
|
||||||
if (object.precondition) {
|
if (object.precondition) {
|
||||||
try {
|
try {
|
||||||
object.precondition = stringify(memoizedParse(object.precondition));
|
object.precondition = memoizedParse(object.precondition).toString();
|
||||||
} catch {
|
} catch {
|
||||||
return void resolve({
|
return void resolve({
|
||||||
precondition: "Precondition must be valid expression",
|
precondition: "Precondition must be valid expression",
|
||||||
@@ -134,7 +144,7 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
const cols = {};
|
const cols = {};
|
||||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
return `api/presets.csv?${m.buildQueryString({
|
return `api/presets.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
@@ -148,8 +158,11 @@ export function init(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let filter: Expression = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let sort: Record<string, number> = null;
|
||||||
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
return Promise.resolve({ filter, sort });
|
return Promise.resolve({ filter, sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +178,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/presets", ops);
|
m.route.set("/presets", ops);
|
||||||
}
|
}
|
||||||
@@ -197,10 +212,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/presets", ops);
|
m.route.set("/presets", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const presets = store.fetch("presets", filter, {
|
const presets = store.fetch("presets", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
@@ -220,7 +234,10 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"instances",
|
"instances",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const provisions = store.fetch("provisions", true);
|
const provisions = store.fetch(
|
||||||
|
"provisions",
|
||||||
|
new Expression.Literal(true),
|
||||||
|
);
|
||||||
if (provisions.fulfilled) {
|
if (provisions.fulfilled) {
|
||||||
for (const p of provisions.value) {
|
for (const p of provisions.value) {
|
||||||
userDefinedProvisions.add(p["_id"]);
|
userDefinedProvisions.add(p["_id"]);
|
||||||
|
|||||||
+30
-17
@@ -1,6 +1,6 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
@@ -9,12 +9,9 @@ import putFormComponent from "./put-form-component.ts";
|
|||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { loadCodeMirror } from "./dynamic-loader.ts";
|
import { loadCodeMirror } from "./dynamic-loader.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
@@ -22,10 +19,22 @@ const attributes = [
|
|||||||
{ id: "script", label: "Script", type: "code" },
|
{ id: "script", label: "Script", type: "code" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("provisions", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"provisions",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
const cols = {};
|
const cols = {};
|
||||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
return `api/provisions.csv?${m.buildQueryString({
|
return `api/provisions.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
@@ -118,8 +127,11 @@ export function init(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let filter: Expression = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let sort: Record<string, number> = null;
|
||||||
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
loadCodeMirror()
|
loadCodeMirror()
|
||||||
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/provisions", ops);
|
m.route.set("/provisions", ops);
|
||||||
}
|
}
|
||||||
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/provisions", ops);
|
m.route.set("/provisions", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const provisions = store.fetch("provisions", filter, {
|
const provisions = store.fetch("provisions", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
|||||||
+150
-72
@@ -1,51 +1,103 @@
|
|||||||
import config from "./config.ts";
|
import { filters } from "./config.ts";
|
||||||
import { Expression } from "../lib/types.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { encodeTag } from "../lib/util.ts";
|
import { encodeTag } from "../lib/util.ts";
|
||||||
|
import Path from "../lib/common/path.ts";
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
devices: {},
|
devices: {},
|
||||||
faults: {
|
faults: {
|
||||||
Device: { parameter: ["PARAM", "device"], type: "string" },
|
Device: {
|
||||||
Channel: { parameter: ["PARAM", "channel"], type: "string" },
|
parameter: new Expression.Parameter(Path.parse("device")),
|
||||||
Code: { parameter: ["PARAM", "code"], type: "string" },
|
type: "string",
|
||||||
Retries: { parameter: ["PARAM", "retries"], type: "number" },
|
},
|
||||||
Timestamp: { parameter: ["PARAM", "timestamp"], type: "timestamp" },
|
Channel: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("channel")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Code: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("code")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Retries: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("retries")),
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Timestamp: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("timestamp")),
|
||||||
|
type: "timestamp",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
presets: {
|
presets: {
|
||||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
ID: {
|
||||||
Channel: { parameter: ["PARAM", "channel"], type: "string" },
|
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||||
Weight: { parameter: ["PARAM", "weight"], type: "number" },
|
type: "string",
|
||||||
|
},
|
||||||
|
Channel: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("channel")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Weight: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("weight")),
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
provisions: {
|
provisions: {
|
||||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
ID: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
virtualParameters: {
|
virtualParameters: {
|
||||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
ID: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
ID: {
|
||||||
Type: { parameter: ["PARAM", "metadata.fileType"], type: "string" },
|
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||||
OUI: { parameter: ["PARAM", "metadata.oui"], type: "string" },
|
type: "string",
|
||||||
|
},
|
||||||
|
Type: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("metadata.fileType")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
OUI: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("metadata.oui")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
"Product class": {
|
"Product class": {
|
||||||
parameter: ["PARAM", "metadata.productClass"],
|
parameter: new Expression.Parameter(Path.parse("metadata.productClass")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Version: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("metadata.version")),
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
Version: { parameter: ["PARAM", "metadata.version"], type: "string" },
|
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
Role: { parameter: ["PARAM", "role"], type: "string" },
|
Role: {
|
||||||
Resource: { parameter: ["PARAM", "resource"], type: "string" },
|
parameter: new Expression.Parameter(Path.parse("role")),
|
||||||
Access: { parameter: ["PARAM", "access"], type: "number" },
|
type: "string",
|
||||||
|
},
|
||||||
|
Resource: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("resource")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
Access: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("access")),
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
Username: {
|
||||||
|
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: { Username: { parameter: ["PARAM", "_id"], type: "string" } },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const v of Object.values(
|
for (const v of filters) {
|
||||||
config.ui.filters as Record<
|
|
||||||
string,
|
|
||||||
{ label: string; parameter: string; type: string }
|
|
||||||
>,
|
|
||||||
)) {
|
|
||||||
resources.devices[v.label] = {
|
resources.devices[v.label] = {
|
||||||
parameter: v.parameter,
|
parameter: v.parameter,
|
||||||
type: (v.type || "").split(",").map((s) => s.trim()),
|
type: (v.type || "").split(",").map((s) => s.trim()),
|
||||||
@@ -70,7 +122,7 @@ function queryNumber(param: Expression, value: string): Expression {
|
|||||||
const v = parseInt(value);
|
const v = parseInt(value);
|
||||||
if (v !== +value) return null;
|
if (v !== +value) return null;
|
||||||
|
|
||||||
return [op, param, v];
|
return new Expression.Binary(op, param, new Expression.Literal(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryTimestamp(param: Expression, value: string): Expression {
|
function queryTimestamp(param: Expression, value: string): Expression {
|
||||||
@@ -86,52 +138,71 @@ function queryTimestamp(param: Expression, value: string): Expression {
|
|||||||
let v = parseInt(value);
|
let v = parseInt(value);
|
||||||
if (v !== +value) v = Date.parse(value);
|
if (v !== +value) v = Date.parse(value);
|
||||||
if (isNaN(v)) return null;
|
if (isNaN(v)) return null;
|
||||||
return [op, param, v];
|
return new Expression.Binary(op, param, new Expression.Literal(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryString(param: Expression, value: string): Expression {
|
function queryString(param: Expression, value: string): Expression {
|
||||||
return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()];
|
return new Expression.Binary(
|
||||||
|
"LIKE",
|
||||||
|
new Expression.FunctionCall("LOWER", [param]),
|
||||||
|
new Expression.Literal(value.toLowerCase()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryStringCaseSensitive(
|
function queryStringCaseSensitive(
|
||||||
param: Expression,
|
param: Expression,
|
||||||
value: string,
|
value: string,
|
||||||
): Expression {
|
): Expression {
|
||||||
return ["LIKE", param, value];
|
return new Expression.Binary("LIKE", param, new Expression.Literal(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryStringMonoCase(param: Expression, value: string): Expression {
|
function queryStringMonoCase(param: Expression, value: string): Expression {
|
||||||
return [
|
return Expression.or(
|
||||||
"OR",
|
new Expression.Binary(
|
||||||
["LIKE", param, value.toLowerCase()],
|
"LIKE",
|
||||||
["LIKE", param, value.toUpperCase()],
|
param,
|
||||||
];
|
new Expression.Literal(value.toLowerCase()),
|
||||||
|
),
|
||||||
|
new Expression.Binary(
|
||||||
|
"LIKE",
|
||||||
|
param,
|
||||||
|
new Expression.Literal(value.toUpperCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryMac(param: Expression, value: string): Expression {
|
function queryMac(param: Expression, value: string): Expression {
|
||||||
value = value.replace(/[^a-f0-9]/gi, "").toLowerCase();
|
value = value.replace(/[^a-f0-9]/gi, "").toLowerCase();
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
if (value.length === 12) {
|
if (value.length === 12) {
|
||||||
return [
|
value = value.replace(/(..)(?!$)/g, "$1:");
|
||||||
"OR",
|
return Expression.or(
|
||||||
["=", param, value.replace(/(..)(?!$)/g, "$1:").toLowerCase()],
|
new Expression.Binary(
|
||||||
["=", param, value.replace(/(..)(?!$)/g, "$1:").toUpperCase()],
|
"=",
|
||||||
];
|
param,
|
||||||
|
new Expression.Literal(value.toLowerCase()),
|
||||||
|
),
|
||||||
|
new Expression.Binary(
|
||||||
|
"=",
|
||||||
|
param,
|
||||||
|
new Expression.Literal(value.toUpperCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
param = new Expression.FunctionCall("LOWER", [param]);
|
||||||
"OR",
|
return Expression.or(
|
||||||
[
|
new Expression.Binary(
|
||||||
"LIKE",
|
"LIKE",
|
||||||
["FUNC", "LOWER", param],
|
param,
|
||||||
`%${value.replace(/(..)(?!$)/g, "$1:")}%`,
|
new Expression.Literal(`%${value.replace(/(..)(?!$)/g, "$1:")}%`),
|
||||||
],
|
),
|
||||||
[
|
new Expression.Binary(
|
||||||
"LIKE",
|
"LIKE",
|
||||||
["FUNC", "LOWER", param],
|
param,
|
||||||
`%${value.replace(/(.)(.)/g, "$1:$2")}%`,
|
new Expression.Literal(`%${value.replace(/(.)(.)/g, "$1:$2")}%`),
|
||||||
],
|
),
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryMacWildcard(param: Expression, value: string): Expression {
|
function queryMacWildcard(param: Expression, value: string): Expression {
|
||||||
@@ -143,23 +214,32 @@ function queryMacWildcard(param: Expression, value: string): Expression {
|
|||||||
p.replace(/(.)(.)/gi, "$1:$2"),
|
p.replace(/(.)(.)/gi, "$1:$2"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res = new Set();
|
const set = new Set<string>();
|
||||||
for (let i = 0; i < 2 ** groups.length; ++i) {
|
for (let i = 0; i < 2 ** groups.length; ++i) {
|
||||||
const r = groups.map((g, j) => g[(i >> j) & 1]).join("%");
|
const r = groups.map((g, j) => g[(i >> j) & 1]).join("%");
|
||||||
if (/^[a-f0-9]:/i.test(r) || /:[a-f0-9]$/i.test(r)) continue;
|
if (/^[a-f0-9]:/i.test(r) || /:[a-f0-9]$/i.test(r)) continue;
|
||||||
res.add(r.toLocaleLowerCase());
|
set.add(r.toLocaleLowerCase());
|
||||||
res.add(r.toUpperCase());
|
set.add(r.toUpperCase());
|
||||||
}
|
}
|
||||||
if (!res.size) return queryStringMonoCase(param, value);
|
if (!set.size) return queryStringMonoCase(param, value);
|
||||||
|
|
||||||
const clauses = [...res].map((r) => ["LIKE", param, r]);
|
let res: Expression = new Expression.Literal(false);
|
||||||
if (clauses.length === 1) return clauses[0];
|
for (const s of set) {
|
||||||
return ["OR", ...clauses];
|
res = Expression.or(
|
||||||
|
res,
|
||||||
|
new Expression.Binary("LIKE", param, new Expression.Literal(s)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryTag(tag: string): Expression {
|
function queryTag(tag: string): Expression {
|
||||||
const t = encodeTag(tag);
|
const t = encodeTag(tag);
|
||||||
return ["IS NOT NULL", ["PARAM", `Tags.${t}`]];
|
return new Expression.Unary(
|
||||||
|
"IS NOT NULL",
|
||||||
|
new Expression.Parameter(Path.parse(`Tags.${t}`)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTip(resource: string, label: string): string {
|
export function getTip(resource: string, label: string): string {
|
||||||
@@ -214,21 +294,21 @@ export function unpack(
|
|||||||
if (!resources[resource]) return null;
|
if (!resources[resource]) return null;
|
||||||
const type = resources[resource][label].type;
|
const type = resources[resource][label].type;
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
const res: Expression = ["OR"];
|
let res: Expression = new Expression.Literal(false);
|
||||||
|
|
||||||
if (type.length === 0 || type.includes("number")) {
|
if (type.length === 0 || type.includes("number")) {
|
||||||
const q = queryNumber(resources[resource][label].parameter, value);
|
const q = queryNumber(resources[resource][label].parameter, value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.length === 0 || type.includes("string")) {
|
if (type.length === 0 || type.includes("string")) {
|
||||||
const q = queryString(resources[resource][label].parameter, value);
|
const q = queryString(resources[resource][label].parameter, value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.length === 0 || type.includes("timestamp")) {
|
if (type.length === 0 || type.includes("timestamp")) {
|
||||||
const q = queryTimestamp(resources[resource][label].parameter, value);
|
const q = queryTimestamp(resources[resource][label].parameter, value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.includes("string-casesensitive")) {
|
if (type.includes("string-casesensitive")) {
|
||||||
@@ -236,30 +316,28 @@ export function unpack(
|
|||||||
resources[resource][label].parameter,
|
resources[resource][label].parameter,
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.includes("string-monocase")) {
|
if (type.includes("string-monocase")) {
|
||||||
const q = queryStringMonoCase(resources[resource][label].parameter, value);
|
const q = queryStringMonoCase(resources[resource][label].parameter, value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.includes("mac")) {
|
if (type.includes("mac")) {
|
||||||
const q = queryMac(resources[resource][label].parameter, value);
|
const q = queryMac(resources[resource][label].parameter, value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.includes("mac-wildcard")) {
|
if (type.includes("mac-wildcard")) {
|
||||||
const q = queryMacWildcard(resources[resource][label].parameter, value);
|
const q = queryMacWildcard(resources[resource][label].parameter, value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.includes("tag")) {
|
if (type.includes("tag")) {
|
||||||
const q = queryTag(value);
|
const q = queryTag(value);
|
||||||
if (q) res.push(q);
|
if (q) res = Expression.or(res, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.length <= 1) return null;
|
return res;
|
||||||
else if (res.length === 2) return res[1];
|
|
||||||
else return res;
|
|
||||||
}
|
}
|
||||||
+85
-41
@@ -1,8 +1,8 @@
|
|||||||
import m from "mithril";
|
import m from "mithril";
|
||||||
import { stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { or, and, evaluate } from "../lib/common/expression/util.ts";
|
import Path from "../lib/common/path.ts";
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
import { Expression, Task } from "../lib/types.ts";
|
import { Task } from "../lib/types.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
import { configSnapshot, genieacsVersion } from "./config.ts";
|
import { configSnapshot, genieacsVersion } from "./config.ts";
|
||||||
import { QueueTask } from "./task-queue.ts";
|
import { QueueTask } from "./task-queue.ts";
|
||||||
@@ -14,7 +14,30 @@ import {
|
|||||||
toBookmark,
|
toBookmark,
|
||||||
} from "../lib/common/expression/pagination.ts";
|
} from "../lib/common/expression/pagination.ts";
|
||||||
|
|
||||||
const memoizedStringify = memoize(stringify);
|
function evaluate(exp: Expression, timestamp: number): Expression;
|
||||||
|
function evaluate(
|
||||||
|
exp: Expression,
|
||||||
|
timestamp: number,
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
): Expression.Literal;
|
||||||
|
function evaluate(
|
||||||
|
exp: Expression,
|
||||||
|
timestamp: number,
|
||||||
|
obj?: Record<string, unknown>,
|
||||||
|
): Expression {
|
||||||
|
return exp.evaluate((e) => {
|
||||||
|
if (e instanceof Expression.Literal) return e;
|
||||||
|
else if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(timestamp);
|
||||||
|
} else if (e instanceof Expression.Parameter && obj) {
|
||||||
|
let v = obj[e.path.toString()];
|
||||||
|
if (v == null) return new Expression.Literal(null);
|
||||||
|
if (typeof v === "object") v = v["value"]?.[0];
|
||||||
|
return new Expression.Literal(v as any);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
const memoizedEvaluate = memoize(evaluate);
|
const memoizedEvaluate = memoize(evaluate);
|
||||||
|
|
||||||
let fulfillTimestamp = 0;
|
let fulfillTimestamp = 0;
|
||||||
@@ -59,7 +82,7 @@ for (const r of [
|
|||||||
objects: new Map(),
|
objects: new Map(),
|
||||||
count: new Map(),
|
count: new Map(),
|
||||||
fetch: new Map(),
|
fetch: new Map(),
|
||||||
combinedFilter: null,
|
combinedFilter: new Expression.Literal(false) as Expression,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,13 +238,11 @@ export async function xhrRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function unpackExpression(exp: Expression): Expression {
|
export function unpackExpression(exp: Expression): Expression {
|
||||||
if (!Array.isArray(exp)) return exp;
|
return memoizedEvaluate(exp, fulfillTimestamp + clockSkew);
|
||||||
const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
|
|
||||||
return e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function count(resourceType: string, filter: Expression): QueryResponse {
|
export function count(resourceType: string, filter: Expression): QueryResponse {
|
||||||
const filterStr = memoizedStringify(filter);
|
const filterStr = filter.toString();
|
||||||
let queryResponse = resources[resourceType].count.get(filterStr);
|
let queryResponse = resources[resourceType].count.get(filterStr);
|
||||||
if (queryResponse) return queryResponse;
|
if (queryResponse) return queryResponse;
|
||||||
|
|
||||||
@@ -271,7 +292,8 @@ function compareFunction(sort: {
|
|||||||
function findMatches(resourceType, filter, sort, limit): any[] {
|
function findMatches(resourceType, filter, sort, limit): any[] {
|
||||||
let value = [];
|
let value = [];
|
||||||
for (const obj of resources[resourceType].objects.values())
|
for (const obj of resources[resourceType].objects.values())
|
||||||
if (evaluate(filter, obj, fulfillTimestamp + clockSkew)) value.push(obj);
|
if (evaluate(filter, fulfillTimestamp + clockSkew, obj).value)
|
||||||
|
value.push(obj);
|
||||||
|
|
||||||
value = value.sort(compareFunction(sort));
|
value = value.sort(compareFunction(sort));
|
||||||
if (limit) value = value.slice(0, limit);
|
if (limit) value = value.slice(0, limit);
|
||||||
@@ -284,7 +306,6 @@ export function fetch(
|
|||||||
filter: Expression,
|
filter: Expression,
|
||||||
options: { limit?: number; sort?: { [param: string]: number } } = {},
|
options: { limit?: number; sort?: { [param: string]: number } } = {},
|
||||||
): QueryResponse {
|
): QueryResponse {
|
||||||
const filterStr = memoizedStringify(filter);
|
|
||||||
const sort = Object.assign({}, options.sort);
|
const sort = Object.assign({}, options.sort);
|
||||||
|
|
||||||
const limit = options.limit || 0;
|
const limit = options.limit || 0;
|
||||||
@@ -292,7 +313,7 @@ export function fetch(
|
|||||||
sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1;
|
sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1;
|
||||||
else sort["_id"] = sort["_id"] || 1;
|
else sort["_id"] = sort["_id"] || 1;
|
||||||
|
|
||||||
const key = `${filterStr}:${limit}:${JSON.stringify(sort)}`;
|
const key = `${filter.toString()}:${limit}:${JSON.stringify(sort)}`;
|
||||||
let queryResponse = resources[resourceType].fetch.get(key);
|
let queryResponse = resources[resourceType].fetch.get(key);
|
||||||
if (queryResponse) return queryResponse;
|
if (queryResponse) return queryResponse;
|
||||||
|
|
||||||
@@ -308,7 +329,10 @@ export function fetch(
|
|||||||
);
|
);
|
||||||
const matches = findMatches(resourceType, satisfied, sort, limit);
|
const matches = findMatches(resourceType, satisfied, sort, limit);
|
||||||
queries.value.set(queryResponse, matches);
|
queries.value.set(queryResponse, matches);
|
||||||
if (!diff || (limit && matches.length >= limit))
|
if (
|
||||||
|
(diff instanceof Expression.Literal && !diff.value) ||
|
||||||
|
(limit && matches.length >= limit)
|
||||||
|
)
|
||||||
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
||||||
else queries.unsatisfied.set(queryResponse, diff);
|
else queries.unsatisfied.set(queryResponse, diff);
|
||||||
return queryResponse;
|
return queryResponse;
|
||||||
@@ -336,7 +360,7 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
url:
|
url:
|
||||||
`api/${resourceType}/?` +
|
`api/${resourceType}/?` +
|
||||||
m.buildQueryString({
|
m.buildQueryString({
|
||||||
filter: memoizedStringify(filter),
|
filter: filter.toString(),
|
||||||
}),
|
}),
|
||||||
extract: (xhr) => {
|
extract: (xhr) => {
|
||||||
if (xhr.status === 403) throw new Error("Not authorized");
|
if (xhr.status === 403) throw new Error("Not authorized");
|
||||||
@@ -393,7 +417,7 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
url:
|
url:
|
||||||
`api/${resourceType}/?` +
|
`api/${resourceType}/?` +
|
||||||
m.buildQueryString({
|
m.buildQueryString({
|
||||||
filter: memoizedStringify(filter),
|
filter: filter.toString(),
|
||||||
limit: 1,
|
limit: 1,
|
||||||
skip: limit - 1,
|
skip: limit - 1,
|
||||||
sort: JSON.stringify(sort),
|
sort: JSON.stringify(sort),
|
||||||
@@ -419,16 +443,19 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
let updated = false;
|
let updated = false;
|
||||||
const allPromises2 = [];
|
const allPromises2 = [];
|
||||||
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
|
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
|
||||||
let combinedFilter = null;
|
let combinedFilter = new Expression.Literal(false) as Expression;
|
||||||
|
|
||||||
for (const queryResponse of toFetch) {
|
for (const queryResponse of toFetch) {
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
|
filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark)
|
if (bookmark)
|
||||||
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
filter = Expression.and(
|
||||||
combinedFilter = or(combinedFilter, filter);
|
filter,
|
||||||
|
bookmarkToExpression(bookmark, sort),
|
||||||
|
);
|
||||||
|
combinedFilter = Expression.or(combinedFilter, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [union, diff] = unionDiff(
|
const [union, diff] = unionDiff(
|
||||||
@@ -436,19 +463,18 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
combinedFilter,
|
combinedFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!diff) {
|
if (diff instanceof Expression.Literal && !diff.value) {
|
||||||
for (const queryResponse of toFetch) {
|
for (const queryResponse of toFetch) {
|
||||||
let filter = queries.filter.get(queryResponse);
|
let filter = queries.filter.get(queryResponse);
|
||||||
filter = memoizedEvaluate(
|
filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
|
||||||
filter,
|
|
||||||
null,
|
|
||||||
fulfillTimestamp + clockSkew,
|
|
||||||
);
|
|
||||||
const limit = queries.limit.get(queryResponse);
|
const limit = queries.limit.get(queryResponse);
|
||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark)
|
if (bookmark)
|
||||||
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
filter = Expression.and(
|
||||||
|
filter,
|
||||||
|
bookmarkToExpression(bookmark, sort),
|
||||||
|
);
|
||||||
|
|
||||||
queries.value.set(
|
queries.value.set(
|
||||||
queryResponse,
|
queryResponse,
|
||||||
@@ -462,7 +488,11 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let deleted = new Set<string>();
|
let deleted = new Set<string>();
|
||||||
if (!resources[resourceType].combinedFilter)
|
if (
|
||||||
|
resources[resourceType].combinedFilter instanceof
|
||||||
|
Expression.Literal &&
|
||||||
|
!resources[resourceType].combinedFilter.value
|
||||||
|
)
|
||||||
deleted = new Set(resources[resourceType].objects.keys());
|
deleted = new Set(resources[resourceType].objects.keys());
|
||||||
|
|
||||||
const combinedFilterDiff = diff;
|
const combinedFilterDiff = diff;
|
||||||
@@ -474,15 +504,12 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
url:
|
url:
|
||||||
`api/${resourceType}/?` +
|
`api/${resourceType}/?` +
|
||||||
m.buildQueryString({
|
m.buildQueryString({
|
||||||
filter: memoizedStringify(combinedFilterDiff),
|
filter: combinedFilterDiff.toString(),
|
||||||
}),
|
}),
|
||||||
background: false,
|
background: false,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
for (const r of res as any[]) {
|
for (const r of res as any[]) {
|
||||||
const id =
|
const id = r["DeviceID.ID"] ?? r["_id"];
|
||||||
resourceType === "devices"
|
|
||||||
? r["DeviceID.ID"].value[0]
|
|
||||||
: r["_id"];
|
|
||||||
resources[resourceType].objects.set(id, r);
|
resources[resourceType].objects.set(id, r);
|
||||||
deleted.delete(id);
|
deleted.delete(id);
|
||||||
}
|
}
|
||||||
@@ -490,7 +517,8 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
for (const d of deleted) {
|
for (const d of deleted) {
|
||||||
const obj = resources[resourceType].objects.get(d);
|
const obj = resources[resourceType].objects.get(d);
|
||||||
if (
|
if (
|
||||||
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew)
|
evaluate(combinedFilterDiff, fulfillTimestamp + clockSkew, obj)
|
||||||
|
.value
|
||||||
)
|
)
|
||||||
resources[resourceType].objects.delete(d);
|
resources[resourceType].objects.delete(d);
|
||||||
}
|
}
|
||||||
@@ -502,7 +530,10 @@ export function fulfill(accessTimestamp: number): void {
|
|||||||
const bookmark = queries.bookmark.get(queryResponse);
|
const bookmark = queries.bookmark.get(queryResponse);
|
||||||
const sort = queries.sort.get(queryResponse);
|
const sort = queries.sort.get(queryResponse);
|
||||||
if (bookmark)
|
if (bookmark)
|
||||||
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
filter = Expression.and(
|
||||||
|
filter,
|
||||||
|
bookmarkToExpression(bookmark, sort),
|
||||||
|
);
|
||||||
|
|
||||||
queries.value.set(
|
queries.value.set(
|
||||||
queryResponse,
|
queryResponse,
|
||||||
@@ -530,7 +561,7 @@ export function setTimestamp(t: number): void {
|
|||||||
if (t > fulfillTimestamp) {
|
if (t > fulfillTimestamp) {
|
||||||
fulfillTimestamp = t;
|
fulfillTimestamp = t;
|
||||||
for (const resource of Object.values(resources))
|
for (const resource of Object.values(resources))
|
||||||
resource.combinedFilter = null;
|
resource.combinedFilter = new Expression.Literal(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,23 +638,32 @@ export function putResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function queryConfig(pattern = "%"): Promise<any[]> {
|
export function queryConfig(pattern = "%"): Promise<any[]> {
|
||||||
const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]);
|
const filter = new Expression.Binary(
|
||||||
|
"LIKE",
|
||||||
|
new Expression.Parameter(Path.parse("_id")),
|
||||||
|
new Expression.Literal(pattern),
|
||||||
|
);
|
||||||
return xhrRequest({
|
return xhrRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `api/config/?${m.buildQueryString({ filter: filter })}`,
|
url: `api/config/?${m.buildQueryString({ filter: filter.toString() })}`,
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resourceExists(resource: string, id: string): Promise<number> {
|
export function resourceExists(resource: string, id: string): Promise<number> {
|
||||||
const param = resource === "devices" ? "DeviceID.ID" : "_id";
|
const param = resource === "devices" ? "DeviceID.ID" : "_id";
|
||||||
const filter = ["=", ["PARAM", param], id];
|
const filter = new Expression.Binary(
|
||||||
|
"=",
|
||||||
|
new Expression.Parameter(Path.parse(param)),
|
||||||
|
new Expression.Literal(id),
|
||||||
|
);
|
||||||
|
|
||||||
return xhrRequest({
|
return xhrRequest({
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
url:
|
url:
|
||||||
`api/${resource}/?` +
|
`api/${resource}/?` +
|
||||||
m.buildQueryString({
|
m.buildQueryString({
|
||||||
filter: memoizedStringify(filter),
|
filter: filter.toString(),
|
||||||
}),
|
}),
|
||||||
extract: (xhr) => {
|
extract: (xhr) => {
|
||||||
if (xhr.status === 403) throw new Error("Not authorized");
|
if (xhr.status === 403) throw new Error("Not authorized");
|
||||||
@@ -636,12 +676,16 @@ export function resourceExists(resource: string, id: string): Promise<number> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function evaluateExpression(exp: Expression): Expression;
|
||||||
export function evaluateExpression(
|
export function evaluateExpression(
|
||||||
exp: Expression,
|
exp: Expression,
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
|
): Expression.Literal;
|
||||||
|
export function evaluateExpression(
|
||||||
|
exp: Expression,
|
||||||
|
obj?: Record<string, unknown>,
|
||||||
): Expression {
|
): Expression {
|
||||||
if (!Array.isArray(exp)) return exp;
|
return memoizedEvaluate(exp, fulfillTimestamp + clockSkew, obj);
|
||||||
return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changePassword(
|
export function changePassword(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as store from "./store.ts";
|
|||||||
import { yaml } from "./dynamic-loader.ts";
|
import { yaml } from "./dynamic-loader.ts";
|
||||||
import * as configFunctions from "./config-functions.ts";
|
import * as configFunctions from "./config-functions.ts";
|
||||||
import codeEditorComponent from "./code-editor-component.ts";
|
import codeEditorComponent from "./code-editor-component.ts";
|
||||||
import { parse } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
|
||||||
function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -27,7 +27,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try parse to ensure valid expressions
|
// Try parse to ensure valid expressions
|
||||||
for (const v of Object.values(updated)) parse(v as string);
|
for (const v of Object.values(updated)) Expression.parse(v as string);
|
||||||
|
|
||||||
store
|
store
|
||||||
.queryConfig(`${prefix.join(".")}.%`)
|
.queryConfig(`${prefix.join(".")}.%`)
|
||||||
|
|||||||
+34
-18
@@ -1,6 +1,6 @@
|
|||||||
import { Children, ClosureComponent, Component } from "mithril";
|
import { Children, ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
import memoize from "../lib/common/memoize.ts";
|
import memoize from "../lib/common/memoize.ts";
|
||||||
@@ -8,13 +8,10 @@ import putFormComponent from "./put-form-component.ts";
|
|||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import changePasswordComponent from "./change-password-component.ts";
|
import changePasswordComponent from "./change-password-component.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
@@ -22,10 +19,22 @@ const attributes = [
|
|||||||
{ id: "roles", label: "Roles", type: "multi", options: [] },
|
{ id: "roles", label: "Roles", type: "multi", options: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("users", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"users",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -120,7 +129,7 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
|
|
||||||
return `api/users.csv?${m.buildQueryString({
|
return `api/users.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
@@ -134,8 +143,11 @@ export function init(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let filter: Expression = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let sort: Record<string, number> = null;
|
||||||
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
return Promise.resolve({ filter, sort });
|
return Promise.resolve({ filter, sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +163,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/users", ops);
|
m.route.set("/users", ops);
|
||||||
}
|
}
|
||||||
@@ -176,10 +190,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/users", ops);
|
m.route.set("/users", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const users = store.fetch("users", filter, {
|
const users = store.fetch("users", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
@@ -189,7 +202,10 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
const count = store.count("users", filter);
|
const count = store.count("users", filter);
|
||||||
|
|
||||||
// Getting the roles
|
// Getting the roles
|
||||||
const permissions = store.fetch("permissions", true);
|
const permissions = store.fetch(
|
||||||
|
"permissions",
|
||||||
|
new Expression.Literal(true),
|
||||||
|
);
|
||||||
if (permissions.fulfilled) {
|
if (permissions.fulfilled) {
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
if (attr.id === "roles")
|
if (attr.id === "roles")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ClosureComponent, Component, Children } from "mithril";
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import config from "./config.ts";
|
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||||
import filterComponent from "./filter-component.ts";
|
import filterComponent from "./filter-component.ts";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
import * as notifications from "./notifications.ts";
|
||||||
@@ -9,12 +9,9 @@ import putFormComponent from "./put-form-component.ts";
|
|||||||
import indexTableComponent from "./index-table-component.ts";
|
import indexTableComponent from "./index-table-component.ts";
|
||||||
import * as overlay from "./overlay.ts";
|
import * as overlay from "./overlay.ts";
|
||||||
import * as smartQuery from "./smart-query.ts";
|
import * as smartQuery from "./smart-query.ts";
|
||||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
import Expression from "../lib/common/expression.ts";
|
||||||
import { loadCodeMirror } from "./dynamic-loader.ts";
|
import { loadCodeMirror } from "./dynamic-loader.ts";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
|
||||||
const memoizedJsonParse = memoize(JSON.parse);
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
@@ -22,10 +19,22 @@ const attributes = [
|
|||||||
{ id: "script", label: "Script", type: "code" },
|
{ id: "script", label: "Script", type: "code" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const unpackSmartQuery = memoize((query) => {
|
const unpackSmartQuery = memoize((query: Expression) => {
|
||||||
return map(query, (e) => {
|
return query.evaluate((e) => {
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
if (e instanceof Expression.FunctionCall) {
|
||||||
return smartQuery.unpack("virtualParameters", e[2], e[3]);
|
if (e.name === "Q") {
|
||||||
|
if (
|
||||||
|
e.args[0] instanceof Expression.Literal &&
|
||||||
|
e.args[1] instanceof Expression.Literal
|
||||||
|
) {
|
||||||
|
return smartQuery.unpack(
|
||||||
|
"virtualParameters",
|
||||||
|
e.args[0].value as string,
|
||||||
|
e.args[1].value as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
|
|||||||
const cols = {};
|
const cols = {};
|
||||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||||
return `api/virtualParameters.csv?${m.buildQueryString({
|
return `api/virtualParameters.csv?${m.buildQueryString({
|
||||||
filter: stringify(filter),
|
filter: filter.toString(),
|
||||||
columns: JSON.stringify(cols),
|
columns: JSON.stringify(cols),
|
||||||
})}`;
|
})}`;
|
||||||
});
|
});
|
||||||
@@ -118,8 +127,11 @@ export function init(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
let filter: Expression = null;
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
let sort: Record<string, number> = null;
|
||||||
|
if (args.hasOwnProperty("filter"))
|
||||||
|
filter = Expression.parse(args["filter"] as string);
|
||||||
|
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
loadCodeMirror()
|
loadCodeMirror()
|
||||||
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChanged(filter): void {
|
function onFilterChanged(filter): void {
|
||||||
const ops = { filter };
|
const ops = {};
|
||||||
|
if (!(filter instanceof Expression.Literal && filter.value))
|
||||||
|
ops["filter"] = filter.toString();
|
||||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
m.route.set("/virtualParameters", ops);
|
m.route.set("/virtualParameters", ops);
|
||||||
}
|
}
|
||||||
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
m.route.set("/virtualParameters", ops);
|
m.route.set("/virtualParameters", ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = vnode.attrs["filter"]
|
const filter = unpackSmartQuery(
|
||||||
? memoizedParse(vnode.attrs["filter"])
|
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||||
: true;
|
);
|
||||||
filter = unpackSmartQuery(filter);
|
|
||||||
|
|
||||||
const virtualParameters = store.fetch("virtualParameters", filter, {
|
const virtualParameters = store.fetch("virtualParameters", filter, {
|
||||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
|||||||
Reference in new issue
Block a user