Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4e1b297ae | ||
|
|
d445aa03cf | ||
|
|
f39e2ce7cf | ||
|
|
6f3ce6f7c8 | ||
|
|
75908e624f | ||
|
|
4a191afdec | ||
|
|
a7256620e0 | ||
|
|
bef840ce0a | ||
|
|
cc723c0b50 | ||
|
|
a2a6fd82f3 | ||
|
|
729beba0ed | ||
|
|
1b14387aa2 | ||
|
|
49cdd39617 | ||
|
|
4be39ed742 | ||
|
|
8ddb692370 | ||
|
|
fc80a7d487 | ||
|
|
f34f10edc7 | ||
|
|
a416f68b76 | ||
|
|
2b61c8f7b8 | ||
|
|
756912eeb1 | ||
|
|
5c4fc3263b | ||
|
|
307b9f57b0 | ||
|
|
0f6604ffbd | ||
|
|
f57a26c95b | ||
|
|
60e6d8b175 | ||
|
|
ab80a62c5c | ||
|
|
3134b63b68 | ||
|
|
2b1bca7dde |
No files matched your search
@@ -0,0 +1,28 @@
|
|||||||
|
# Dependências e artefatos de build
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Arquivos de ambiente (contêm segredos)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Logs e debug
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Artefatos de teste
|
||||||
|
test/*.js
|
||||||
|
|
||||||
|
# Docs geradas
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# Dados de desenvolvimento
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# ============================================================
|
||||||
|
# GenieACS — variáveis de ambiente
|
||||||
|
# Copie este arquivo para .env e ajuste os valores
|
||||||
|
# NUNCA faça commit do .env com dados reais em repositórios
|
||||||
|
# públicos.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ── MongoDB ─────────────────────────────────────────────────
|
||||||
|
# Quando usando docker-compose o host é o nome do serviço "mongo"
|
||||||
|
GENIEACS_MONGODB_CONNECTION_URL=mongodb://mongo:27017/genieacs
|
||||||
|
|
||||||
|
# ── Extensões ───────────────────────────────────────────────
|
||||||
|
GENIEACS_EXT_DIR=/opt/genieacs/config/ext
|
||||||
|
GENIEACS_EXT_TIMEOUT=3000
|
||||||
|
|
||||||
|
# ── Logs ────────────────────────────────────────────────────
|
||||||
|
# json | simple
|
||||||
|
GENIEACS_LOG_FORMAT=json
|
||||||
|
GENIEACS_ACCESS_LOG_FORMAT=json
|
||||||
|
|
||||||
|
# Para gravar logs em arquivo (opcional, senão vai para stdout/stderr)
|
||||||
|
# GENIEACS_CWMP_LOG_FILE=/opt/genieacs/logs/cwmp.log
|
||||||
|
# GENIEACS_CWMP_ACCESS_LOG_FILE=/opt/genieacs/logs/cwmp-access.log
|
||||||
|
# GENIEACS_NBI_LOG_FILE=/opt/genieacs/logs/nbi.log
|
||||||
|
# GENIEACS_NBI_ACCESS_LOG_FILE=/opt/genieacs/logs/nbi-access.log
|
||||||
|
# GENIEACS_FS_LOG_FILE=/opt/genieacs/logs/fs.log
|
||||||
|
# GENIEACS_FS_ACCESS_LOG_FILE=/opt/genieacs/logs/fs-access.log
|
||||||
|
# GENIEACS_UI_LOG_FILE=/opt/genieacs/logs/ui.log
|
||||||
|
# GENIEACS_UI_ACCESS_LOG_FILE=/opt/genieacs/logs/ui-access.log
|
||||||
|
|
||||||
|
# ── Debug (opcional) ────────────────────────────────────────
|
||||||
|
# GENIEACS_DEBUG_FILE=/opt/genieacs/logs/debug.yaml
|
||||||
|
# GENIEACS_DEBUG_FORMAT=yaml
|
||||||
|
|
||||||
|
# ── Worker processes (0 = 1 por CPU core) ───────────────────
|
||||||
|
GENIEACS_CWMP_WORKER_PROCESSES=1
|
||||||
|
GENIEACS_NBI_WORKER_PROCESSES=1
|
||||||
|
GENIEACS_FS_WORKER_PROCESSES=1
|
||||||
|
GENIEACS_UI_WORKER_PROCESSES=1
|
||||||
|
|
||||||
|
# ── Portas ──────────────────────────────────────────────────
|
||||||
|
GENIEACS_CWMP_PORT=7547
|
||||||
|
GENIEACS_NBI_PORT=7557
|
||||||
|
GENIEACS_FS_PORT=7567
|
||||||
|
GENIEACS_UI_PORT=3000
|
||||||
|
|
||||||
|
# ── Interface de bind (:: = todas as interfaces) ────────────
|
||||||
|
GENIEACS_CWMP_INTERFACE=::
|
||||||
|
GENIEACS_NBI_INTERFACE=::
|
||||||
|
GENIEACS_FS_INTERFACE=::
|
||||||
|
GENIEACS_UI_INTERFACE=::
|
||||||
|
|
||||||
|
# ── UI — JWT Secret (OBRIGATÓRIO trocar em produção!) ───────
|
||||||
|
# Mínimo 32 chars, máximo 64 chars
|
||||||
|
GENIEACS_UI_JWT_SECRET=mude-isto-em-producao-use-uma-string-aleatoria-longa
|
||||||
|
|
||||||
|
# ── FS — URL prefix (configure se CWMP e FS estiverem em ────
|
||||||
|
# hosts/IPs diferentes ou atrás de proxy)
|
||||||
|
# GENIEACS_FS_URL_PREFIX=http://seu-ip-publico:7567/
|
||||||
|
|
||||||
|
# ── SSL (opcional — passe o caminho para cert/key PEM) ──────
|
||||||
|
# GENIEACS_CWMP_SSL_CERT=/opt/genieacs/certs/cwmp.crt
|
||||||
|
# GENIEACS_CWMP_SSL_KEY=/opt/genieacs/certs/cwmp.key
|
||||||
|
# GENIEACS_NBI_SSL_CERT=/opt/genieacs/certs/nbi.crt
|
||||||
|
# GENIEACS_NBI_SSL_KEY=/opt/genieacs/certs/nbi.key
|
||||||
|
# GENIEACS_FS_SSL_CERT=/opt/genieacs/certs/fs.crt
|
||||||
|
# GENIEACS_FS_SSL_KEY=/opt/genieacs/certs/fs.key
|
||||||
|
# GENIEACS_UI_SSL_CERT=/opt/genieacs/certs/ui.crt
|
||||||
|
# GENIEACS_UI_SSL_KEY=/opt/genieacs/certs/ui.key
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"es2022": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"no-shadow": ["error", { "allow": ["err"] }],
|
|
||||||
"handle-callback-err": "error",
|
|
||||||
"prefer-arrow-callback": "error",
|
|
||||||
"no-buffer-constructor": "error",
|
|
||||||
"prefer-const": ["error", { "destructuring": "all" }],
|
|
||||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
|
||||||
"@typescript-eslint/no-use-before-define": [
|
|
||||||
"error",
|
|
||||||
{ "functions": false }
|
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": [
|
|
||||||
"error",
|
|
||||||
{ "allowExpressions": true }
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"no-prototype-builtins": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
GitHub Copilot Instructions: GenieACS -> UniFi UI Theme
|
||||||
|
You are an expert senior software engineer and frontend architect focused on UI/UX modernization, maintainability, and clean architecture. Your primary goal is to help modify the GenieACS user interface to closely resemble the UniFi Controller, while strictly ensuring the codebase remains easy to synchronize (via merge or rebase) with the original GenieACS upstream repository for bug fixes and core updates.
|
||||||
|
|
||||||
|
1. Core Principles (Non-Negotiable)
|
||||||
|
Sync-Friendly Architecture (Crucial): Write code defensively to prevent future merge conflicts with the original GenieACS project. Prefer extending, overriding, or wrapping components rather than heavily modifying deeply ingrained original core files.
|
||||||
|
|
||||||
|
Visual Fidelity (UniFi): Implement the clean, dashboard-centric, modern aesthetic of the UniFi Controller. Use comparable color palettes, typography, spacing, and network topology layouts.
|
||||||
|
|
||||||
|
Safety First: Never modify the underlying TR-069 communication logic, backend models, or database schemas unless explicitly requested. The focus is strictly on the presentation layer (View).
|
||||||
|
|
||||||
|
Minimal Footprint: When generating files, aim to isolate custom UI assets. Keep custom CSS/SCSS and new UI components in distinct directories or files whenever possible.
|
||||||
|
|
||||||
|
2. GenieACS Customization & Upstream Integration
|
||||||
|
CRITICAL: Whenever the user requests a modification to a page or component, you MUST prioritize the following strategies to keep the fork maintainable:
|
||||||
|
|
||||||
|
Style Overrides: Prefer creating custom stylesheets (e.g., unifi-theme.scss) to override existing GenieACS classes instead of changing inline styles or the original CSS files directly.
|
||||||
|
|
||||||
|
Component Wrapping: If an existing UI component needs significant structural changes, create a new "UniFi-styled" wrapper component that consumes the original GenieACS state/props, rather than gutting the original component.
|
||||||
|
|
||||||
|
Clear Demarcation: When modifying an original GenieACS file is unavoidable, clearly mark the beginning and end of the custom code with comments (e.g., // --- BEGIN UNIFI THEME --- and // --- END UNIFI THEME ---). This makes resolving Git merge conflicts significantly easier.
|
||||||
|
|
||||||
|
3. Code Generation Rules
|
||||||
|
A. Refactoring & Modification
|
||||||
|
Context Awareness: Read the surrounding code to understand the existing state management and API calls. Do not break the data flow between the GenieACS backend and the frontend.
|
||||||
|
|
||||||
|
Preserve Logic: When changing how data is displayed (e.g., changing a simple table to a UniFi-style card grid), ensure all original data points remain accessible or are intentionally hidden via CSS, rather than deleting the data-fetching logic.
|
||||||
|
|
||||||
|
B. New Features (UI Components)
|
||||||
|
Modularity: Build reusable UI components (Buttons, Cards, Navbars) that match the UniFi design system, so they can be applied globally across the GenieACS interface.
|
||||||
|
|
||||||
|
Responsiveness: Ensure all new UI elements are fully responsive, as network management dashboards are often viewed on various screen sizes.
|
||||||
|
|
||||||
|
C. Testing
|
||||||
|
Prioritize visual regression checks and ensure that frontend unit tests (if applicable) are updated to reflect structural DOM changes. Ensure the connection to the GenieACS API remains mocked and functional in tests.
|
||||||
|
|
||||||
|
4. Response Format
|
||||||
|
Plan First: Before generating code, briefly outline your strategy. Specifically state which files will be modified and how you plan to implement the change while minimizing upstream merge conflicts.
|
||||||
|
|
||||||
|
Complete Code: Output the full, functional block of code needed for the file. Do not use placeholders like // ... rest of code unless the file is massive and the change is highly localized.
|
||||||
|
|
||||||
|
Git Context: If a change requires creating a new file to override an old one, explain how to integrate it into the project's build system.
|
||||||
|
|
||||||
|
5. "Do No Harm" Checklist
|
||||||
|
Before outputting the final response, ask yourself:
|
||||||
|
|
||||||
|
Does this modification break the core GenieACS device management logic?
|
||||||
|
|
||||||
|
Will this change cause a massive merge conflict if the upstream repository updates this specific component? (If yes, rethink the approach).
|
||||||
|
|
||||||
|
Does the resulting UI genuinely reflect the UniFi Controller design language?
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
docs/_build
|
docs/_build
|
||||||
|
.env
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# AGENTS.md — GenieACS
|
||||||
|
|
||||||
|
GenieACS is a TR-069 Auto Configuration Server for remote management of CPE
|
||||||
|
devices (routers, modems, gateways). TypeScript codebase compiled with esbuild,
|
||||||
|
backed by MongoDB.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
Four services share a single MongoDB instance:
|
||||||
|
|
||||||
|
- **CWMP** (port 7547) — TR-069 protocol handler; manages device sessions
|
||||||
|
- **NBI** (port 7557) — Northbound REST API for external consumers
|
||||||
|
- **FS** (port 7567) — File server for firmware/config (GridFS-backed)
|
||||||
|
- **UI** (port 3000) — Web interface (Koa backend + Mithril.js SPA frontend)
|
||||||
|
|
||||||
|
Key subsystems: expression engine (`lib/common/expression/`) compiles a
|
||||||
|
Lisp-like DSL used for queries, config, and authorization; session engine
|
||||||
|
(`lib/session.ts`) drives CWMP interactions via declarations rather than
|
||||||
|
imperative RPCs; sandbox (`lib/sandbox.ts`) runs user-defined provision scripts
|
||||||
|
in `vm.Script` with deterministic replay.
|
||||||
|
|
||||||
|
Read `ARCHITECTURE.md` for a full map of the codebase when working on unfamiliar
|
||||||
|
areas. It covers service boundaries, the expression pipeline, the path system,
|
||||||
|
the CWMP session state machine, the database layer, and architectural
|
||||||
|
invariants.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `lib/` — Core server-side logic
|
||||||
|
- `lib/common/` — Shared code (runs in both Node.js and browser)
|
||||||
|
- `lib/db/` — MongoDB database layer
|
||||||
|
- `lib/ui/` — UI backend helpers
|
||||||
|
- `ui/` — Frontend SPA (Mithril.js)
|
||||||
|
- `bin/` — Service entry points (5 executables)
|
||||||
|
- `build/` — Build scripts (esbuild pipeline)
|
||||||
|
- `test/` — Unit tests (node:test)
|
||||||
|
- `docs/` — User docs (Sphinx/reStructuredText)
|
||||||
|
- `public/` — Static assets (favicon, logo)
|
||||||
|
|
||||||
|
## Build / Lint / Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # Production build (esbuild pipeline -> dist/)
|
||||||
|
NODE_ENV=development npm run build # Dev build (no minification)
|
||||||
|
npm run lint # Prettier + ESLint + tsc --noEmit in parallel
|
||||||
|
npm test # Compile tests with esbuild, run with node --test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running a Single Test File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
esbuild --log-level=warning --bundle --platform=node --target=node18 \
|
||||||
|
--packages=external --sourcemap=inline --outdir=test test/path.ts \
|
||||||
|
&& node --test --enable-source-maps test/path.js \
|
||||||
|
&& rm test/path.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running a Single Test Case
|
||||||
|
|
||||||
|
```bash
|
||||||
|
esbuild --log-level=warning --bundle --platform=node --target=node18 \
|
||||||
|
--packages=external --sourcemap=inline --outdir=test test/path.ts \
|
||||||
|
&& node --test --enable-source-maps --test-name-pattern="^parse$" test/path.js \
|
||||||
|
&& rm test/path.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint Sub-commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
prettier --prose-wrap always --write .
|
||||||
|
eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts'
|
||||||
|
tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Before Committing
|
||||||
|
|
||||||
|
Read `CONTRIBUTING.md` and ensure your changes comply with it. In particular:
|
||||||
|
|
||||||
|
- Run `npm run lint` and `npm test` and fix any failures.
|
||||||
|
- Follow the code style, naming, import, and comment conventions documented
|
||||||
|
there.
|
||||||
|
- Use the Conventional Commits format for commit messages.
|
||||||
+479
@@ -0,0 +1,479 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document describes the high-level architecture of GenieACS. If you want to
|
||||||
|
familiarize yourself with the codebase, you are in the right place.
|
||||||
|
|
||||||
|
## Bird's Eye View
|
||||||
|
|
||||||
|
GenieACS is a TR-069 Auto Configuration Server (ACS). It manages CPE devices
|
||||||
|
(routers, modems, gateways) using the CWMP protocol (TR-069). The system
|
||||||
|
consists of four network-facing services that share a MongoDB database:
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------+
|
||||||
|
CPE Devices --->| CWMP (7547)|-+
|
||||||
|
+------------+ |
|
||||||
|
+-----------+ | +---------+
|
||||||
|
CPE Devices --->| FS (7567) |--+---->| MongoDB |
|
||||||
|
+-----------+ | +---------+
|
||||||
|
+-----------+ |
|
||||||
|
OSS / Scripts ->| NBI (7557)|--+
|
||||||
|
+-----------+ |
|
||||||
|
+-----------+ |
|
||||||
|
Administrators->| UI (3000) |--+
|
||||||
|
+-----------+
|
||||||
|
```
|
||||||
|
|
||||||
|
- **CWMP** -- The core TR-069 protocol server. CPE devices connect here to
|
||||||
|
report their state and receive configuration instructions via SOAP/XML over
|
||||||
|
HTTP.
|
||||||
|
- **NBI** -- Northbound Interface. A REST API for external systems (OSS/BSS,
|
||||||
|
scripts, automation) to manage devices, tasks, presets, and configuration
|
||||||
|
programmatically.
|
||||||
|
- **FS** -- File Server. Serves firmware images and configuration files to CPE
|
||||||
|
devices during download operations.
|
||||||
|
- **UI** -- Web interface. A Koa-based backend serving a Mithril.js single-page
|
||||||
|
application for administrators to browse devices, manage configuration, and
|
||||||
|
trigger operations.
|
||||||
|
|
||||||
|
All four services follow the same process model: a primary process forks
|
||||||
|
configurable worker processes via Node.js `cluster` (see `cluster.ts`). Workers
|
||||||
|
connect to MongoDB and start an HTTP(S) server.
|
||||||
|
|
||||||
|
## Code Map
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/ Service entry points (5 executables)
|
||||||
|
lib/ All backend logic
|
||||||
|
common/ Shared utilities (expression engine, Path, errors)
|
||||||
|
expression.ts Expression class hierarchy (base + subclasses)
|
||||||
|
expression/ Expression parser, evaluator, normalizer, minimizer
|
||||||
|
cwmp/ CWMP-service-specific DB and caching
|
||||||
|
db/ Database layer (MongoDB collections, query synthesis)
|
||||||
|
ui/ UI-service-specific API, DB, and caching
|
||||||
|
types/ (empty, reserved)
|
||||||
|
ui/ Frontend SPA (Mithril.js)
|
||||||
|
components/ Reusable UI components (parameter, tags, ping, etc.)
|
||||||
|
css/ Stylesheets (vanilla CSS)
|
||||||
|
icons/ SVG icons (compiled into a sprite)
|
||||||
|
test/ Unit tests (Node.js native test runner)
|
||||||
|
build/ Build scripts (esbuild-based)
|
||||||
|
public/ Static assets (logo, favicon)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entry Points (`bin/`)
|
||||||
|
|
||||||
|
Each file in `bin/` bootstraps one service. They are structurally identical:
|
||||||
|
initialize logging, read config, fork workers in the primary process, connect to
|
||||||
|
MongoDB and start the HTTP server in each worker.
|
||||||
|
|
||||||
|
- `genieacs-cwmp.ts` -- CWMP service. Unique in that it disables HTTP keep-alive
|
||||||
|
(`keepAliveTimeout: 0`) and provides custom `onConnection` / `onClientError`
|
||||||
|
hooks for TR-069 session lifecycle management.
|
||||||
|
- `genieacs-nbi.ts` -- NBI service. Straightforward REST API server.
|
||||||
|
- `genieacs-fs.ts` -- File server. The leanest service; does not use the
|
||||||
|
extensions subsystem.
|
||||||
|
- `genieacs-ui.ts` -- UI service. Wraps the Koa application as the HTTP
|
||||||
|
listener.
|
||||||
|
- `genieacs-ext.ts` -- **Not a service.** This is a child process worker spawned
|
||||||
|
by the extensions subsystem. It communicates with its parent via IPC,
|
||||||
|
executing user-defined extension scripts in isolation.
|
||||||
|
|
||||||
|
### The Expression System (`lib/common/expression.ts`, `lib/common/expression/`)
|
||||||
|
|
||||||
|
The `Expression` class (defined in `lib/common/expression.ts`) is the most
|
||||||
|
important abstraction in the codebase. It uses a typed class hierarchy:
|
||||||
|
`Expression.Literal` (wraps `string | number | boolean | null`),
|
||||||
|
`Expression.Parameter` (wraps a `Path`), `Expression.Binary` (operator +
|
||||||
|
left/right), `Expression.Unary` (operator + operand), `Expression.FunctionCall`
|
||||||
|
(name + args), and `Expression.Conditional` (condition/then/otherwise). For
|
||||||
|
example, in the SQL-like text syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
Device.ModelName = "BrandX" AND Events.Inform > 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
Expressions are used pervasively:
|
||||||
|
|
||||||
|
- **Query/filter language** -- Database queries for devices, faults, presets,
|
||||||
|
etc. are all represented as expressions and compiled to MongoDB filters.
|
||||||
|
- **Configuration values** -- Config entries and preset preconditions are
|
||||||
|
expressions, enabling dynamic evaluation.
|
||||||
|
- **Authorization** -- Permission filters and validators are expressions.
|
||||||
|
- **Pagination cursors** -- Keyset pagination boundaries are expressed as filter
|
||||||
|
expressions.
|
||||||
|
|
||||||
|
The expression pipeline flows through four modules:
|
||||||
|
|
||||||
|
1. `parser.ts` -- Parses a SQL-like text syntax into the AST using a hand-rolled
|
||||||
|
recursive descent parser with a `Cursor`-based scanner. Also provides
|
||||||
|
`stringifyExpression()` for serialization. The `map()` / `mapAsync()`
|
||||||
|
tree-walking primitives are abstract methods on the `Expression` class, and
|
||||||
|
`stringify()` is now `Expression.toString()`.
|
||||||
|
2. `normalize.ts` -- Algebraic normalization using exact rational polynomial
|
||||||
|
arithmetic over native `bigint` values. The `Polynomial` class extends
|
||||||
|
`Expression` as an intermediate representation. Ensures equivalent
|
||||||
|
expressions have the same canonical form (e.g., `a + 2 > b` and `a > b - 2`
|
||||||
|
normalize identically).
|
||||||
|
3. `synth.ts` -- Boolean logic minimization via the Espresso algorithm
|
||||||
|
(espresso-iisojs). Converts expressions to minimal sum-of-products form with
|
||||||
|
three-valued logic (true/false/null). Handles domain-specific constraints
|
||||||
|
like comparison ordering and LIKE pattern relationships.
|
||||||
|
4. `evaluate.ts` -- Runtime evaluation. The `reduce()` function evaluates
|
||||||
|
operators on literal values and supports partial evaluation (returns a
|
||||||
|
reduced expression if some values are unknown). Parameter resolution is
|
||||||
|
handled by the `Expression.evaluate()` method (in `lib/common/expression.ts`)
|
||||||
|
which calls `reduce()` after mapping children via a user-supplied callback.
|
||||||
|
|
||||||
|
`pagination.ts` implements cursor-based pagination by generating filter
|
||||||
|
expressions from sort-key bookmarks.
|
||||||
|
|
||||||
|
### The Path System (`lib/common/path.ts`, `lib/common/path-set.ts`)
|
||||||
|
|
||||||
|
`Path` represents a TR-069 parameter path (e.g., `Device.WiFi.SSID.1.Name`).
|
||||||
|
Paths can contain wildcards (`*`) and alias expressions (`[key:value]`) for
|
||||||
|
query-based addressing.
|
||||||
|
|
||||||
|
Key design decisions:
|
||||||
|
|
||||||
|
- **Cached** -- `Path.parse()` caches instances in a two-generation LRU cache
|
||||||
|
rotated every 120 seconds. The constructor is public (used directly by the
|
||||||
|
parser and by methods like `slice()`, `concat()`, and `stripAlias()`).
|
||||||
|
- **Bitmask encoding** -- The `wildcard` and `alias` fields are bitfields for
|
||||||
|
O(1) segment-type checking. This limits paths to 32 segments. A `colon` field
|
||||||
|
tracks the number of attribute path segments (after a `:` separator), enabling
|
||||||
|
`paramLength` and `attrLength` accessors.
|
||||||
|
- **Immutable** -- Segment arrays are `Object.freeze()`-d.
|
||||||
|
|
||||||
|
`PathSet` is a multi-indexed collection of paths supporting pattern-matching
|
||||||
|
queries. It maintains separate `paramSegmentIndex` and `attrSegmentIndex` arrays
|
||||||
|
(one `Map<string, Set<Path>>` per position), plus a `stringIndex` map. The
|
||||||
|
`find()` method takes bitmasks to control which segments require exact matches
|
||||||
|
vs. wildcard compatibility, then uses set intersection across the smallest index
|
||||||
|
sets. A higher-level `findCompat()` method computes the appropriate bitmasks for
|
||||||
|
superset/subset matching.
|
||||||
|
|
||||||
|
### CWMP Protocol Layer (`lib/cwmp.ts`, `lib/soap.ts`, `lib/xml-parser.ts`)
|
||||||
|
|
||||||
|
`xml-parser.ts` is a custom single-pass XML parser (no DOM). It scans
|
||||||
|
character-by-character with bitwise state flags, building a tree of elements
|
||||||
|
with namespace support. Does not support CDATA.
|
||||||
|
|
||||||
|
`soap.ts` handles both parsing CPE SOAP messages and generating ACS SOAP
|
||||||
|
responses. It dispatches on the SOAP body's method name to type-specific parsers
|
||||||
|
for Inform, TransferComplete, GetParameterNamesResponse, etc. Supports CWMP
|
||||||
|
versions 1.0 through 1.4.
|
||||||
|
|
||||||
|
`cwmp.ts` is the HTTP-level CWMP request handler. It manages the session state
|
||||||
|
machine:
|
||||||
|
|
||||||
|
1. **State 0** -- Expects an Inform. Authenticates the device (Basic or Digest
|
||||||
|
auth, configurable via expression). Acquires a distributed lock. Loads device
|
||||||
|
data from MongoDB. Sends InformResponse.
|
||||||
|
2. **State 1** -- Waits for the CPE to send an empty POST (ready for ACS RPCs).
|
||||||
|
Processes any TransferComplete messages.
|
||||||
|
3. **State 2** -- The ACS drives RPCs (GetParameterNames, GetParameterValues,
|
||||||
|
SetParameterValues, AddObject, DeleteObject, Download, Reboot, FactoryReset).
|
||||||
|
The CPE responds to each.
|
||||||
|
|
||||||
|
Session persistence across TCP disconnects: when a socket closes mid-session,
|
||||||
|
the entire `SessionContext` is serialized to Redis (the MongoDB `cache`
|
||||||
|
collection) and restored when the CPE reconnects (identified by a session
|
||||||
|
cookie).
|
||||||
|
|
||||||
|
### Session Engine (`lib/session.ts`)
|
||||||
|
|
||||||
|
The session engine implements the **declaration-driven data fetching** pattern.
|
||||||
|
Rather than issuing RPCs imperatively, provisions create `Declaration` objects
|
||||||
|
stating what paths and attributes they need to read or write. The engine then:
|
||||||
|
|
||||||
|
1. Processes all declarations into a `SyncState` -- a structured plan of which
|
||||||
|
parameters to refresh, which values to set, which instances to create/delete,
|
||||||
|
etc.
|
||||||
|
2. Generates the minimal set of CWMP RPCs needed to fulfill the plan.
|
||||||
|
3. After each RPC response, updates `DeviceData` and re-evaluates.
|
||||||
|
4. Iterates until all declarations are satisfied.
|
||||||
|
|
||||||
|
The preset system (`applyPresets` in `cwmp.ts`) implements a policy engine:
|
||||||
|
presets are rules with precondition expressions that, when matched, contribute
|
||||||
|
provisions to the session. After provisions execute, if device data changed,
|
||||||
|
presets are re-evaluated (up to 4 cycles to prevent infinite loops).
|
||||||
|
|
||||||
|
### Provisions and Virtual Parameters
|
||||||
|
|
||||||
|
**Provisions** are the unit of configuration intent. Built-in provisions
|
||||||
|
(`default-provisions.ts`) include `refresh`, `value`, `tag`, `reboot`, `reset`,
|
||||||
|
`download`, and `instances`. Custom provisions are user-defined JavaScript
|
||||||
|
scripts stored in the `provisions` MongoDB collection.
|
||||||
|
|
||||||
|
**Virtual parameters** are scripts that present computed/derived values as if
|
||||||
|
they were real device parameters under the `VirtualParameters.*` namespace. They
|
||||||
|
run in two phases: a "get" phase reads real parameters and returns a computed
|
||||||
|
value; a "set" phase translates a desired value into real parameter changes.
|
||||||
|
Virtual parameters can reference other virtual parameters (up to depth 8).
|
||||||
|
|
||||||
|
### Sandbox (`lib/sandbox.ts`)
|
||||||
|
|
||||||
|
The sandbox provides a secure execution environment for provision and virtual
|
||||||
|
parameter scripts using `vm.Script` with a 50ms timeout. It uses a
|
||||||
|
**replay-based execution model**:
|
||||||
|
|
||||||
|
1. A script runs and calls `declare()` to request data.
|
||||||
|
2. When `commit()` is called, the script throws a sentinel symbol and exits.
|
||||||
|
3. The engine fetches the requested data via CWMP RPCs.
|
||||||
|
4. The script is **re-run from the beginning** with the fetched data available.
|
||||||
|
5. Earlier `declare()` calls return cached results; the script progresses
|
||||||
|
further.
|
||||||
|
6. This repeats until the script completes without throwing.
|
||||||
|
|
||||||
|
The sandbox API: `declare(path, timestamps, values)` returns a
|
||||||
|
`ParameterWrapper` proxy; `clear(path, timestamp, attributes)` invalidates
|
||||||
|
cached data; `ext(...args)` calls external extensions (results are cached per
|
||||||
|
revision to survive replays); `commit()` explicitly triggers a fetch cycle.
|
||||||
|
`Math.random()` is replaced with a seeded PRNG for determinism.
|
||||||
|
|
||||||
|
### Device Data Model (`lib/types.ts`, `lib/device.ts`)
|
||||||
|
|
||||||
|
`DeviceData` is the in-memory working copy of a device's parameter tree during a
|
||||||
|
session:
|
||||||
|
|
||||||
|
- `paths: PathSet` -- All known parameter paths.
|
||||||
|
- `timestamps: VersionedMap<Path, number>` -- When each path was last refreshed.
|
||||||
|
- `attributes: VersionedMap<Path, Attributes>` -- Per-path attributes (object,
|
||||||
|
writable, value, notification, accessList), each paired with a timestamp.
|
||||||
|
- `trackers` / `changes` -- Change tracking for re-evaluation.
|
||||||
|
|
||||||
|
`VersionedMap` (in `versioned-map.ts`) provides multi-revision snapshots,
|
||||||
|
enabling the sandbox replay model where scripts may be re-run at different
|
||||||
|
revision levels.
|
||||||
|
|
||||||
|
`device.ts` handles setting and clearing parameter data with invariant
|
||||||
|
enforcement (e.g., if `value` is set, `object` is forced to 0; parent paths are
|
||||||
|
ensured to exist). The `unpack()` function resolves wildcards and alias paths
|
||||||
|
against concrete device data.
|
||||||
|
|
||||||
|
### NBI (`lib/nbi.ts`)
|
||||||
|
|
||||||
|
A raw Node.js HTTP listener (no framework) with regex-based URL routing.
|
||||||
|
Endpoints include CRUD for presets, provisions, virtual parameters, objects, and
|
||||||
|
files; device task management (with optional synchronous execution via
|
||||||
|
connection request); fault management; generic collection querying; and ping.
|
||||||
|
|
||||||
|
The NBI uses MongoDB-style JSON queries directly. For the `devices` collection,
|
||||||
|
`query.ts` expands user-friendly queries by auto-appending `._value` to
|
||||||
|
parameter paths and generating multi-type interpretations (string, number, date,
|
||||||
|
regex) for filter values.
|
||||||
|
|
||||||
|
### File Server (`lib/fs.ts`)
|
||||||
|
|
||||||
|
A minimal HTTP file server reading from MongoDB GridFS. Supports GET/HEAD with
|
||||||
|
full HTTP caching (ETag, Last-Modified, If-None-Match, If-Modified-Since) and
|
||||||
|
Range requests (HTTP 206) for partial content downloads. Files are cached
|
||||||
|
in-memory via memoization.
|
||||||
|
|
||||||
|
### UI Backend (`lib/ui.ts`, `lib/ui/`)
|
||||||
|
|
||||||
|
A Koa application with JWT authentication, role-based authorization, and a rich
|
||||||
|
CRUD API under `/api/`. The root route serves an HTML shell that bootstraps the
|
||||||
|
Mithril.js SPA with injected config, user info, and hashed asset filenames.
|
||||||
|
|
||||||
|
`lib/ui/api.ts` defines generic CRUD endpoints for all resource types (devices,
|
||||||
|
presets, provisions, files, config, users, permissions, faults, tasks) with
|
||||||
|
authorization checks at every level. Specialized endpoints handle file
|
||||||
|
upload/download, synchronous task execution, tag management, password changes,
|
||||||
|
and CSV export.
|
||||||
|
|
||||||
|
`lib/ui/db.ts` translates between the UI's flat parameter representation and
|
||||||
|
MongoDB's nested document structure. The `flattenDevice()` function is the key
|
||||||
|
transformation: it recursively walks the nested device document and produces a
|
||||||
|
flat key-value map with colon-delimited attribute keys (e.g.,
|
||||||
|
`"Device.WiFi.SSID.1.Name" -> value`,
|
||||||
|
`"Device.WiFi.SSID.1.Name:type" -> "xsd:string"`,
|
||||||
|
`"Device.WiFi.SSID.1.Name:writable" -> true`). The `FlatDevice` type is
|
||||||
|
`Record<string, Value>` where `Value = string | number | boolean | null`.
|
||||||
|
|
||||||
|
`lib/ui/local-cache.ts` caches permissions, users, and config in-process with
|
||||||
|
hash-based revision tracking. The `getConfig()` function uses typed overloads --
|
||||||
|
it takes a typed default value (`string`, `number`, or `boolean`) and an
|
||||||
|
expression evaluation callback, returning a value of the same type as the
|
||||||
|
default.
|
||||||
|
|
||||||
|
### Database Layer (`lib/db/`)
|
||||||
|
|
||||||
|
`db/db.ts` is the single entry point to MongoDB. It manages 14 collections:
|
||||||
|
|
||||||
|
| Collection | Purpose |
|
||||||
|
| ------------------- | -------------------------------- |
|
||||||
|
| `devices` | CPE device parameter trees |
|
||||||
|
| `presets` | Configuration rules |
|
||||||
|
| `provisions` | Provision scripts |
|
||||||
|
| `virtualParameters` | Virtual parameter scripts |
|
||||||
|
| `objects` | Generic objects |
|
||||||
|
| `tasks` | Queued device management tasks |
|
||||||
|
| `faults` | Error records with retry state |
|
||||||
|
| `operations` | In-flight async operations |
|
||||||
|
| `files` (GridFS) | Firmware images and config files |
|
||||||
|
| `permissions` | RBAC rules |
|
||||||
|
| `users` | User accounts |
|
||||||
|
| `config` | Key-value configuration |
|
||||||
|
| `cache` | Distributed cache (TTL index) |
|
||||||
|
| `locks` | Distributed locks (TTL index) |
|
||||||
|
|
||||||
|
`db/synth.ts` is the sophisticated expression-to-MongoDB query compiler. It
|
||||||
|
normalizes expressions, converts them to a Boolean satisfiability representation
|
||||||
|
using the `Clause` hierarchy, minimizes via Espresso, and emits MongoDB
|
||||||
|
`$and`/`$or`/`$not` filter objects. This is used by the UI backend.
|
||||||
|
|
||||||
|
`cwmp/db.ts` handles CWMP-specific persistence: loading the nested device
|
||||||
|
document into `DeviceData` (`fetchDevice`), diffing changes back into MongoDB
|
||||||
|
update operations (`saveDevice`), and managing faults, tasks, and operations.
|
||||||
|
|
||||||
|
### Query Systems
|
||||||
|
|
||||||
|
There are two separate query paths:
|
||||||
|
|
||||||
|
1. **NBI queries** (`lib/query.ts`) -- Processes MongoDB-style JSON queries from
|
||||||
|
external API consumers. Expands parameter names, generates multi-type value
|
||||||
|
interpretations, and passes through to MongoDB directly.
|
||||||
|
|
||||||
|
2. **UI/Expression queries** (`lib/db/synth.ts`) -- Compiles the internal
|
||||||
|
expression language into optimized MongoDB filters via Boolean minimization.
|
||||||
|
This is the more sophisticated path, used by the UI backend.
|
||||||
|
|
||||||
|
### Frontend (`ui/`)
|
||||||
|
|
||||||
|
A Mithril.js SPA with hash-based routing (`#!/`). Key modules:
|
||||||
|
|
||||||
|
- `app.ts` -- Route definitions. The `pagify()` function wraps each page into a
|
||||||
|
`RouteResolver` that handles initialization, error boundaries, and data
|
||||||
|
fulfillment.
|
||||||
|
- `layout.ts` -- Top-level layout (header, navigation, content, overlay).
|
||||||
|
- `store.ts` -- Centralized data store. Implements a query-based reactive cache
|
||||||
|
with deduplication and incremental fetching. The `fulfill()` method (called
|
||||||
|
after every render) batches pending queries, computes filter diffs via
|
||||||
|
`unionDiff()`, and fetches only missing data. Connection monitoring polls
|
||||||
|
every 3 seconds for server health, clock skew, and config changes.
|
||||||
|
- `components.ts` -- Component registry with a context propagation system. The
|
||||||
|
proxied `m()` function resolves string component names and the `m.context()`
|
||||||
|
API passes data (like the current device object) down the component tree
|
||||||
|
without explicit prop threading.
|
||||||
|
- `smart-query.ts` -- Translates user-friendly `Label: value` searches into
|
||||||
|
filter expressions with type-aware matching (string, number, timestamp, MAC
|
||||||
|
address, tag).
|
||||||
|
- `task-queue.ts` -- Two-stage task pipeline: staging (user configures tasks)
|
||||||
|
then queue (tasks are committed and executed via the backend).
|
||||||
|
- `dynamic-loader.ts` -- Lazy loading of heavy libraries (CodeMirror, YAML) via
|
||||||
|
dynamic `import()`.
|
||||||
|
|
||||||
|
### Build System (`build/`)
|
||||||
|
|
||||||
|
The build is a self-bootstrapping esbuild pipeline (`npm run build` pipes
|
||||||
|
`build/build.ts` through esbuild then node). It produces:
|
||||||
|
|
||||||
|
- **Backend binaries** -- 5 entry points bundled for Node.js 12+ with shebang
|
||||||
|
banners, executable permissions, and `.js` extension stripped.
|
||||||
|
- **Frontend bundle** -- `ui/app.ts` bundled for browsers (ESM, code-split) with
|
||||||
|
content-hashed filenames.
|
||||||
|
- **CSS** -- `ui/css/app.css` bundled and minified with content-hashed output.
|
||||||
|
- **SVG sprite** -- All icons in `ui/icons/` optimized via SVGO and combined
|
||||||
|
into a single sprite.
|
||||||
|
|
||||||
|
`build/assets.ts` is a compile-time bridge: at rest it contains placeholder
|
||||||
|
filenames; during build, the `assetsPlugin` replaces them with actual
|
||||||
|
content-hashed names so both backend and frontend reference the correct assets.
|
||||||
|
|
||||||
|
Build metadata includes the date and a hash derived from the git state, appended
|
||||||
|
to the package version.
|
||||||
|
|
||||||
|
## Cross-Cutting Concerns
|
||||||
|
|
||||||
|
### Multi-Level Caching
|
||||||
|
|
||||||
|
```
|
||||||
|
memoize.ts In-process function cache (2-4 min, two-generation rotation)
|
||||||
|
|
|
||||||
|
local-cache.ts In-process snapshot cache (5s refresh, hash-based revisions)
|
||||||
|
|
|
||||||
|
cache.ts MongoDB-backed distributed cache (configurable TTL)
|
||||||
|
|
|
||||||
|
MongoDB TTL index auto-expiration
|
||||||
|
```
|
||||||
|
|
||||||
|
Each service has its own `local-cache` that periodically checks the distributed
|
||||||
|
cache for staleness. Distributed locks (`lock.ts`) coordinate rebuilds so only
|
||||||
|
one worker does the expensive computation.
|
||||||
|
|
||||||
|
### Configuration (`lib/config.ts`)
|
||||||
|
|
||||||
|
Three-tier priority: CLI args > environment variables (`GENIEACS_*`) > config
|
||||||
|
file (`config.json`) > defaults. Supports per-device overrides by appending
|
||||||
|
`-OUI-ProductClass-SerialNumber` to option names.
|
||||||
|
|
||||||
|
### Distributed Locking (`lib/lock.ts`)
|
||||||
|
|
||||||
|
MongoDB-based mutual exclusion using upsert + duplicate key detection. TTL
|
||||||
|
indexes prevent deadlocks from crashed processes. Clock skew tolerance of 30
|
||||||
|
seconds.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- **CWMP devices** -- HTTP Basic or Digest auth, configurable per-device via
|
||||||
|
expressions (`auth.ts`).
|
||||||
|
- **UI users** -- JWT tokens in cookies. Passwords hashed with PBKDF2-SHA512
|
||||||
|
(10000 iterations). Role-based authorization via `Authorizer` with
|
||||||
|
expression-based filters and validators.
|
||||||
|
|
||||||
|
### Connection Requests (`lib/connection-request.ts`)
|
||||||
|
|
||||||
|
Three methods to ask a CPE device to initiate a CWMP session:
|
||||||
|
|
||||||
|
- **HTTP** -- Standard TR-069 connection request with Digest/Basic auth.
|
||||||
|
- **UDP** -- For NAT traversal (STUN-based) with HMAC-SHA1 signed messages.
|
||||||
|
- **XMPP** -- TR-069 Annex K via a full XMPP client (`xmpp-client.ts`).
|
||||||
|
|
||||||
|
### Extensions (`lib/extensions.ts`)
|
||||||
|
|
||||||
|
User-defined scripts executed in long-lived child processes (`genieacs-ext.ts`)
|
||||||
|
communicating via IPC. Processes are lazily spawned per script name and reused.
|
||||||
|
Each request gets a unique ID; responses are matched by ID with a configurable
|
||||||
|
timeout.
|
||||||
|
|
||||||
|
### Logging (`lib/logger.ts`)
|
||||||
|
|
||||||
|
Dual-stream structured logging (application + access logs). Supports simple and
|
||||||
|
JSON formats, systemd journal integration, and automatic log rotation detection.
|
||||||
|
Protocol traces can be written to a debug file in YAML or JSON format
|
||||||
|
(`debug.ts`).
|
||||||
|
|
||||||
|
### Three-Valued Logic
|
||||||
|
|
||||||
|
The system consistently implements SQL-style three-valued logic
|
||||||
|
(true/false/null). This is visible in expression evaluation, the `Clause`
|
||||||
|
hierarchy's separate true/false/null methods, and the 2-bit minterm encoding in
|
||||||
|
the Boolean minimizer. NULL means "unknown" and propagates through operations
|
||||||
|
following SQL semantics.
|
||||||
|
|
||||||
|
## Architectural Invariants
|
||||||
|
|
||||||
|
- **The expression system does not depend on any service-specific code.** The
|
||||||
|
`lib/common/expression/` modules are pure and shared across all services.
|
||||||
|
|
||||||
|
- **The sandbox is deterministic across replays.** `Math.random()` is seeded
|
||||||
|
from the device ID, `Date.now()` is controlled, and extension results are
|
||||||
|
cached. A script re-run with the same inputs produces the same outputs.
|
||||||
|
|
||||||
|
- **Services share no in-process state.** All cross-process coordination goes
|
||||||
|
through MongoDB (the `cache` and `locks` collections). Each worker process is
|
||||||
|
independent.
|
||||||
|
|
||||||
|
- **Device data is never mutated in place during a session.** `VersionedMap`
|
||||||
|
provides revision-based snapshots. The sandbox writes to new revisions;
|
||||||
|
earlier revisions remain readable for re-evaluation.
|
||||||
|
|
||||||
|
- **CWMP session exclusivity.** A distributed lock (`cwmp_session_<deviceId>`)
|
||||||
|
ensures only one session exists per device at a time. The lock is refreshed
|
||||||
|
periodically and released at session end.
|
||||||
|
|
||||||
|
- **The UI backend never queries MongoDB with raw user input.** All queries go
|
||||||
|
through the expression-to-MongoDB compiler (`db/synth.ts`), which validates
|
||||||
|
and normalizes expressions before generating filters.
|
||||||
@@ -1,5 +1,32 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 1.2.14 (2026-03-12)
|
||||||
|
|
||||||
|
- Prevent UI crash when a malformed URL is sent to the server.
|
||||||
|
|
||||||
|
- Fix potential edge-case bugs in expression evaluation and session
|
||||||
|
serialization.
|
||||||
|
|
||||||
|
## 1.2.13 (2024-06-06)
|
||||||
|
|
||||||
|
- Increase connection timeout for UI and NBI from 30 to 120 seconds to avoid
|
||||||
|
timeouts when running unindexed queries in large deployments.
|
||||||
|
|
||||||
|
- Fix race condition causing 503 error when deleting multiple faults at once.
|
||||||
|
|
||||||
|
- Fix some UI config options not being evaluated as dynamic expressions.
|
||||||
|
|
||||||
|
- Fix an issue where certain edge-case query expressions were not being
|
||||||
|
correctly converted to MongoDB queries, resulting in inaccurate search
|
||||||
|
results.
|
||||||
|
|
||||||
|
## 1.2.12 (2024-03-28)
|
||||||
|
|
||||||
|
- Fix broken XMPP support in the previous release.
|
||||||
|
|
||||||
|
- Fix regression causing CSV downloads to be buffered in memory before being
|
||||||
|
streamed to the client.
|
||||||
|
|
||||||
## 1.2.11 (2024-03-21)
|
## 1.2.11 (2024-03-21)
|
||||||
|
|
||||||
- Resolved an issue from the previous release that caused incompatibility with
|
- Resolved an issue from the previous release that caused incompatibility with
|
||||||
|
|||||||
+334
-21
@@ -1,31 +1,348 @@
|
|||||||
# Have a question?
|
# Contributing to GenieACS
|
||||||
|
|
||||||
Please ask in the [forum] instead of opening a Github Issue. The Github issue
|
## Questions and Support
|
||||||
tracker is strictly for bug reports.
|
|
||||||
|
|
||||||
# Issues
|
Please use the [forum](https://forum.genieacs.com) for questions, help requests,
|
||||||
|
and general discussion. GitHub Issues are reserved for confirmed bug reports.
|
||||||
|
|
||||||
We use GitHub issues to track bugs. Please ensure your bug description is clear
|
## Issues
|
||||||
and has sufficient instructions to be able to reproduce the issue.
|
|
||||||
|
|
||||||
If you face interoperability issues with your CPE, more often than not it's a
|
We use GitHub Issues to track bugs. When filing a bug report:
|
||||||
device related issue. Please consult the [forum] before submitting an Issue.
|
|
||||||
|
|
||||||
# Pull Requests
|
- Provide a clear description of the problem.
|
||||||
|
- Include steps to reproduce the issue.
|
||||||
|
- Note the GenieACS version, Node.js version, and MongoDB version.
|
||||||
|
- Include relevant log output if applicable.
|
||||||
|
|
||||||
Pull requests are welcome. If you have a fix for a bug, go ahead and create a
|
If you face interoperability issues with a CPE, it is more often than not a
|
||||||
pull request. For other kinds of enhancements, please discuss in the [forum]
|
device-specific issue. Please consult the [forum](https://forum.genieacs.com)
|
||||||
first.
|
before opening an Issue.
|
||||||
|
|
||||||
Please follow the guidelines [here][commit-guidelines] when committing your
|
## Pull Requests
|
||||||
changes.
|
|
||||||
|
Pull requests are welcome. For bug fixes, go ahead and open a PR directly. For
|
||||||
|
new features or significant changes, please discuss in the
|
||||||
|
[forum](https://forum.genieacs.com) first to ensure alignment with the project's
|
||||||
|
direction.
|
||||||
|
|
||||||
|
When submitting a PR:
|
||||||
|
|
||||||
|
- Keep changes focused. One PR should address one concern.
|
||||||
|
- Run `npm run lint` and `npm test` before submitting.
|
||||||
|
- Update documentation in `docs/` if your change affects user-facing behavior.
|
||||||
|
- Add tests where appropriate (see [Testing](#testing)).
|
||||||
|
- Write a clear PR description explaining what the change does and why.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
- **Variables and functions**: `camelCase`
|
||||||
|
- **Classes**: `PascalCase`
|
||||||
|
- **Interfaces and type aliases**: `PascalCase`, without an `I` or `T` prefix
|
||||||
|
- **Module-level constants**: `UPPER_SNAKE_CASE`
|
||||||
|
- **Private class members**: prefixed with underscore (`_name`, `_cache`)
|
||||||
|
|
||||||
|
Choose descriptive, meaningful names. A longer clear name is better than a short
|
||||||
|
ambiguous one.
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
Organize imports in three groups, in this order:
|
||||||
|
|
||||||
|
1. Node.js built-in modules, using the `node:` prefix
|
||||||
|
2. External packages
|
||||||
|
3. Local project modules
|
||||||
|
|
||||||
|
Use `.ts` extensions for all local imports.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import * as http from "node:http";
|
||||||
|
|
||||||
|
import { Collection } from "mongodb";
|
||||||
|
|
||||||
|
import { connect, disconnect } from "./db.ts";
|
||||||
|
import Path from "./common/path.ts";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions and Exports
|
||||||
|
|
||||||
|
Use the `function` keyword for all named and exported function declarations.
|
||||||
|
Arrow functions should only be used for callbacks and short inline expressions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Named/exported functions use function keyword
|
||||||
|
export function processRequest(req: Request): Response {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow functions for callbacks
|
||||||
|
items.filter((item) => item.active);
|
||||||
|
promise.then((result) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
- All function declarations must have explicit return types. This is enforced by
|
||||||
|
ESLint.
|
||||||
|
- Using `any` is permitted where full typing would be impractical, but prefer
|
||||||
|
more specific types when reasonable.
|
||||||
|
- Prefer `Record<string, T>` over `{ [key: string]: T }` for mapped types.
|
||||||
|
- Use type assertions (`as`) sparingly and only when you have stronger type
|
||||||
|
knowledge than the compiler.
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
Code should be self-documenting. Use comments sparingly and only when the "why"
|
||||||
|
is not obvious from the code itself. When you do comment, use inline `//` style.
|
||||||
|
|
||||||
|
Do not use JSDoc (`/** */`) or block comments (`/* */`).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Good: explains a non-obvious reason
|
||||||
|
// Escapes everything except alphanumerics and underscore
|
||||||
|
function escapeRegExp(str: string): string {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: links to an external reference
|
||||||
|
// Source: http://stackoverflow.com/a/6969486
|
||||||
|
|
||||||
|
// Good: flags a known limitation
|
||||||
|
// TODO support "MD5-sess" algorithm directive
|
||||||
|
|
||||||
|
// Bad: restates what the code does
|
||||||
|
// Loop through each item and increment the counter
|
||||||
|
for (const item of items) {
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Use `== null` (not `=== null || === undefined`) for null/undefined checks.
|
||||||
|
ESLint is configured to allow this.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### When to Write Tests
|
||||||
|
|
||||||
|
Tests are most valuable for pure logic: parsers, data transformations,
|
||||||
|
algorithms, and utility functions where inputs and outputs are well defined. A
|
||||||
|
regression test accompanying a bug fix is also worthwhile to prevent the same
|
||||||
|
issue from resurfacing.
|
||||||
|
|
||||||
|
Not every change needs a test. Use judgment and consider the likelihood and cost
|
||||||
|
of breakage. Avoid writing tests that duplicate coverage already provided by
|
||||||
|
existing tests, and resist the urge to test trivial code just for the sake of
|
||||||
|
coverage numbers.
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
|
||||||
|
Tests use the Node.js built-in test runner (`node:test`) and assertion module
|
||||||
|
(`node:assert`). No external test libraries or mocking frameworks are used.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
void test("parseValue returns correct type for integer strings", () => {
|
||||||
|
const result = parseValue("42");
|
||||||
|
assert.strictEqual(result, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("parseValue throws on invalid input", () => {
|
||||||
|
assert.throws(() => parseValue(""), new Error("empty value"));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Key conventions:
|
||||||
|
|
||||||
|
- Prefix `test()` calls with `void` to satisfy the `no-floating-promises` lint
|
||||||
|
rule.
|
||||||
|
- Keep tests flat. Do not nest `describe` blocks.
|
||||||
|
- Use descriptive test names that state what is being tested and the expected
|
||||||
|
outcome.
|
||||||
|
- Use `assert.strictEqual()` for value comparisons and
|
||||||
|
`assert.deepStrictEqual()` for objects and arrays.
|
||||||
|
|
||||||
|
Test files live in the `test/` directory and are named after the module they
|
||||||
|
test (e.g. `test/path.ts` tests `lib/common/path.ts`).
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This project deliberately keeps its dependency footprint small. Before adding a
|
||||||
|
new dependency:
|
||||||
|
|
||||||
|
- Prefer Node.js built-in APIs when they can do the job.
|
||||||
|
- Consider whether the functionality is simple enough to implement directly.
|
||||||
|
- Justify the addition in your PR description.
|
||||||
|
|
||||||
|
Do not add development tool dependencies (linter plugins, editor integrations,
|
||||||
|
etc.) without prior discussion.
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
| Directory | Contents |
|
||||||
|
| ------------- | -------------------------------------------- |
|
||||||
|
| `lib/` | Core server-side application code |
|
||||||
|
| `lib/common/` | Code shared between server and browser |
|
||||||
|
| `lib/db/` | Database layer (MongoDB) |
|
||||||
|
| `lib/ui/` | UI backend helpers |
|
||||||
|
| `ui/` | Frontend code (Mithril.js SPA) |
|
||||||
|
| `bin/` | Service entry points |
|
||||||
|
| `build/` | Build tooling |
|
||||||
|
| `test/` | Test files |
|
||||||
|
| `docs/` | User documentation (Sphinx/reStructuredText) |
|
||||||
|
| `public/` | Static assets (favicon, logo) |
|
||||||
|
|
||||||
|
Place new code in the directory that matches its purpose. Server-side logic
|
||||||
|
belongs in `lib/`, code that must run in both Node.js and the browser belongs in
|
||||||
|
`lib/common/`, and frontend-only code belongs in `ui/`.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
User documentation lives in the `docs/` directory as reStructuredText files
|
||||||
|
built with Sphinx and published to
|
||||||
|
[docs.genieacs.com](https://docs.genieacs.com).
|
||||||
|
|
||||||
|
When your change affects user-facing behavior:
|
||||||
|
|
||||||
|
- Update the relevant documentation in `docs/`.
|
||||||
|
- If adding a new feature, consider whether it warrants a new page or a section
|
||||||
|
in an existing page.
|
||||||
|
- Keep documentation concise and practical. Match the existing tone: direct,
|
||||||
|
factual, no filler.
|
||||||
|
|
||||||
|
Documentation changes should be included in the same PR as the code change, not
|
||||||
|
submitted separately.
|
||||||
|
|
||||||
|
### ARCHITECTURE.md
|
||||||
|
|
||||||
|
`ARCHITECTURE.md` describes the high-level architecture of the project: the
|
||||||
|
service boundaries, major subsystems, data flow, and key invariants. It is aimed
|
||||||
|
at contributors who need a mental map of the codebase.
|
||||||
|
|
||||||
|
This file has a different update cadence than `docs/`. It should be revisited a
|
||||||
|
few times a year rather than kept in lockstep with every code change. When you
|
||||||
|
do update it, follow these principles:
|
||||||
|
|
||||||
|
- **Only describe things that are unlikely to change frequently.** Module
|
||||||
|
responsibilities, service boundaries, key data structures, and architectural
|
||||||
|
invariants belong here. Implementation details, function signatures, and
|
||||||
|
config option lists do not.
|
||||||
|
- **Name important files, modules, and types but do not link them.** Links go
|
||||||
|
stale. Encourage the reader to use symbol search to find named entities; this
|
||||||
|
also helps them discover related, similarly named things.
|
||||||
|
- **Keep it short.** Every recurring contributor will read it. A shorter
|
||||||
|
document is less likely to become stale and more likely to be maintained.
|
||||||
|
- **Describe the "what" and "where", not the "how".** This is a map of the
|
||||||
|
country, not an atlas of its states. Pull detailed explanations into inline
|
||||||
|
code comments or separate documents.
|
||||||
|
- **Call out architectural invariants explicitly.** Important invariants are
|
||||||
|
often expressed as the _absence_ of something (e.g., "the expression system
|
||||||
|
does not depend on service-specific code") and are hard to discover by reading
|
||||||
|
code alone.
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
This project follows the
|
||||||
|
[Conventional Commits](https://www.conventionalcommits.org/) format:
|
||||||
|
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
- Use the imperative mood, present tense: "Fix bug", not "Fixed bug" or "Fixes
|
||||||
|
bug".
|
||||||
|
- Do not capitalize the first letter of the subject (the type prefix handles
|
||||||
|
visual structure).
|
||||||
|
- Do not end the subject line with a period.
|
||||||
|
- Keep the subject line under 72 characters.
|
||||||
|
- When more context is helpful, add a body separated from the subject by a blank
|
||||||
|
line, wrapped at 72 characters. The goal is to provide enough information for
|
||||||
|
someone scanning the commit history to find a specific change (e.g. for
|
||||||
|
troubleshooting or rebasing) or to draft changelog entries for a release.
|
||||||
|
Don't be verbose, but don't be cryptic either.
|
||||||
|
|
||||||
|
#### Types
|
||||||
|
|
||||||
|
| Type | When to use |
|
||||||
|
| ---------- | --------------------------------------------------------------------------- |
|
||||||
|
| `fix` | Bug fixes |
|
||||||
|
| `feat` | New features or capabilities |
|
||||||
|
| `refactor` | Code restructuring with no behavior change |
|
||||||
|
| `test` | Adding or updating tests |
|
||||||
|
| `docs` | Documentation changes (`docs/`, `CONTRIBUTING.md`, `README.md`) |
|
||||||
|
| `build` | Build system, dependencies, esbuild config, `package.json` scripts |
|
||||||
|
| `chore` | Maintenance tasks that don't fit above (`.gitignore`, tooling config, etc.) |
|
||||||
|
|
||||||
|
#### Scopes
|
||||||
|
|
||||||
|
Scope is optional. Use it when it adds useful context; omit it when the change
|
||||||
|
is cross-cutting or the subject already makes it obvious.
|
||||||
|
|
||||||
|
| Scope | Covers |
|
||||||
|
| ------ | ----------------------------------- |
|
||||||
|
| `cwmp` | CWMP service (including extensions) |
|
||||||
|
| `nbi` | Northbound REST API service |
|
||||||
|
| `fs` | File service |
|
||||||
|
| `ui` | Web UI (frontend and backend) |
|
||||||
|
| `db` | Database layer |
|
||||||
|
|
||||||
|
If a change touches multiple scopes, either pick the primary one or omit the
|
||||||
|
scope entirely.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
feat(nbi): add bulk device delete endpoint
|
||||||
|
fix(cwmp): handle missing ParameterKey in InformResponse
|
||||||
|
refactor(db): replace raw queries with parameterized calls
|
||||||
|
fix(ui): correct parameter table sort order
|
||||||
|
test: add XML parser edge case coverage
|
||||||
|
docs: update provisioning guide for new API
|
||||||
|
build: upgrade esbuild to v0.20
|
||||||
|
|
||||||
|
fix(cwmp): increase server timeout to 2 mins
|
||||||
|
|
||||||
|
To allow enough time for running unindexed queries in large
|
||||||
|
deployments.
|
||||||
|
|
||||||
|
### Branches
|
||||||
|
|
||||||
|
- `master` is the main development branch.
|
||||||
|
- Create a feature or fix branch for your work and open a PR against `master`.
|
||||||
|
- Use concise, descriptive branch names in lowercase with hyphens:
|
||||||
|
`fix-race-condition`, `support-xmpp-requests`.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
The changelog (`CHANGELOG.md`) is maintained for each release and is written for
|
||||||
|
users and system administrators, not developers.
|
||||||
|
|
||||||
|
- Write entries as clear, user-facing prose. Do not copy commit messages
|
||||||
|
verbatim.
|
||||||
|
- Each entry should describe what changed and, when helpful, why it matters.
|
||||||
|
- Group entries under a version heading with the release date:
|
||||||
|
`## 1.2.13 (2024-06-06)`.
|
||||||
|
- Start each entry with a verb: "Fix", "Add", "Improve", "Remove", etc.
|
||||||
|
- Include enough context that a user can understand the impact without reading
|
||||||
|
the code.
|
||||||
|
|
||||||
|
You do not need to update the changelog in your PR. The maintainer will add
|
||||||
|
changelog entries during the release process.
|
||||||
|
|
||||||
## Contributor License Agreement
|
## Contributor License Agreement
|
||||||
|
|
||||||
By submitting a pull request to this repository, you acknowledge that, while
|
By submitting a pull request to this repository, you acknowledge that, while
|
||||||
maintaining copyright, you grant Zaid Abdulla a perpetual, worldwide, non-
|
maintaining copyright, you grant GenieACS Inc. a perpetual, worldwide,
|
||||||
exclusive, no-charge, royalty-free, irrevocable license to reproduce, prepare
|
non-exclusive, no-charge, royalty-free, irrevocable license to reproduce,
|
||||||
derivative works of, publicly display, publicly perform, sublicense, and
|
prepare derivative works of, publicly display, publicly perform, sublicense, and
|
||||||
distribute your contributions and such derivative works under the AGPLv3 license
|
distribute your contributions and such derivative works under the AGPLv3 license
|
||||||
or any other license terms, including, but not limited to, proprietary or
|
or any other license terms, including, but not limited to, proprietary or
|
||||||
commercial license terms.
|
commercial license terms.
|
||||||
@@ -33,7 +350,3 @@ commercial license terms.
|
|||||||
You confirm that you own or have rights to distribute and sublicense the source
|
You confirm that you own or have rights to distribute and sublicense the source
|
||||||
code contained therein, and that your content does not infringe upon the
|
code contained therein, and that your content does not infringe upon the
|
||||||
intellectual property rights of a third party.
|
intellectual property rights of a third party.
|
||||||
|
|
||||||
[commit-guidelines]:
|
|
||||||
http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
|
||||||
[forum]: https://forum.genieacs.com
|
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
# =========================================================
|
||||||
|
# GenieACS — UniFi Theme Fork
|
||||||
|
# Multi-stage build:
|
||||||
|
# Stage 1 (builder): instala devDeps + compila o projeto
|
||||||
|
# Stage 2 (runtime): imagem mínima só com dist/ + prodDeps
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
# ── Stage 1: builder ─────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
# Dependências de sistema necessárias para o build
|
||||||
|
# (git é exigido pelo build.ts para calcular o metadata de versão)
|
||||||
|
RUN apk add --no-cache git python3 make g++
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copiar manifests primeiro para cache de camadas do npm install
|
||||||
|
COPY package.json npm-shrinkwrap.json ./
|
||||||
|
|
||||||
|
# Instalar TODAS as dependências (incluindo devDependencies)
|
||||||
|
RUN npm ci --fund=false --audit=false
|
||||||
|
|
||||||
|
# Copiar o restante do fonte
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Compilar o projeto (gera dist/)
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ── Stage 2: runtime ─────────────────────────────────────
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
|
|
||||||
|
# Timezone e utilitários mínimos
|
||||||
|
RUN apk add --no-cache tzdata dumb-init
|
||||||
|
|
||||||
|
WORKDIR /opt/genieacs
|
||||||
|
|
||||||
|
# Copiar artefatos compilados
|
||||||
|
COPY --from=builder /build/dist ./
|
||||||
|
|
||||||
|
# Instalar apenas dependências de produção a partir do dist/
|
||||||
|
RUN npm ci --omit=dev --fund=false --audit=false
|
||||||
|
|
||||||
|
# Criar diretórios de configuração e extensões
|
||||||
|
RUN mkdir -p \
|
||||||
|
/opt/genieacs/config/ext \
|
||||||
|
/opt/genieacs/logs \
|
||||||
|
&& addgroup -S genieacs \
|
||||||
|
&& adduser -S -G genieacs genieacs \
|
||||||
|
&& chown -R genieacs:genieacs /opt/genieacs
|
||||||
|
|
||||||
|
USER genieacs
|
||||||
|
|
||||||
|
# Variáveis de ambiente padrão (sobrescreva no compose ou .env)
|
||||||
|
ENV GENIEACS_MONGODB_CONNECTION_URL=mongodb://mongo:27017/genieacs \
|
||||||
|
GENIEACS_EXT_DIR=/opt/genieacs/config/ext \
|
||||||
|
GENIEACS_LOG_FORMAT=json \
|
||||||
|
GENIEACS_ACCESS_LOG_FORMAT=json \
|
||||||
|
GENIEACS_UI_JWT_SECRET=change-me-in-production \
|
||||||
|
GENIEACS_CWMP_WORKER_PROCESSES=1 \
|
||||||
|
GENIEACS_NBI_WORKER_PROCESSES=1 \
|
||||||
|
GENIEACS_FS_WORKER_PROCESSES=1 \
|
||||||
|
GENIEACS_UI_WORKER_PROCESSES=1
|
||||||
|
|
||||||
|
# Expor todas as portas dos serviços
|
||||||
|
EXPOSE 3000 7547 7557 7567
|
||||||
|
|
||||||
|
# dumb-init garante propagação correta de sinais
|
||||||
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# GenieACS
|
# GenieACS
|
||||||
|
|
||||||
|
**This is the development branch for GenieACS v1.3. It is unstable and not ready
|
||||||
|
for production use. For the latest stable release, see the
|
||||||
|
[v1.2 branch](https://github.com/genieacs/genieacs/tree/v1.2).**
|
||||||
|
|
||||||
GenieACS is a high performance Auto Configuration Server (ACS) for remote
|
GenieACS is a high performance Auto Configuration Server (ACS) for remote
|
||||||
management of TR-069 enabled devices. It utilizes a declarative and fault
|
management of TR-069 enabled devices. It utilizes a declarative and fault
|
||||||
tolerant configuration engine for automating complex provisioning scenarios at
|
tolerant configuration engine for automating complex provisioning scenarios at
|
||||||
@@ -72,5 +76,5 @@ For commercial support options, please visit
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2013-2021 GenieACS Inc. GenieACS is released under the
|
Copyright 2013-2026 GenieACS Inc. GenieACS is released under the
|
||||||
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
||||||
@@ -55,6 +55,7 @@ process.on("message", (message) => {
|
|||||||
if (!script) {
|
if (!script) {
|
||||||
const cwd = process.env["GENIEACS_EXT_DIR"];
|
const cwd = process.env["GENIEACS_EXT_DIR"];
|
||||||
process.chdir(cwd);
|
process.chdir(cwd);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
script = require(`${cwd}/${fileName}`);
|
script = require(`${cwd}/${fileName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ if (!cluster.worker) {
|
|||||||
port: SERVICE_PORT,
|
port: SERVICE_PORT,
|
||||||
host: SERVICE_ADDRESS,
|
host: SERVICE_ADDRESS,
|
||||||
ssl: key && cert ? { key, cert } : null,
|
ssl: key && cert ? { key, cert } : null,
|
||||||
timeout: 30000,
|
timeout: 120000,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Need this for Node < 15
|
// Need this for Node < 15
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ if (!cluster.worker) {
|
|||||||
port: SERVICE_PORT,
|
port: SERVICE_PORT,
|
||||||
host: SERVICE_ADDRESS,
|
host: SERVICE_ADDRESS,
|
||||||
ssl: key && cert ? { key, cert } : null,
|
ssl: key && cert ? { key, cert } : null,
|
||||||
timeout: 30000,
|
timeout: 120000,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Need this for Node < 15
|
// Need this for Node < 15
|
||||||
|
|||||||
+48
-11
@@ -92,6 +92,19 @@ const assetsPlugin = {
|
|||||||
},
|
},
|
||||||
} as esbuild.Plugin;
|
} as esbuild.Plugin;
|
||||||
|
|
||||||
|
const seedPlugin = {
|
||||||
|
name: "seed",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\/seed\// }, (args) => {
|
||||||
|
if (args.with?.["type"] !== "text") return undefined;
|
||||||
|
let contents = fs.readFileSync(args.path, "utf8");
|
||||||
|
// Strip TypeScript directives that are only needed for type-checking
|
||||||
|
contents = contents.replace(/^\s*\/\/\s*@ts-.*\n/gm, "");
|
||||||
|
return { contents, loader: "text" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as esbuild.Plugin;
|
||||||
|
|
||||||
const packageDotJsonPlugin = {
|
const packageDotJsonPlugin = {
|
||||||
name: "packageDotJson",
|
name: "packageDotJson",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
@@ -107,15 +120,25 @@ const packageDotJsonPlugin = {
|
|||||||
const inlineDepsPlugin = {
|
const inlineDepsPlugin = {
|
||||||
name: "inlineDeps",
|
name: "inlineDeps",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
const deps = [
|
const deps = ["espresso-iisojs", "@codemirror", "mithril", "yaml"];
|
||||||
"parsimmon",
|
const depFiles = new Set();
|
||||||
"espresso-iisojs",
|
|
||||||
"codemirror",
|
|
||||||
"mithril",
|
|
||||||
"yaml",
|
|
||||||
];
|
|
||||||
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
||||||
if (deps.some((d) => args.path.startsWith(d))) return undefined;
|
if (args.pluginData === "inlineDeps") return undefined;
|
||||||
|
if (
|
||||||
|
depFiles.has(args.importer) ||
|
||||||
|
deps.some((d) => args.path.startsWith(d))
|
||||||
|
) {
|
||||||
|
const res = await build.resolve(args.path, {
|
||||||
|
importer: args.importer,
|
||||||
|
namespace: args.namespace,
|
||||||
|
resolveDir: args.resolveDir,
|
||||||
|
kind: args.kind,
|
||||||
|
with: args.with,
|
||||||
|
pluginData: "inlineDeps",
|
||||||
|
});
|
||||||
|
depFiles.add(res.path);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
return { sideEffects: false, external: true };
|
return { sideEffects: false, external: true };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -226,6 +249,16 @@ async function copyStatic(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateCss(): Promise<void> {
|
async function generateCss(): Promise<void> {
|
||||||
|
const tailwindPlugin = {
|
||||||
|
name: "tailwind",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\/ui\/css\/app.css$/ }, async (args) => {
|
||||||
|
const res = await execAsync(`npx @tailwindcss/cli -i ${args.path}`);
|
||||||
|
return { loader: "css", contents: res.stdout };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as esbuild.Plugin;
|
||||||
|
|
||||||
const res = await esbuild.build({
|
const res = await esbuild.build({
|
||||||
bundle: true,
|
bundle: true,
|
||||||
absWorkingDir: INPUT_DIR,
|
absWorkingDir: INPUT_DIR,
|
||||||
@@ -235,7 +268,11 @@ async function generateCss(): Promise<void> {
|
|||||||
entryPoints: ["ui/css/app.css"],
|
entryPoints: ["ui/css/app.css"],
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
entryNames: "[dir]/[name]-[hash]",
|
||||||
outfile: path.join(OUTPUT_DIR, "public/app.css"),
|
outfile: path.join(OUTPUT_DIR, "public/app.css"),
|
||||||
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
|
plugins: [tailwindPlugin],
|
||||||
|
loader: {
|
||||||
|
".woff2": "dataurl",
|
||||||
|
},
|
||||||
|
target: ["chrome111", "safari16.4", "firefox128"],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -271,7 +308,7 @@ async function generateBackendJs(): Promise<void> {
|
|||||||
banner: { js: "#!/usr/bin/env node" },
|
banner: { js: "#!/usr/bin/env node" },
|
||||||
entryPoints: services.map((s) => `bin/${s}.ts`),
|
entryPoints: services.map((s) => `bin/${s}.ts`),
|
||||||
outdir: path.join(OUTPUT_DIR, "bin"),
|
outdir: path.join(OUTPUT_DIR, "bin"),
|
||||||
plugins: [packageDotJsonPlugin, assetsPlugin],
|
plugins: [packageDotJsonPlugin, assetsPlugin, seedPlugin],
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const bin of services) {
|
for (const bin of services) {
|
||||||
@@ -293,7 +330,7 @@ async function generateFrontendJs(): Promise<void> {
|
|||||||
sourcesContent: false,
|
sourcesContent: false,
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
|
target: ["chrome111", "safari16.4", "firefox128"],
|
||||||
entryPoints: ["ui/app.ts"],
|
entryPoints: ["ui/app.ts"],
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
entryNames: "[dir]/[name]-[hash]",
|
||||||
outdir: path.join(OUTPUT_DIR, "public"),
|
outdir: path.join(OUTPUT_DIR, "public"),
|
||||||
|
|||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
declare -A FONTS=(
|
||||||
|
[InterVariable]=https://rsms.me/inter/font-files/InterVariable.woff2
|
||||||
|
[InterVariable-Italic]=https://rsms.me/inter/font-files/InterVariable-Italic.woff2
|
||||||
|
[RobotoMono]=https://raw.githubusercontent.com/googlefonts/RobotoMono/main/fonts/variable/RobotoMono%5Bwght%5D.ttf
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in "${!FONTS[@]}"
|
||||||
|
do
|
||||||
|
url="${FONTS[$name]}"
|
||||||
|
echo "Downloading $url"
|
||||||
|
TMP=$(mktemp)
|
||||||
|
curl "$url" -s --output "$TMP"
|
||||||
|
pyftsubset "$TMP" --output-file="../ui/css/$name.woff2" --flavor=woff2 --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" --layout-features+="tnum"
|
||||||
|
done
|
||||||
+2
-2
@@ -5,7 +5,7 @@ const execPromise = promisify(exec);
|
|||||||
|
|
||||||
async function runEslint(): Promise<string> {
|
async function runEslint(): Promise<string> {
|
||||||
const CMD =
|
const CMD =
|
||||||
"eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts'";
|
"eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts' 'seed/*'";
|
||||||
const env = {
|
const env = {
|
||||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -21,7 +21,7 @@ async function runEslint(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runTsc(): Promise<string> {
|
async function runTsc(): Promise<string> {
|
||||||
const CMD = "tsc --noEmit";
|
const CMD = "tsc --noEmit && tsc -p seed/tsconfig.json";
|
||||||
const env = {
|
const env = {
|
||||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|||||||
@@ -177,3 +177,4 @@ CIDR
|
|||||||
XMPP
|
XMPP
|
||||||
PEM
|
PEM
|
||||||
JID
|
JID
|
||||||
|
unindexed
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
# ── MongoDB ──────────────────────────────────────────────────────────────
|
||||||
|
mongo:
|
||||||
|
image: mongo:6
|
||||||
|
container_name: genieacs-mongo
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
- mongo_config:/data/configdb
|
||||||
|
networks:
|
||||||
|
- genieacs
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
# ── CWMP — TR-069 protocol handler (porta 7547) ──────────────────────────
|
||||||
|
cwmp:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: runtime
|
||||||
|
image: genieacs-unifi:latest
|
||||||
|
container_name: genieacs-cwmp
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["node", "bin/genieacs-cwmp"]
|
||||||
|
ports:
|
||||||
|
- "7547:7547"
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ext_dir:/opt/genieacs/config/ext
|
||||||
|
- logs:/opt/genieacs/logs
|
||||||
|
networks:
|
||||||
|
- genieacs
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:7547/ || true"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
# ── NBI — Northbound REST API (porta 7557) ───────────────────────────────
|
||||||
|
nbi:
|
||||||
|
image: genieacs-unifi:latest
|
||||||
|
container_name: genieacs-nbi
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["node", "bin/genieacs-nbi"]
|
||||||
|
ports:
|
||||||
|
- "7557:7557"
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
cwmp:
|
||||||
|
condition: service_started
|
||||||
|
volumes:
|
||||||
|
- ext_dir:/opt/genieacs/config/ext
|
||||||
|
- logs:/opt/genieacs/logs
|
||||||
|
networks:
|
||||||
|
- genieacs
|
||||||
|
|
||||||
|
# ── FS — File Server / GridFS (porta 7567) ───────────────────────────────
|
||||||
|
fs:
|
||||||
|
image: genieacs-unifi:latest
|
||||||
|
container_name: genieacs-fs
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["node", "bin/genieacs-fs"]
|
||||||
|
ports:
|
||||||
|
- "7567:7567"
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
cwmp:
|
||||||
|
condition: service_started
|
||||||
|
volumes:
|
||||||
|
- ext_dir:/opt/genieacs/config/ext
|
||||||
|
- logs:/opt/genieacs/logs
|
||||||
|
networks:
|
||||||
|
- genieacs
|
||||||
|
|
||||||
|
# ── UI — Web Interface (porta 3000) ──────────────────────────────────────
|
||||||
|
ui:
|
||||||
|
image: genieacs-unifi:latest
|
||||||
|
container_name: genieacs-ui
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["node", "bin/genieacs-ui"]
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
nbi:
|
||||||
|
condition: service_started
|
||||||
|
volumes:
|
||||||
|
- ext_dir:/opt/genieacs/config/ext
|
||||||
|
- logs:/opt/genieacs/logs
|
||||||
|
networks:
|
||||||
|
- genieacs
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/ || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
driver: local
|
||||||
|
mongo_config:
|
||||||
|
driver: local
|
||||||
|
ext_dir:
|
||||||
|
driver: local
|
||||||
|
logs:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
genieacs:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Read the Docs configuration file for Sphinx projects
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.12"
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
formats: all
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
+2
-2
@@ -19,7 +19,7 @@ import json
|
|||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'GenieACS Documentation'
|
project = 'GenieACS Documentation'
|
||||||
copyright = '2019, GenieACS Inc.'
|
copyright = '2024, GenieACS Inc.'
|
||||||
author = 'GenieACS Inc.'
|
author = 'GenieACS Inc.'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
@@ -58,7 +58,7 @@ html_theme_options = {
|
|||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
# html_static_path = ['_static']
|
||||||
|
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
sphinx_rtd_theme==2.0.0
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { defineConfig } from "@eslint/config-helpers";
|
||||||
|
import eslint from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import eslintConfigPrettier from "eslint-config-prettier";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
{
|
||||||
|
ignores: ["dist/", "test/*.js"],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.es2022,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-shadow": ["error", { allow: ["err"] }],
|
||||||
|
"handle-callback-err": "error",
|
||||||
|
"prefer-arrow-callback": "error",
|
||||||
|
"no-buffer-constructor": "error",
|
||||||
|
"prefer-const": ["error", { destructuring: "all" }],
|
||||||
|
eqeqeq: ["error", "always", { null: "ignore" }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{ functions: false },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"error",
|
||||||
|
{ allowExpressions: true },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
|
"@typescript-eslint/no-misused-promises": "error",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["ui/**/*.ts", "ui/**/*.tsx"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["seed/*.js"],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: null,
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
declare: "readonly",
|
||||||
|
clear: "readonly",
|
||||||
|
commit: "readonly",
|
||||||
|
ext: "readonly",
|
||||||
|
log: "readonly",
|
||||||
|
args: "readonly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off",
|
||||||
|
"@typescript-eslint/no-shadow": "off",
|
||||||
|
"no-shadow": ["error", { allow: ["err", "total"] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
+67
-76
@@ -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,117 +58,108 @@ 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 {
|
||||||
remoteAddress = new URL(connectionRequestUrl).hostname;
|
remoteAddress = new URL(connectionRequestUrl).hostname;
|
||||||
} catch (err) {
|
} catch {
|
||||||
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) {
|
||||||
@@ -185,7 +176,7 @@ export async function connectionRequest(
|
|||||||
() => true,
|
() => true,
|
||||||
() => false,
|
() => false,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Ignore invalid address
|
// Ignore invalid address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ function parseHeaderFeilds(str: string): Record<string, string> {
|
|||||||
try {
|
try {
|
||||||
value = JSON.parse(value);
|
value = JSON.parse(value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error("Unable to parse auth header");
|
throw new Error("Unable to parse auth header", { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res[name.trim()] = value;
|
res[name.trim()] = value;
|
||||||
|
|||||||
+34
-34
@@ -1,14 +1,14 @@
|
|||||||
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 {
|
||||||
private declare permissionSets: PermissionSet[];
|
declare private permissionSets: PermissionSet[];
|
||||||
private declare validatorCache: WeakMap<
|
declare private validatorCache: WeakMap<
|
||||||
any,
|
any,
|
||||||
(mutationType, mutation, any) => boolean
|
(mutationType, mutation, any) => boolean
|
||||||
>;
|
>;
|
||||||
private declare hasAccessCache: Map<string, boolean>;
|
declare private hasAccessCache: Map<string, boolean>;
|
||||||
private declare getFilterCache: Map<string, Expression>;
|
declare private getFilterCache: Map<string, Expression>;
|
||||||
|
|
||||||
public constructor(permissionSets: PermissionSet[]) {
|
public constructor(permissionSets: PermissionSet[]) {
|
||||||
this.permissionSets = permissionSets;
|
this.permissionSets = permissionSets;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+230
-232
@@ -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 {
|
||||||
public declare map: Map<string, number>;
|
declare public map: Map<Expression, number>;
|
||||||
public declare 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 {
|
||||||
public declare 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";
|
import Expression from "../expression.ts";
|
||||||
import normalize from "./normalize";
|
import normalize from "./normalize.ts";
|
||||||
import { Clause, SynthContext } from "./synth";
|
import { Clause, SynthContext } from "./synth.ts";
|
||||||
import { and, or } from "./util";
|
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);
|
||||||
|
}
|
||||||
+449
-374
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||||
private declare lengthIndex: Set<Path>[];
|
private paramSegmentIndex: Map<string, Set<Path>>[] = [];
|
||||||
private declare fragmentIndex: Map<string, Set<Path>>[];
|
private attrSegmentIndex: Map<string, Set<Path>>[] = [];
|
||||||
private declare 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+80
-175
@@ -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 {
|
||||||
public declare readonly segments: Segments;
|
declare public readonly segments: Segments;
|
||||||
public declare readonly wildcard: number;
|
declare public readonly colon: number;
|
||||||
public declare readonly alias: number;
|
declare public readonly wildcard: number;
|
||||||
protected declare _string: string;
|
declare public readonly alias: number;
|
||||||
protected declare _stringIndex: number[];
|
declare protected _string: string;
|
||||||
|
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(() => {
|
||||||
|
|||||||
+25
-22
@@ -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;
|
||||||
@@ -157,7 +160,7 @@ export async function httpConnectionRequest(
|
|||||||
authHeader = auth.parseWwwAuthenticateHeader(
|
authHeader = auth.parseWwwAuthenticateHeader(
|
||||||
res.headers["www-authenticate"],
|
res.headers["www-authenticate"],
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch {
|
||||||
return "Connection request error: Error parsing www-authenticate header";
|
return "Connection request error: Error parsing www-authenticate header";
|
||||||
}
|
}
|
||||||
[username, password, authExp] = await extractAuth(authExp, false);
|
[username, password, authExp] = await extractAuth(authExp, false);
|
||||||
|
|||||||
+109
-103
@@ -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;
|
||||||
|
|
||||||
@@ -89,7 +84,7 @@ async function authenticate(
|
|||||||
authentication = auth.parseAuthorizationHeader(
|
authentication = auth.parseAuthorizationHeader(
|
||||||
sessionContext.httpRequest.headers["authorization"],
|
sessionContext.httpRequest.headers["authorization"],
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1274,7 +1277,7 @@ async function clientError(
|
|||||||
body: string,
|
body: string,
|
||||||
msg: string,
|
msg: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let debugEnabled = false;
|
let debugEnabled: boolean;
|
||||||
let deviceId: string = null;
|
let deviceId: string = null;
|
||||||
|
|
||||||
if (sessionContext) {
|
if (sessionContext) {
|
||||||
@@ -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;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1312,7 +1318,7 @@ async function clientError(
|
|||||||
function decodeString(buffer: Buffer, charset: string): string {
|
function decodeString(buffer: Buffer, charset: string): string {
|
||||||
try {
|
try {
|
||||||
return buffer.toString(charset as BufferEncoding);
|
return buffer.toString(charset as BufferEncoding);
|
||||||
} catch (err) {
|
} catch {
|
||||||
if (encodingExists(charset)) return decode(buffer, charset);
|
if (encodingExists(charset)) return decode(buffer, charset);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -1383,7 +1389,7 @@ async function listenerAsync(
|
|||||||
// connection is closed before the stream is finished.
|
// connection is closed before the stream is finished.
|
||||||
// For Node 12.9+ we can just use stream.readableEnded
|
// For Node 12.9+ we can just use stream.readableEnded
|
||||||
if (!readableEnded) throw new Error("Connection closed");
|
if (!readableEnded) throw new Error("Connection closed");
|
||||||
} catch (err) {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-27
@@ -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,8 +72,8 @@ 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 (err) {
|
} 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
|
||||||
// encode the invalid characters.
|
// encode the invalid characters.
|
||||||
@@ -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;
|
||||||
|
|||||||
+74
-58
@@ -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,
|
||||||
@@ -82,7 +79,7 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
|
|||||||
try {
|
try {
|
||||||
schedule.duration = +parts.shift() * 1000;
|
schedule.duration = +parts.shift() * 1000;
|
||||||
schedule.schedule = scheduling.parseCron(parts.join(" "));
|
schedule.schedule = scheduling.parseCron(parts.join(" "));
|
||||||
} catch (err) {
|
} catch {
|
||||||
logger.warn({
|
logger.warn({
|
||||||
message: "Invalid preset schedule",
|
message: "Invalid preset schedule",
|
||||||
preset: preset["_id"],
|
preset: preset["_id"],
|
||||||
@@ -93,18 +90,18 @@ 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 (error) {
|
} catch {
|
||||||
precondition = convertOldPrecondition(
|
precondition = convertOldPrecondition(
|
||||||
JSON.parse(preset["precondition"]),
|
JSON.parse(preset["precondition"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
||||||
|
|||||||
+75
-57
@@ -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");
|
||||||
@@ -578,6 +584,8 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
toQuery(minterms: Minterm[]): Filter<unknown> {
|
toQuery(minterms: Minterm[]): Filter<unknown> {
|
||||||
const or = [];
|
const or = [];
|
||||||
|
|
||||||
|
const ejsonOps = ["$oid", "$date", "$regularExpression"];
|
||||||
|
|
||||||
for (const minterm of minterms) {
|
for (const minterm of minterms) {
|
||||||
const query = {};
|
const query = {};
|
||||||
loop: for (const clause of minterm) {
|
loop: for (const clause of minterm) {
|
||||||
@@ -588,8 +596,6 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
if (Object.keys(q).length !== 1)
|
if (Object.keys(q).length !== 1)
|
||||||
throw new Error("Invalid query expression");
|
throw new Error("Invalid query expression");
|
||||||
const [param, value] = Object.entries(q)[0];
|
const [param, value] = Object.entries(q)[0];
|
||||||
if (Object.getPrototypeOf(value).constructor !== Object)
|
|
||||||
throw new Error("Invalid query expression");
|
|
||||||
const dests = [query, ...(query["$and"] ?? [])];
|
const dests = [query, ...(query["$and"] ?? [])];
|
||||||
for (const dest of dests) {
|
for (const dest of dests) {
|
||||||
if (!(param in dest)) {
|
if (!(param in dest)) {
|
||||||
@@ -599,16 +605,28 @@ class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|||||||
|
|
||||||
let src = value;
|
let src = value;
|
||||||
let dst = dest[param];
|
let dst = dest[param];
|
||||||
|
if (Object.getPrototypeOf(src).constructor !== Object) continue;
|
||||||
|
if (Object.getPrototypeOf(dst).constructor !== Object) continue;
|
||||||
|
let srcKeys = Object.keys(src);
|
||||||
|
let dstKeys = Object.keys(dst);
|
||||||
|
|
||||||
if (src["$not"] && dst["$not"]) {
|
if ([...srcKeys, ...dstKeys].every((k) => k === "$not")) {
|
||||||
src = src["$not"];
|
src = src["$not"];
|
||||||
dst = dst["$not"];
|
dst = dst["$not"];
|
||||||
|
if (Object.getPrototypeOf(src).constructor !== Object) continue;
|
||||||
|
if (Object.getPrototypeOf(dst).constructor !== Object) continue;
|
||||||
|
srcKeys = Object.keys(src);
|
||||||
|
dstKeys = Object.keys(dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.keys(src).some((k) => k in dst)) {
|
if (srcKeys.some((k) => dstKeys.includes(k))) continue;
|
||||||
Object.assign(dst, src);
|
|
||||||
continue loop;
|
// Don't mix regular operators with EJSON special operators
|
||||||
}
|
if (srcKeys.some((k) => ejsonOps.includes(k))) continue;
|
||||||
|
if (dstKeys.some((k) => ejsonOps.includes(k))) continue;
|
||||||
|
|
||||||
|
Object.assign(dst, src);
|
||||||
|
continue loop;
|
||||||
}
|
}
|
||||||
|
|
||||||
query["$and"] ??= [];
|
query["$and"] ??= [];
|
||||||
@@ -629,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));
|
||||||
@@ -638,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)
|
||||||
|
|||||||
+2
-2
@@ -20,12 +20,12 @@ const cidrs: [IPv4 | IPv6, number][] = [];
|
|||||||
for (const str of FORWARDED_HEADER.split(",").map((s) => s.trim())) {
|
for (const str of FORWARDED_HEADER.split(",").map((s) => s.trim())) {
|
||||||
try {
|
try {
|
||||||
cidrs.push(parseCIDR(str));
|
cidrs.push(parseCIDR(str));
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Not a valid CIDR format, try parsing as IP
|
// Not a valid CIDR format, try parsing as IP
|
||||||
try {
|
try {
|
||||||
const ip = parse(str);
|
const ip = parse(str);
|
||||||
cidrs.push([ip, ip.toByteArray().length * 8]);
|
cidrs.push([ip, ip.toByteArray().length * 8]);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Not a valid IP either, ignore
|
// Not a valid IP either, ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function matchEtag(etag: string, header: string): boolean {
|
|||||||
if (t.startsWith("W/")) t = t.substring(2);
|
if (t.startsWith("W/")) t = t.substring(2);
|
||||||
try {
|
try {
|
||||||
t = JSON.parse(t);
|
t = JSON.parse(t);
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
if (t === "*") return true;
|
if (t === "*") return true;
|
||||||
|
|||||||
+17
-57
@@ -9,6 +9,9 @@ import {
|
|||||||
putUser,
|
putUser,
|
||||||
} from "./ui/db.ts";
|
} from "./ui/db.ts";
|
||||||
import { del } from "./cache.ts";
|
import { del } from "./cache.ts";
|
||||||
|
import BOOTSTRAP_SCRIPT from "../seed/bootstrap.js" with { type: "text" };
|
||||||
|
import DEFAULT_SCRIPT from "../seed/default.js" with { type: "text" };
|
||||||
|
import INFORM_SCRIPT from "../seed/inform.js" with { type: "text" };
|
||||||
|
|
||||||
interface Status {
|
interface Status {
|
||||||
users: boolean;
|
users: boolean;
|
||||||
@@ -19,58 +22,6 @@ interface Status {
|
|||||||
overview: boolean;
|
overview: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BOOTSTRAP_SCRIPT = `
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Clear cached data model to force a refresh
|
|
||||||
clear("Device", now);
|
|
||||||
clear("InternetGatewayDevice", now);
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const DEFAULT_SCRIPT = `
|
|
||||||
const hourly = Date.now(3600000);
|
|
||||||
|
|
||||||
// Refresh basic parameters hourly
|
|
||||||
declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", {path: hourly, value: hourly});
|
|
||||||
declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", {path: hourly, value: hourly});
|
|
||||||
declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress", {path: hourly, value: hourly});
|
|
||||||
declare("InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress", {path: hourly, value: hourly});
|
|
||||||
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", {path: hourly, value: hourly});
|
|
||||||
// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons
|
|
||||||
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", {path: hourly, value: 1});
|
|
||||||
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", {path: hourly, value: hourly});
|
|
||||||
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", {path: hourly, value: hourly});
|
|
||||||
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", {path: hourly, value: hourly});
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const INFORM_SCRIPT = `
|
|
||||||
// Device ID as user name
|
|
||||||
const username = declare("DeviceID.ID", {value: 1}).value[0]
|
|
||||||
|
|
||||||
// Password will be fixed for a given device because Math.random() is seeded with device ID by default.
|
|
||||||
const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
|
|
||||||
|
|
||||||
const informInterval = 300;
|
|
||||||
|
|
||||||
// Refresh values daily
|
|
||||||
const daily = Date.now(86400000);
|
|
||||||
|
|
||||||
// Unique inform offset per device for better load distribution
|
|
||||||
const informTime = daily % 86400000;
|
|
||||||
|
|
||||||
declare("InternetGatewayDevice.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username});
|
|
||||||
declare("InternetGatewayDevice.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password});
|
|
||||||
declare("InternetGatewayDevice.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true});
|
|
||||||
declare("InternetGatewayDevice.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval});
|
|
||||||
declare("InternetGatewayDevice.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime});
|
|
||||||
|
|
||||||
declare("Device.ManagementServer.ConnectionRequestUsername", {value: daily}, {value: username});
|
|
||||||
declare("Device.ManagementServer.ConnectionRequestPassword", {value: daily}, {value: password});
|
|
||||||
declare("Device.ManagementServer.PeriodicInformEnable", {value: daily}, {value: true});
|
|
||||||
declare("Device.ManagementServer.PeriodicInformInterval", {value: daily}, {value: informInterval});
|
|
||||||
declare("Device.ManagementServer.PeriodicInformTime", {value: daily}, {value: informTime});
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
export async function getStatus(): Promise<Status> {
|
export async function getStatus(): Promise<Status> {
|
||||||
const [configSnapshot, presetCount] = await Promise.all([
|
const [configSnapshot, presetCount] = await Promise.all([
|
||||||
getRevision(),
|
getRevision(),
|
||||||
@@ -79,14 +30,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> {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ interface Instance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class InstanceSet {
|
export default class InstanceSet {
|
||||||
private declare set: Set<Instance>;
|
declare private set: Set<Instance>;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.set = new Set();
|
this.set = new Set();
|
||||||
|
|||||||
+23
-21
@@ -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);
|
||||||
@@ -501,7 +503,7 @@ async function handler(
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await filesBucket.delete(filename as unknown as ObjectId);
|
await filesBucket.delete(filename as unknown as ObjectId);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Ignore error if file doesn't exist
|
// Ignore error if file doesn't exist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ export function start(
|
|||||||
res.writeHead(500, { Connection: "close" });
|
res.writeHead(500, { Connection: "close" });
|
||||||
res.end(`${err.name}: ${err.message}`);
|
res.end(`${err.name}: ${err.message}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
+3
-3
@@ -119,7 +119,7 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
|||||||
if (param && !param.endsWith("."))
|
if (param && !param.endsWith("."))
|
||||||
return [Path.parse(param), false, parsed];
|
return [Path.parse(param), false, parsed];
|
||||||
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
||||||
} catch (err) {
|
} catch {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
message: "Missing or invalid XML node",
|
message: "Missing or invalid XML node",
|
||||||
element: "Name",
|
element: "Name",
|
||||||
@@ -201,7 +201,7 @@ function parameterValueList(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return [Path.parse(param), parsed, valueType];
|
return [Path.parse(param), parsed, valueType];
|
||||||
} catch (err) {
|
} catch {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
message: "Missing or invalid XML node",
|
message: "Missing or invalid XML node",
|
||||||
element: "Name",
|
element: "Name",
|
||||||
@@ -250,7 +250,7 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return [Path.parse(param), notification, accessList];
|
return [Path.parse(param), notification, accessList];
|
||||||
} catch (err) {
|
} catch {
|
||||||
warnings.push({
|
warnings.push({
|
||||||
message: "Missing or invalid XML node",
|
message: "Missing or invalid XML node",
|
||||||
element: "Name",
|
element: "Name",
|
||||||
|
|||||||
+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;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { constants } from "node:zlib";
|
import { constants } from "node:zlib";
|
||||||
import Koa from "koa";
|
import Koa from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "@koa/router";
|
||||||
import * as jwt from "jsonwebtoken";
|
import * as jwt from "jsonwebtoken";
|
||||||
import koaStatic from "koa-static";
|
import koaSend from "koa-send";
|
||||||
import koaCompress from "koa-compress";
|
import koaCompress from "koa-compress";
|
||||||
import koaBodyParser from "koa-bodyparser";
|
import koaBodyParser from "@koa/bodyparser";
|
||||||
import koaJwt from "koa-jwt";
|
import koaJwt from "koa-jwt";
|
||||||
import * as config from "./config.ts";
|
import * as config from "./config.ts";
|
||||||
import api from "./ui/api.ts";
|
import api from "./ui/api.ts";
|
||||||
@@ -18,12 +18,6 @@ import { version as VERSION } from "../package.json";
|
|||||||
import memoize from "./common/memoize.ts";
|
import memoize from "./common/memoize.ts";
|
||||||
import { APP_JS, APP_CSS, FAVICON_PNG } from "../build/assets.ts";
|
import { APP_JS, APP_CSS, FAVICON_PNG } from "../build/assets.ts";
|
||||||
|
|
||||||
declare module "koa" {
|
|
||||||
interface Request {
|
|
||||||
body: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const koa = new Koa();
|
const koa = new Koa();
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
@@ -111,6 +105,9 @@ router.post("/login", async (ctx) => {
|
|||||||
|
|
||||||
const username = ctx.request.body.username;
|
const username = ctx.request.body.username;
|
||||||
const password = ctx.request.body.password;
|
const password = ctx.request.body.password;
|
||||||
|
const remember = ctx.request.body.remember;
|
||||||
|
const TWO_WEEKS_SECS = 1209600;
|
||||||
|
const ONE_DAY_SECS = 86400;
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: "Log in",
|
message: "Log in",
|
||||||
@@ -121,8 +118,14 @@ router.post("/login", async (ctx) => {
|
|||||||
|
|
||||||
function success(authMethod): void {
|
function success(authMethod): void {
|
||||||
log.method = authMethod;
|
log.method = authMethod;
|
||||||
const token = jwt.sign({ username, authMethod }, JWT_SECRET);
|
const expiresIn = remember ? TWO_WEEKS_SECS : ONE_DAY_SECS;
|
||||||
ctx.cookies.set(JWT_COOKIE, token, { sameSite: "lax" });
|
const token = jwt.sign({ username, authMethod }, JWT_SECRET, {
|
||||||
|
expiresIn,
|
||||||
|
});
|
||||||
|
ctx.cookies.set(JWT_COOKIE, token, {
|
||||||
|
sameSite: "lax",
|
||||||
|
maxAge: remember ? expiresIn * 1000 : null,
|
||||||
|
});
|
||||||
ctx.body = JSON.stringify(token);
|
ctx.body = JSON.stringify(token);
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
}
|
}
|
||||||
@@ -211,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)
|
||||||
@@ -225,12 +237,10 @@ router.get("/", async (ctx) => {
|
|||||||
<link rel="shortcut icon" type="image/png" href="${FAVICON_PNG}" />
|
<link rel="shortcut icon" type="image/png" href="${FAVICON_PNG}" />
|
||||||
<link rel="stylesheet" href="${APP_CSS}">
|
<link rel="stylesheet" href="${APP_CSS}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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(
|
||||||
@@ -262,6 +272,17 @@ koa.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
koa.use(router.routes());
|
koa.use(router.routes());
|
||||||
koa.use(koaStatic(config.ROOT_DIR + "/public"));
|
koa.use(async (ctx, next) => {
|
||||||
|
await next();
|
||||||
|
if (ctx.method !== "HEAD" && ctx.method !== "GET") return;
|
||||||
|
if (ctx.body != null || ctx.status !== 404) return;
|
||||||
|
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(ctx.path)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await koaSend(ctx, ctx.path, { root: config.ROOT_DIR + "/public" });
|
||||||
|
} catch (err) {
|
||||||
|
if (err.status !== 404) throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const listener = koa.callback();
|
export const listener = koa.callback();
|
||||||
+243
-169
@@ -1,13 +1,13 @@
|
|||||||
import { PassThrough } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import Router from "koa-router";
|
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);
|
||||||
@@ -88,39 +91,53 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
|||||||
.replace(/[:.]/g, "")}.csv`,
|
.replace(/[:.]/g, "")}.csv`,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.body = new PassThrough();
|
const lines: string[] = [
|
||||||
ctx.body.write(
|
"Parameter,Object,Writable,Value,Value type,Timestamp,Value timestamp,Notification,Access list,Attributes timestamp",
|
||||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n",
|
];
|
||||||
);
|
|
||||||
|
|
||||||
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()
|
||||||
|
: "",
|
||||||
];
|
];
|
||||||
ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n");
|
lines.push(row.map((r) => (r != null ? r : "")).join(","));
|
||||||
}
|
}
|
||||||
ctx.body.end();
|
|
||||||
|
for (const k of keys) {
|
||||||
|
const colonIdx = k.lastIndexOf(":");
|
||||||
|
const param = colonIdx === -1 ? k : k.slice(0, colonIdx);
|
||||||
|
const attr = colonIdx === -1 ? "value" : k.slice(colonIdx + 1);
|
||||||
|
|
||||||
|
if (param !== prevParam) {
|
||||||
|
flushRow();
|
||||||
|
prevParam = param;
|
||||||
|
attrs = {};
|
||||||
|
}
|
||||||
|
attrs[attr] = device[k];
|
||||||
|
}
|
||||||
|
flushRow();
|
||||||
|
ctx.body = lines.join("\n");
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,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}`,
|
||||||
@@ -145,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);
|
||||||
@@ -166,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)
|
||||||
@@ -194,23 +224,31 @@ 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = new PassThrough();
|
|
||||||
ctx.type = "application/json";
|
|
||||||
|
|
||||||
let c = 0;
|
|
||||||
ctx.body.write("[\n");
|
|
||||||
for await (const obj of db.query(resource, filter, options))
|
|
||||||
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
|
|
||||||
|
|
||||||
ctx.body.end("]");
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
|
ctx.type = "application/json";
|
||||||
|
ctx.body = Readable.from(
|
||||||
|
(async function* () {
|
||||||
|
let c = 0;
|
||||||
|
yield "[\n";
|
||||||
|
for await (const obj of db.query(resource, filter, options))
|
||||||
|
yield (c++ ? "," : "") + JSON.stringify(obj) + "\n";
|
||||||
|
yield "]";
|
||||||
|
})(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// CSV download
|
// CSV download
|
||||||
@@ -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,81 +280,91 @@ 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = new PassThrough();
|
logger.accessInfo(log);
|
||||||
ctx.type = "text/csv";
|
ctx.type = "text/csv";
|
||||||
ctx.attachment(
|
ctx.attachment(
|
||||||
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`,
|
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.body.write(
|
ctx.body = Readable.from(
|
||||||
Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) + "\n",
|
(async function* () {
|
||||||
|
yield Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) +
|
||||||
|
"\n";
|
||||||
|
for await (const obj of db.query(resource, filter, options)) {
|
||||||
|
const arr = Object.values(columns).map((exp) => {
|
||||||
|
return exp.evaluate((e) => {
|
||||||
|
if (e instanceof Expression.Literal) return e;
|
||||||
|
else if (e instanceof Expression.FunctionCall) {
|
||||||
|
if (e.name === "NOW") return new Expression.Literal(now);
|
||||||
|
if (e.name === "DATE_STRING") {
|
||||||
|
if (e.args[0] instanceof Expression.Literal)
|
||||||
|
return new Expression.Literal(
|
||||||
|
new Date(e.args[0].value as number).toJSON(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (e instanceof Expression.Parameter) {
|
||||||
|
let v = obj[e.path.toString()];
|
||||||
|
if (resource === "devices") {
|
||||||
|
if (e.path.toString() === "Tags") {
|
||||||
|
const tags = [];
|
||||||
|
for (const p in obj)
|
||||||
|
if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
|
||||||
|
tags.push(decodeTag(p.slice(5)));
|
||||||
|
v = tags.join(", ");
|
||||||
|
}
|
||||||
|
if (e === exp) {
|
||||||
|
const type = obj[e.path.toString() + ":type"];
|
||||||
|
if (type === "xsd:dateTime" && typeof v === "number")
|
||||||
|
v = new Date(v).toJSON();
|
||||||
|
}
|
||||||
|
} else if (resource === "faults") {
|
||||||
|
if (e.path.toString() === "detail") v = yamlStringify(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "string") v = `"${v.replace(/"/g, '""')}"`;
|
||||||
|
if (v != null) return new Expression.Literal(v);
|
||||||
|
}
|
||||||
|
return new Expression.Literal(null);
|
||||||
|
}).value;
|
||||||
|
});
|
||||||
|
yield arr.join(",") + "\n";
|
||||||
|
}
|
||||||
|
})(),
|
||||||
);
|
);
|
||||||
for await (const obj of db.query(resource, filter, options)) {
|
|
||||||
const arr = Object.values(columns).map((exp) => {
|
|
||||||
const v = evaluate(exp, obj, null, (e) => {
|
|
||||||
if (Array.isArray(e)) {
|
|
||||||
if (e[0] === "PARAM") {
|
|
||||||
if (resource === "devices") {
|
|
||||||
if (e[1] === "Tags") {
|
|
||||||
const tags = [];
|
|
||||||
for (const p in obj)
|
|
||||||
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
|
|
||||||
|
|
||||||
return tags.join(", ");
|
|
||||||
}
|
|
||||||
if (e === exp) {
|
|
||||||
const p = obj[e[1]];
|
|
||||||
if (
|
|
||||||
p &&
|
|
||||||
p.value &&
|
|
||||||
p.value[1] === "xsd:dateTime" &&
|
|
||||||
typeof p.value[0] === "number"
|
|
||||||
)
|
|
||||||
return new Date(p.value[0]).toJSON();
|
|
||||||
}
|
|
||||||
} else if (resource === "faults") {
|
|
||||||
if (e[1] === "detail") return yamlStringify(obj["detail"]);
|
|
||||||
}
|
|
||||||
} else if (e[0] === "FUNC") {
|
|
||||||
if (e[1] === "DATE_STRING") {
|
|
||||||
if (e[2] && !Array.isArray(e[2]))
|
|
||||||
return new Date(e[2]).toJSON();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(v) || v == null) return "";
|
|
||||||
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
ctx.body.write(arr.join(",") + "\n");
|
|
||||||
}
|
|
||||||
ctx.body.end();
|
|
||||||
logger.accessInfo(log);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.head(`/${resource}/:id`, async (ctx) => {
|
router.head(`/${resource}/:id`, async (ctx) => {
|
||||||
@@ -324,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);
|
||||||
@@ -350,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);
|
||||||
@@ -377,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);
|
||||||
@@ -465,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);
|
||||||
@@ -517,7 +581,7 @@ router.put("/files/:id", async (ctx) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await db.deleteFile(id);
|
await db.deleteFile(id);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// File doesn't exist, ignore
|
// File doesn't exist, ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,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);
|
||||||
|
|
||||||
@@ -594,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) {
|
||||||
@@ -665,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);
|
||||||
@@ -743,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);
|
||||||
|
|||||||
+67
-137
@@ -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 (error) {
|
} 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 {
|
||||||
|
|||||||
+17
-5
@@ -7,10 +7,10 @@ interface Revisions<V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class VersionedMap<K, V> {
|
export default class VersionedMap<K, V> {
|
||||||
private declare _sizeDiff: number[];
|
declare private _sizeDiff: number[];
|
||||||
private declare _revision: number;
|
declare private _revision: number;
|
||||||
private declare map: Map<K, (V | symbol)[]>;
|
declare private map: Map<K, (V | symbol)[]>;
|
||||||
public declare dirty: number;
|
declare public dirty: number;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this._sizeDiff = [0];
|
this._sizeDiff = [0];
|
||||||
@@ -112,6 +112,8 @@ export default class VersionedMap<K, V> {
|
|||||||
prev = v;
|
prev = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prev === NONEXISTENT && !res.delete) return null;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,8 @@ export default class VersionedMap<K, V> {
|
|||||||
del,
|
del,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mutations) return;
|
||||||
|
|
||||||
const revisions = [];
|
const revisions = [];
|
||||||
|
|
||||||
let prev: V | symbol = NONEXISTENT;
|
let prev: V | symbol = NONEXISTENT;
|
||||||
@@ -149,7 +153,7 @@ export default class VersionedMap<K, V> {
|
|||||||
|
|
||||||
public *diff(): IterableIterator<[K, V, V]> {
|
public *diff(): IterableIterator<[K, V, V]> {
|
||||||
for (const [key, revisions] of this.map) {
|
for (const [key, revisions] of this.map) {
|
||||||
if (revisions.length === 1) continue;
|
if (revisions.length <= 1) continue;
|
||||||
let first = revisions[0];
|
let first = revisions[0];
|
||||||
let last = revisions[revisions.length - 1];
|
let last = revisions[revisions.length - 1];
|
||||||
if (first === NONEXISTENT && last === NONEXISTENT) continue;
|
if (first === NONEXISTENT && last === NONEXISTENT) continue;
|
||||||
@@ -179,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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -41,7 +41,7 @@ export function parseXmlDeclaration(buffer: Buffer): Attribute[] {
|
|||||||
str = str.slice(0, str.indexOf("?>"));
|
str = str.slice(0, str.indexOf("?>"));
|
||||||
try {
|
try {
|
||||||
return parseAttrs(str.slice(5));
|
return parseAttrs(str.slice(5));
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-42
@@ -321,50 +321,44 @@ export default class XmppClient extends EventEmitter {
|
|||||||
this._iqStanzaCallbacks = new Map();
|
this._iqStanzaCallbacks = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
static connect(opts: XmppClientOptions): Promise<XmppClient> {
|
static async connect(opts: XmppClientOptions): Promise<XmppClient> {
|
||||||
return new Promise((resolve, reject) => {
|
function connectSocket(host: string, port: number): Promise<net.Socket> {
|
||||||
let socket = new net.Socket();
|
return new Promise((resolve, reject) => {
|
||||||
socket.on("error", reject);
|
const socket = new net.Socket();
|
||||||
|
socket.on("error", reject);
|
||||||
socket.connect(
|
socket.connect(port, host, () => {
|
||||||
opts.port || 5222,
|
|
||||||
opts.host,
|
|
||||||
void async function () {
|
|
||||||
socket.removeListener("error", reject);
|
socket.removeListener("error", reject);
|
||||||
try {
|
resolve(socket);
|
||||||
let status = 1;
|
});
|
||||||
while (status) {
|
});
|
||||||
if (status === STATUS_STARTTLS)
|
}
|
||||||
socket = await upgradeTls(socket, opts.host);
|
|
||||||
status = await xmppStream(
|
|
||||||
socket,
|
|
||||||
init(
|
|
||||||
socket,
|
|
||||||
opts.host,
|
|
||||||
opts.username,
|
|
||||||
opts.password,
|
|
||||||
opts.resource,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new XmppClient();
|
let socket = await connectSocket(opts.host, opts.port || 5222);
|
||||||
client._socket = socket;
|
try {
|
||||||
client._host = opts.host;
|
let status = 1;
|
||||||
client._username = opts.username;
|
while (status) {
|
||||||
client._resource = opts.resource;
|
if (status === STATUS_STARTTLS)
|
||||||
socket.on("data", client._onData.bind(client));
|
socket = await upgradeTls(socket, opts.host);
|
||||||
socket.on("error", client._onError.bind(client));
|
status = await xmppStream(
|
||||||
if (opts.timeout)
|
socket,
|
||||||
socket.setTimeout(opts.timeout, client.close.bind(client));
|
init(socket, opts.host, opts.username, opts.password, opts.resource),
|
||||||
resolve(client);
|
);
|
||||||
} catch (err) {
|
}
|
||||||
socket.destroy();
|
} catch (err) {
|
||||||
reject(err);
|
socket.destroy();
|
||||||
}
|
throw err;
|
||||||
},
|
}
|
||||||
);
|
|
||||||
});
|
const client = new XmppClient();
|
||||||
|
client._socket = socket;
|
||||||
|
client._host = opts.host;
|
||||||
|
client._username = opts.username;
|
||||||
|
client._resource = opts.resource;
|
||||||
|
socket.on("data", client._onData.bind(client));
|
||||||
|
socket.on("error", client._onError.bind(client));
|
||||||
|
if (opts.timeout)
|
||||||
|
socket.setTimeout(opts.timeout, client.close.bind(client));
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
|
|||||||
Generated
+5603
-4164
File diff suppressed because it is too large.
Load diff
+32
-28
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "genieacs",
|
"name": "genieacs",
|
||||||
"version": "1.2.11",
|
"version": "1.3.0-dev",
|
||||||
"description": "A TR-069 Auto Configuration Server (ACS)",
|
"description": "A TR-069 Auto Configuration Server (ACS)",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -26,43 +26,47 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/later": "^4.2.0",
|
"@breejs/later": "^4.2.0",
|
||||||
|
"@koa/bodyparser": "^5.1.2",
|
||||||
|
"@koa/router": "^13.1.1",
|
||||||
"bson": "^4.7.2",
|
"bson": "^4.7.2",
|
||||||
"espresso-iisojs": "^1.0.8",
|
"espresso-iisojs": "^1.0.8",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.3.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"koa": "^2.15.2",
|
"koa": "^2.16.4",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-compress": "^5.2.0",
|
||||||
"koa-compress": "^5.1.0",
|
|
||||||
"koa-jwt": "^4.0.3",
|
"koa-jwt": "^4.0.3",
|
||||||
"koa-router": "^12.0.0",
|
"koa-send": "^5.0.1",
|
||||||
"koa-static": "^5.0.0",
|
|
||||||
"mongodb": "^4.16.0",
|
"mongodb": "^4.16.0",
|
||||||
"parsimmon": "^1.18.1",
|
|
||||||
"seedrandom": "^3.0.5"
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/codemirror": "^5.60.15",
|
"@codemirror/commands": "^6.10.2",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@codemirror/lang-javascript": "^6.2.5",
|
||||||
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
|
"@codemirror/language": "^6.12.2",
|
||||||
|
"@codemirror/state": "^6.5.4",
|
||||||
|
"@codemirror/view": "^6.39.16",
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@tailwindcss/cli": "^4.2.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/koa": "^2.15.0",
|
"@types/koa": "^2.15.0",
|
||||||
"@types/koa-compress": "^4.0.6",
|
"@types/koa-compress": "^4.0.7",
|
||||||
"@types/koa-router": "^7.4.8",
|
"@types/mithril": "^2.2.7",
|
||||||
"@types/koa-static": "^4.0.4",
|
"@types/node": "^25.3.5",
|
||||||
"@types/mithril": "^2.2.6",
|
|
||||||
"@types/node": "^20.11.30",
|
|
||||||
"@types/parsimmon": "^1.10.9",
|
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
"esbuild": "^0.27.4",
|
||||||
"@typescript-eslint/parser": "^7.3.1",
|
"eslint": "^10.0.3",
|
||||||
"codemirror": "^5.65.16",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"esbuild": "^0.20.2",
|
"globals": "^17.4.0",
|
||||||
"eslint": "^8.57.0",
|
"mithril": "^2.3.8",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"prettier": "^3.8.1",
|
||||||
"mithril": "^2.2.2",
|
"sql.js": "^1.14.1",
|
||||||
"prettier": "^3.2.5",
|
"svgo": "^3.3.3",
|
||||||
"sql.js": "^1.10.2",
|
"tailwindcss": "^4.2.1",
|
||||||
"svgo": "^3.2.0",
|
"typescript": "^5.9.3",
|
||||||
"typescript": "^5.4.3",
|
"typescript-eslint": "^8.56.1",
|
||||||
"yaml": "^1.10.2"
|
"yaml": "^1.10.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Clear cached data model to force a refresh
|
||||||
|
clear("Device", now);
|
||||||
|
clear("InternetGatewayDevice", now);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const hourly = Date.now(3600000);
|
||||||
|
|
||||||
|
// Refresh basic parameters hourly
|
||||||
|
declare("InternetGatewayDevice.DeviceInfo.HardwareVersion", {
|
||||||
|
path: hourly,
|
||||||
|
value: hourly,
|
||||||
|
});
|
||||||
|
declare("InternetGatewayDevice.DeviceInfo.SoftwareVersion", {
|
||||||
|
path: hourly,
|
||||||
|
value: hourly,
|
||||||
|
});
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress",
|
||||||
|
{ path: hourly, value: hourly },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress",
|
||||||
|
{ path: hourly, value: hourly },
|
||||||
|
);
|
||||||
|
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", {
|
||||||
|
path: hourly,
|
||||||
|
value: hourly,
|
||||||
|
});
|
||||||
|
// Don't refresh password field periodically because CPEs always report blank passowrds for security reasons
|
||||||
|
declare("InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", {
|
||||||
|
path: hourly,
|
||||||
|
value: 1,
|
||||||
|
});
|
||||||
|
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", {
|
||||||
|
path: hourly,
|
||||||
|
value: hourly,
|
||||||
|
});
|
||||||
|
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", {
|
||||||
|
path: hourly,
|
||||||
|
value: hourly,
|
||||||
|
});
|
||||||
|
declare("InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", {
|
||||||
|
path: hourly,
|
||||||
|
value: hourly,
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Device ID as user name
|
||||||
|
const username = declare("DeviceID.ID", { value: 1 }).value[0];
|
||||||
|
|
||||||
|
// Password will be fixed for a given device because Math.random() is seeded with device ID by default.
|
||||||
|
const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(
|
||||||
|
36,
|
||||||
|
);
|
||||||
|
|
||||||
|
const informInterval = 300;
|
||||||
|
|
||||||
|
// Refresh values daily
|
||||||
|
const daily = Date.now(86400000);
|
||||||
|
|
||||||
|
// Unique inform offset per device for better load distribution
|
||||||
|
const informTime = daily % 86400000;
|
||||||
|
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: username },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: password },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.ManagementServer.PeriodicInformEnable",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: true },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.ManagementServer.PeriodicInformInterval",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: informInterval },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"InternetGatewayDevice.ManagementServer.PeriodicInformTime",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: informTime },
|
||||||
|
);
|
||||||
|
|
||||||
|
declare(
|
||||||
|
"Device.ManagementServer.ConnectionRequestUsername",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: username },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"Device.ManagementServer.ConnectionRequestPassword",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: password },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"Device.ManagementServer.PeriodicInformEnable",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: true },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"Device.ManagementServer.PeriodicInformInterval",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: informInterval },
|
||||||
|
);
|
||||||
|
declare(
|
||||||
|
"Device.ManagementServer.PeriodicInformTime",
|
||||||
|
{ value: daily },
|
||||||
|
{ value: informTime },
|
||||||
|
);
|
||||||
Vendored
+71
@@ -0,0 +1,71 @@
|
|||||||
|
interface Timestamps {
|
||||||
|
path?: number;
|
||||||
|
object?: number;
|
||||||
|
writable?: number;
|
||||||
|
value?: number;
|
||||||
|
notification?: number;
|
||||||
|
accessList?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Values {
|
||||||
|
path?: number | [number, number];
|
||||||
|
object?: boolean;
|
||||||
|
writable?: boolean;
|
||||||
|
value?: string | number | boolean | [string | number | boolean, string?];
|
||||||
|
notification?: number;
|
||||||
|
accessList?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParameterWrapper extends Iterable<ParameterWrapper> {
|
||||||
|
readonly path: string | undefined;
|
||||||
|
readonly size: number | undefined;
|
||||||
|
readonly object: 0 | 1 | undefined;
|
||||||
|
readonly writable: 0 | 1 | undefined;
|
||||||
|
readonly value: [string | number | boolean, string] | undefined;
|
||||||
|
readonly notification: number | undefined;
|
||||||
|
readonly accessList: string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function declare(
|
||||||
|
path: string,
|
||||||
|
timestamps?: Timestamps | null,
|
||||||
|
values?: Values | null,
|
||||||
|
): ParameterWrapper;
|
||||||
|
|
||||||
|
declare function clear(
|
||||||
|
path: string,
|
||||||
|
timestamp: number,
|
||||||
|
attributes?: Timestamps,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
declare function commit(): void;
|
||||||
|
|
||||||
|
declare function ext(...args: unknown[]): unknown;
|
||||||
|
|
||||||
|
declare function log(msg: string, meta?: Record<string, unknown>): void;
|
||||||
|
|
||||||
|
declare const args: unknown[];
|
||||||
|
|
||||||
|
interface DateConstructor {
|
||||||
|
new (): Date;
|
||||||
|
new (
|
||||||
|
year: number,
|
||||||
|
monthIndex?: number,
|
||||||
|
day?: number,
|
||||||
|
hours?: number,
|
||||||
|
minutes?: number,
|
||||||
|
seconds?: number,
|
||||||
|
milliseconds?: number,
|
||||||
|
): Date;
|
||||||
|
now(intervalOrCron?: number | string, variance?: number): number;
|
||||||
|
parse(dateString: string): number;
|
||||||
|
UTC(
|
||||||
|
year: number,
|
||||||
|
monthIndex?: number,
|
||||||
|
day?: number,
|
||||||
|
hours?: number,
|
||||||
|
minutes?: number,
|
||||||
|
seconds?: number,
|
||||||
|
milliseconds?: number,
|
||||||
|
): number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["./*.js", "./*.d.ts"]
|
||||||
|
}
|
||||||
+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);
|
||||||
});
|
});
|
||||||
+124
-6
@@ -1,8 +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 { 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";
|
||||||
|
|
||||||
|
function isFalse(expr: Expression): boolean {
|
||||||
|
return expr instanceof Expression.Literal && expr.value === false;
|
||||||
|
}
|
||||||
|
|
||||||
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 +90,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 +108,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)));
|
||||||
@@ -115,3 +119,117 @@ void test("unionDiff", async () => {
|
|||||||
assert.strictEqual(setsEqual(res4, diffSet), true);
|
assert.strictEqual(setsEqual(res4, diffSet), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void test("covers", async () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
["decimal >= 0", "decimal > 0"],
|
||||||
|
["decimal > 0", "decimal > 5"],
|
||||||
|
["string IS NOT NULL", "string = 'a'"],
|
||||||
|
["true", "decimal > 0"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [c1, c2] of cases) {
|
||||||
|
const res1 = await query(c1);
|
||||||
|
const res2 = await query(c2);
|
||||||
|
const coversResult = covers(Expression.parse(c1), Expression.parse(c2));
|
||||||
|
const actuallyCovers = Array.from(res2).every((r) => res1.has(r));
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
coversResult,
|
||||||
|
actuallyCovers,
|
||||||
|
`covers(${c1}, ${c2}) should match actual coverage`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("LIKE-Compare DC set relationships", () => {
|
||||||
|
const likeExpr = Expression.parse("string LIKE 'a%'");
|
||||||
|
|
||||||
|
const eqExpr = Expression.parse("string = 'a'");
|
||||||
|
const conjExpr: Expression = new Expression.Binary("AND", eqExpr, likeExpr);
|
||||||
|
assert.strictEqual(
|
||||||
|
isFalse(minimize(conjExpr, true)),
|
||||||
|
false,
|
||||||
|
"(string = 'a') AND (string LIKE 'a%') should NOT minimize to false",
|
||||||
|
);
|
||||||
|
|
||||||
|
const nonMatchingExpr = Expression.parse("string = 'b'");
|
||||||
|
const conjNonMatch: Expression = new Expression.Binary(
|
||||||
|
"AND",
|
||||||
|
nonMatchingExpr,
|
||||||
|
likeExpr,
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
isFalse(minimize(conjNonMatch, true)),
|
||||||
|
true,
|
||||||
|
"(string = 'b') AND (string LIKE 'a%') should minimize to false",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test("LIKE-Compare DC set with range operators", () => {
|
||||||
|
const likeExpr = Expression.parse("string LIKE 'abc%'");
|
||||||
|
|
||||||
|
const ltExpr = Expression.parse("string < 'abc'");
|
||||||
|
const ltConj: Expression = new Expression.Binary("AND", ltExpr, likeExpr);
|
||||||
|
assert.strictEqual(
|
||||||
|
isFalse(minimize(ltConj, true)),
|
||||||
|
true,
|
||||||
|
"(string < 'abc') AND (string LIKE 'abc%') should be false",
|
||||||
|
);
|
||||||
|
|
||||||
|
const ltExpr2 = Expression.parse("string < 'abd'");
|
||||||
|
const ltConj2: Expression = new Expression.Binary("AND", ltExpr2, likeExpr);
|
||||||
|
assert.strictEqual(
|
||||||
|
isFalse(minimize(ltConj2, true)),
|
||||||
|
false,
|
||||||
|
"(string < 'abd') AND (string LIKE 'abc%') should NOT be false",
|
||||||
|
);
|
||||||
|
|
||||||
|
const gtExpr = Expression.parse("string > 'abd'");
|
||||||
|
const gtConj: Expression = new Expression.Binary("AND", gtExpr, likeExpr);
|
||||||
|
assert.strictEqual(
|
||||||
|
isFalse(minimize(gtConj, true)),
|
||||||
|
true,
|
||||||
|
"(string > 'abd') AND (string LIKE 'abc%') should be false",
|
||||||
|
);
|
||||||
|
|
||||||
|
const gtExpr2 = Expression.parse("string > 'abc'");
|
||||||
|
const gtConj2: Expression = new Expression.Binary("AND", gtExpr2, likeExpr);
|
||||||
|
assert.strictEqual(
|
||||||
|
isFalse(minimize(gtConj2, true)),
|
||||||
|
false,
|
||||||
|
"(string > 'abc') AND (string LIKE 'abc%') should NOT be false",
|
||||||
|
);
|
||||||
|
});
|
||||||
+5
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2022", "dom"],
|
"lib": ["ES2022", "dom"],
|
||||||
"module": "ES2022",
|
"module": "Preserve",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
@@ -11,7 +11,10 @@
|
|||||||
"strictFunctionTypes": true,
|
"strictFunctionTypes": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true
|
"allowImportingTsExtensions": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "m"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./bin/*.ts",
|
"./bin/*.ts",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import m, { ClosureComponent, Component } from "mithril";
|
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
|
||||||
return {
|
|
||||||
view: (vnode) => {
|
|
||||||
const active = { [vnode.attrs["page"]]: "active" };
|
|
||||||
const tabs = [];
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("presets", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["presets"] },
|
|
||||||
m("a", { href: "#!/admin/presets" }, "Presets"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("provisions", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["provisions"] },
|
|
||||||
m("a", { href: "#!/admin/provisions" }, "Provisions"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("virtualParameters", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["virtualParameters"] },
|
|
||||||
m(
|
|
||||||
"a",
|
|
||||||
{ href: "#!/admin/virtualParameters" },
|
|
||||||
"Virtual Parameters",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("files", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["files"] },
|
|
||||||
m("a", { href: "#!/admin/files" }, "Files"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("config", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["config"] },
|
|
||||||
m("a", { href: "#!/admin/config" }, "Config"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("permissions", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["permissions"] },
|
|
||||||
m("a", { href: "#!/admin/permissions" }, "Permissions"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("users", 1)) {
|
|
||||||
tabs.push(
|
|
||||||
m(
|
|
||||||
"li",
|
|
||||||
{ class: active["users"] },
|
|
||||||
m("a", { href: "#!/admin/users" }, "Users"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return m("nav#side-menu", m("ul", tabs));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default component;
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import m, { RouteResolver } from "mithril";
|
import m, { RouteResolver } from "mithril";
|
||||||
import layout from "./layout.ts";
|
import layout from "./layout.tsx";
|
||||||
import * as store from "./store.ts";
|
import * as store from "./store.ts";
|
||||||
import * as wizardPage from "./wizard-page.ts";
|
import * as wizardPage from "./wizard-page.ts";
|
||||||
import * as loginPage from "./login-page.ts";
|
import * as loginPage from "./login-page.tsx";
|
||||||
import * as overviewPage from "./overview-page.ts";
|
import * as overviewPage from "./overview-page.ts";
|
||||||
import * as devicesPage from "./devices-page.ts";
|
import * as devicesPage from "./devices-page.ts";
|
||||||
import * as devicePage from "./device-page.ts";
|
import * as devicePage from "./device-page.ts";
|
||||||
@@ -16,32 +16,45 @@ import * as configPage from "./config-page.ts";
|
|||||||
import * as permissionsPage from "./permissions-page.ts";
|
import * as permissionsPage from "./permissions-page.ts";
|
||||||
import * as usersPage from "./users-page.ts";
|
import * as usersPage from "./users-page.ts";
|
||||||
import Authorizer from "../lib//common/authorizer.ts";
|
import Authorizer from "../lib//common/authorizer.ts";
|
||||||
import * as notifications from "./notifications.ts";
|
|
||||||
import { contextifyComponent } from "./components.ts";
|
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 { render as renderOverlay } from "./overlay.ts";
|
||||||
|
import Expression from "../lib/common/expression.ts";
|
||||||
|
// --- BEGIN UNIFI THEME ---
|
||||||
|
import * as unifiDevicesPage from "./unifi-devices-page.ts";
|
||||||
|
import * as unifiDevicePage from "./unifi-device-detail-page.ts";
|
||||||
|
// --- END UNIFI THEME ---
|
||||||
|
|
||||||
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),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const adminPages = [
|
window.authorizer = new Authorizer(permissionSets);
|
||||||
"presets",
|
|
||||||
"provisions",
|
|
||||||
"virtualParameters",
|
|
||||||
"files",
|
|
||||||
"config",
|
|
||||||
"users",
|
|
||||||
"permissions",
|
|
||||||
];
|
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
@@ -52,10 +65,14 @@ function pagify(pageName, page): RouteResolver {
|
|||||||
let p;
|
let p;
|
||||||
if (state?.error) p = m(errorPage.component, state);
|
if (state?.error) p = m(errorPage.component, state);
|
||||||
else p = m(contextifyComponent(page.component), state);
|
else p = m(contextifyComponent(page.component), state);
|
||||||
const attrs = {};
|
const attrs = {
|
||||||
attrs["page"] = pageName;
|
page: pageName,
|
||||||
attrs["oncreate"] = attrs["onupdate"] = () => {
|
oncreate: () => {
|
||||||
store.fulfill(lastRenderTimestamp);
|
store.fulfill(lastRenderTimestamp);
|
||||||
|
},
|
||||||
|
onupdate: () => {
|
||||||
|
store.fulfill(lastRenderTimestamp);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return m(layout, attrs, p);
|
return m(layout, attrs, p);
|
||||||
},
|
},
|
||||||
@@ -78,10 +95,8 @@ function pagify(pageName, page): RouteResolver {
|
|||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
if (!window.username && err.message.indexOf("authorized") >= 0)
|
||||||
notifications.push("error", err.message);
|
|
||||||
m.route.set("/login", { continue: requestedPath });
|
m.route.set("/login", { continue: requestedPath });
|
||||||
}
|
|
||||||
state = { error: err.message };
|
state = { error: err.message };
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@@ -91,37 +106,22 @@ function pagify(pageName, page): RouteResolver {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectAdminPage(): RouteResolver {
|
|
||||||
const component: RouteResolver = {
|
|
||||||
onmatch: () => {
|
|
||||||
for (const page of adminPages) {
|
|
||||||
if (window.authorizer.hasAccess(page, 2)) {
|
|
||||||
m.route.set(`/admin/${page}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
m.route(document.body, "/overview", {
|
m.route(document.body, "/overview", {
|
||||||
"/wizard": pagify("wizard", wizardPage),
|
"/wizard": pagify("wizard", wizardPage),
|
||||||
"/login": pagify("login", loginPage),
|
|
||||||
"/overview": pagify("overview", overviewPage),
|
"/overview": pagify("overview", overviewPage),
|
||||||
"/devices": pagify("devices", devicesPage),
|
// --- BEGIN UNIFI THEME ---
|
||||||
"/devices/:id": pagify("devices", devicePage),
|
"/devices": pagify("devices", unifiDevicesPage),
|
||||||
|
"/devices/:id": pagify("devices", unifiDevicePage),
|
||||||
|
// --- END UNIFI THEME ---
|
||||||
"/faults": pagify("faults", faultsPage),
|
"/faults": pagify("faults", faultsPage),
|
||||||
"/admin": redirectAdminPage(),
|
"/presets": pagify("presets", presetsPage),
|
||||||
"/admin/presets": pagify("presets", presetsPage),
|
"/provisions": pagify("provisions", provisionsPage),
|
||||||
"/admin/provisions": pagify("provisions", provisionsPage),
|
"/virtualParameters": pagify("virtualParameters", virtualParametersPage),
|
||||||
"/admin/virtualParameters": pagify(
|
"/files": pagify("files", filesPage),
|
||||||
"virtualParameters",
|
"/config": pagify("config", configPage),
|
||||||
virtualParametersPage,
|
"/users": pagify("users", usersPage),
|
||||||
),
|
"/permissions": pagify("permissions", permissionsPage),
|
||||||
"/admin/files": pagify("files", filesPage),
|
"/login": {
|
||||||
"/admin/config": pagify("config", configPage),
|
render: () => [m(loginPage.component), renderOverlay(), m(drawerComponent)],
|
||||||
"/admin/users": pagify("users", usersPage),
|
},
|
||||||
"/admin/permissions": pagify("permissions", permissionsPage),
|
|
||||||
});
|
});
|
||||||
+16
-12
@@ -4,15 +4,15 @@ type AutocompleteCallback = (
|
|||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export default class Autocomplete {
|
export default class Autocomplete {
|
||||||
private declare callback: AutocompleteCallback;
|
declare private callback: AutocompleteCallback;
|
||||||
private declare element: HTMLInputElement;
|
declare private element: HTMLInputElement;
|
||||||
private declare hideTimeout: NodeJS.Timeout;
|
declare private hideTimeout: NodeJS.Timeout;
|
||||||
private declare visible: boolean;
|
declare private visible: boolean;
|
||||||
private declare default: string;
|
declare private default: string;
|
||||||
private declare selection: number;
|
declare private selection: number;
|
||||||
private declare container: HTMLElement;
|
declare private container: HTMLElement;
|
||||||
|
|
||||||
public constructor(className: string, callback: AutocompleteCallback) {
|
public constructor(callback: AutocompleteCallback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.element = null;
|
this.element = null;
|
||||||
this.hideTimeout = null;
|
this.hideTimeout = null;
|
||||||
@@ -24,7 +24,8 @@ export default class Autocomplete {
|
|||||||
this.container.style.position = "absolute";
|
this.container.style.position = "absolute";
|
||||||
this.container.style.display = "block";
|
this.container.style.display = "block";
|
||||||
this.container.style.opacity = "0";
|
this.container.style.opacity = "0";
|
||||||
this.container.className = className;
|
this.container.className =
|
||||||
|
"absolute py-1 mt-2 rounded-md shadow-lg bg-white";
|
||||||
}
|
}
|
||||||
|
|
||||||
public attach(el: HTMLInputElement): void {
|
public attach(el: HTMLInputElement): void {
|
||||||
@@ -118,7 +119,9 @@ export default class Autocomplete {
|
|||||||
if (!this.visible) {
|
if (!this.visible) {
|
||||||
if (!this.hideTimeout) {
|
if (!this.hideTimeout) {
|
||||||
document.body.appendChild(this.container);
|
document.body.appendChild(this.container);
|
||||||
window.getComputedStyle(this.container).opacity;
|
// Force style recalc so the initial opacity is resolved before
|
||||||
|
// setting it to "1", allowing the CSS transition to play.
|
||||||
|
void window.getComputedStyle(this.container).opacity;
|
||||||
} else {
|
} else {
|
||||||
clearTimeout(this.hideTimeout);
|
clearTimeout(this.hideTimeout);
|
||||||
this.hideTimeout = null;
|
this.hideTimeout = null;
|
||||||
@@ -140,9 +143,10 @@ export default class Autocomplete {
|
|||||||
for (const [idx, suggestion] of suggestions.entries()) {
|
for (const [idx, suggestion] of suggestions.entries()) {
|
||||||
const e = document.createElement("div");
|
const e = document.createElement("div");
|
||||||
if (suggestion.tip) e.title = suggestion.tip;
|
if (suggestion.tip) e.title = suggestion.tip;
|
||||||
e.classList.add("suggestion");
|
e.className =
|
||||||
|
"text-stone-700 block px-4 py-2 text-sm hover:bg-stone-100 hover:text-stone-900";
|
||||||
if (idx === this.selection) {
|
if (idx === this.selection) {
|
||||||
e.classList.add("selected");
|
e.classList.add("bg-stone-100", "text-stone-900");
|
||||||
selectedElement = e;
|
selectedElement = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,26 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
const form = [
|
const form = [
|
||||||
m(
|
m(
|
||||||
"p",
|
"p",
|
||||||
m("label", { for: "username" }, "Username"),
|
m(
|
||||||
m("input", {
|
"label.block text-sm font-semibold text-stone-700 mt-2 mb-1",
|
||||||
name: "username",
|
{ for: "username" },
|
||||||
type: "text",
|
"Username",
|
||||||
value: vnode.state["username"],
|
),
|
||||||
disabled: !!username,
|
m(
|
||||||
oninput: (e) => {
|
"input.shadow-sm focus:ring-cyan-500 focus:border-cyan-500 block sm:text-sm border-stone-300 rounded-md",
|
||||||
vnode.state["username"] = e.target.value;
|
{
|
||||||
|
name: "username",
|
||||||
|
type: "text",
|
||||||
|
value: vnode.state["username"],
|
||||||
|
disabled: !!username,
|
||||||
|
oninput: (e) => {
|
||||||
|
vnode.state["username"] = e.target.value;
|
||||||
|
},
|
||||||
|
oncreate: (_vnode) => {
|
||||||
|
(_vnode.dom as HTMLSelectElement).focus();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
oncreate: (_vnode) => {
|
),
|
||||||
(_vnode.dom as HTMLSelectElement).focus();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -48,31 +55,38 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
form.push(
|
form.push(
|
||||||
m(
|
m(
|
||||||
"p",
|
"p",
|
||||||
m("label", { for: f }, l),
|
m(
|
||||||
m("input", {
|
"label.block text-sm font-semibold text-stone-700 mt-2 mb-1",
|
||||||
name: f,
|
{ for: f },
|
||||||
type: "password",
|
l,
|
||||||
value: vnode.state[f],
|
),
|
||||||
oninput: (e) => {
|
m(
|
||||||
vnode.state[f] = e.target.value;
|
"input.shadow-sm focus:ring-cyan-500 focus:border-cyan-500 block sm:text-sm border-stone-300 rounded-md",
|
||||||
|
{
|
||||||
|
name: f,
|
||||||
|
type: "password",
|
||||||
|
value: vnode.state[f],
|
||||||
|
oninput: (e) => {
|
||||||
|
vnode.state[f] = e.target.value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = m(
|
const submit = m(
|
||||||
"button.primary",
|
"button.ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
{
|
{
|
||||||
type: "submit",
|
type: "submit",
|
||||||
},
|
},
|
||||||
"Change password",
|
"Change password",
|
||||||
) as VnodeDOM;
|
) as VnodeDOM;
|
||||||
|
|
||||||
form.push(m(".actions-bar", submit));
|
form.push(m("div.flex justify-end mt-5", submit));
|
||||||
|
|
||||||
const children = [
|
const children = [
|
||||||
m("h1", "Change password"),
|
m("h2.text-lg leading-6 font-medium text-stone-900", "Change password"),
|
||||||
m(
|
m(
|
||||||
"form",
|
"form",
|
||||||
{
|
{
|
||||||
|
|||||||
+50
-25
@@ -1,4 +1,3 @@
|
|||||||
import { Editor } from "codemirror";
|
|
||||||
import { ClosureComponent } from "mithril";
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "./components.ts";
|
import { m } from "./components.ts";
|
||||||
import { codeMirror } from "./dynamic-loader.ts";
|
import { codeMirror } from "./dynamic-loader.ts";
|
||||||
@@ -11,41 +10,67 @@ interface Attrs {
|
|||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onSubmit?: (dom: Element) => void;
|
onSubmit?: (dom: Element) => void;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onReady?: (editor: Editor) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent<Attrs> = () => {
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("textarea", {
|
return m("div.font-mono text-sm", {
|
||||||
name: vnode.attrs.id,
|
|
||||||
value: vnode.attrs.value,
|
|
||||||
oncreate: (_vnode) => {
|
oncreate: (_vnode) => {
|
||||||
const editor = codeMirror.fromTextArea(
|
const theme = codeMirror.EditorView.theme({
|
||||||
_vnode.dom as HTMLTextAreaElement,
|
"&.cm-editor": {
|
||||||
{
|
display: "block",
|
||||||
mode: vnode.attrs.mode,
|
width: "50rem",
|
||||||
lineNumbers: true,
|
height: "30rem",
|
||||||
readOnly: vnode.attrs.readOnly,
|
"max-width": "100%",
|
||||||
extraKeys: {
|
"border-radius": "0.375rem",
|
||||||
"Ctrl-Enter": () => {
|
"border-width": "1px",
|
||||||
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
"border-color": "var(--color-stone-300)",
|
||||||
},
|
"box-shadow":
|
||||||
"Cmd-Enter": () => {
|
"var(--tw-ring-shadow, 0 0 #0000), 0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||||
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
overflow: "hidden",
|
||||||
},
|
"& > .cm-scroller": {
|
||||||
|
"font-family": "inherit",
|
||||||
|
"line-height": "inherit",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
"&.cm-editor.cm-focused": {
|
||||||
|
outline: "none",
|
||||||
|
"border-color": "var(--color-cyan-500)",
|
||||||
|
"--tw-ring-shadow": "0 0 0 1px var(--color-cyan-500)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (vnode.attrs.onChange) {
|
const extensions = [
|
||||||
editor.on("change", (e) => {
|
theme,
|
||||||
vnode.attrs.onChange(e.getValue());
|
codeMirror.lineNumbers(),
|
||||||
});
|
codeMirror.history(),
|
||||||
}
|
codeMirror.syntaxHighlighting(codeMirror.defaultHighlightStyle),
|
||||||
|
codeMirror.keymap.of([
|
||||||
|
...codeMirror.defaultKeymap,
|
||||||
|
...codeMirror.historyKeymap,
|
||||||
|
]),
|
||||||
|
codeMirror.EditorState.readOnly.of(!!vnode.attrs.readOnly),
|
||||||
|
codeMirror.EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged && vnode.attrs.onChange)
|
||||||
|
vnode.attrs.onChange(update.state.doc.toString());
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (vnode.attrs.mode === "javascript")
|
||||||
|
extensions.push(codeMirror.javascript());
|
||||||
|
else if (vnode.attrs.mode === "yaml")
|
||||||
|
extensions.push(codeMirror.yaml());
|
||||||
|
|
||||||
|
const editor = new codeMirror.EditorView({
|
||||||
|
state: codeMirror.EditorState.create({
|
||||||
|
doc: vnode.attrs.value,
|
||||||
|
extensions,
|
||||||
|
}),
|
||||||
|
parent: _vnode.dom as HTMLTextAreaElement,
|
||||||
|
});
|
||||||
|
|
||||||
if (vnode.attrs.focus) editor.focus();
|
if (vnode.attrs.focus) editor.focus();
|
||||||
if (vnode.attrs.onReady) vnode.attrs.onReady(editor);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export { EditorView, keymap, lineNumbers } from "@codemirror/view";
|
||||||
|
export { EditorState } from "@codemirror/state";
|
||||||
|
export { history, historyKeymap, defaultKeymap } from "@codemirror/commands";
|
||||||
|
export {
|
||||||
|
syntaxHighlighting,
|
||||||
|
defaultHighlightStyle,
|
||||||
|
} from "@codemirror/language";
|
||||||
|
export { javascript } from "@codemirror/lang-javascript";
|
||||||
|
export { yaml } from "@codemirror/lang-yaml";
|
||||||
+1
-1
@@ -127,7 +127,7 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
|
|||||||
return res;
|
return res;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return m(
|
return m(
|
||||||
"p.error",
|
"p.text-sm font-bold text-red-500 cursor-pointer",
|
||||||
{
|
{
|
||||||
title: "Click to print stack trace to console",
|
title: "Click to print stack trace to console",
|
||||||
onclick: () => console.error(err),
|
onclick: () => console.error(err),
|
||||||
|
|||||||
+109
-64
@@ -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 { getIcon } from "../icons.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,18 +65,25 @@ 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("input", {
|
const search = m(
|
||||||
type: "text",
|
"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",
|
||||||
placeholder: "Search parameters",
|
{
|
||||||
oninput: (e) => {
|
type: "text",
|
||||||
formQueryString(e.target.value);
|
placeholder: "Search parameters",
|
||||||
e.redraw = false;
|
oninput: (e) => {
|
||||||
|
formQueryString(e.target.value);
|
||||||
|
e.redraw = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const instanceRegex = /\.[0-9]+$/;
|
const instanceRegex = /\.[0-9]+$/;
|
||||||
let re;
|
let re;
|
||||||
@@ -72,37 +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 = [];
|
||||||
const attrs = { key: k };
|
if (p.value) {
|
||||||
|
|
||||||
if (p.object === false) {
|
|
||||||
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",
|
||||||
@@ -111,12 +131,16 @@ 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(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("delete-instance"),
|
m(icon, {
|
||||||
|
name: "delete-instance",
|
||||||
|
class:
|
||||||
|
"inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -128,12 +152,16 @@ 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(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("add-instance"),
|
m(icon, {
|
||||||
|
name: "add-instance",
|
||||||
|
class:
|
||||||
|
"inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -147,20 +175,25 @@ 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()],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("refresh"),
|
m(icon, {
|
||||||
|
name: "refresh",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"tr",
|
"tr",
|
||||||
attrs,
|
m(
|
||||||
m("td.left", m("long-text", { text: k })),
|
"td.pl-4 pr-2 py-2 truncate",
|
||||||
m("td.right", val),
|
m("long-text", { text: p.path.toString() }),
|
||||||
|
),
|
||||||
|
m("td.pr-4 py-2 text-right flex justify-end", val),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,25 +201,37 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
"loading",
|
"loading",
|
||||||
{ queries: [vnode.attrs.deviceQuery] },
|
{ queries: [vnode.attrs.deviceQuery] },
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
".bg-white shadow-sm rounded-lg",
|
||||||
m(
|
|
||||||
"a.download-csv",
|
|
||||||
{
|
|
||||||
href: `api/devices/${encodeURIComponent(
|
|
||||||
device["DeviceID.ID"].value[0],
|
|
||||||
)}.csv`,
|
|
||||||
download: "",
|
|
||||||
style: "float: right;",
|
|
||||||
},
|
|
||||||
"Download",
|
|
||||||
),
|
|
||||||
search,
|
search,
|
||||||
m(
|
m(
|
||||||
".parameter-list",
|
".overflow-hidden",
|
||||||
m("table", m("tbody", rows)),
|
|
||||||
m(
|
m(
|
||||||
"m",
|
".overflow-y-scroll h-96 shadow-inner",
|
||||||
`Displaying ${filteredKeys.length} out of ${count} parameters.`,
|
m(
|
||||||
|
"table.w-full table-fixed font-mono text-xs text-stone-900",
|
||||||
|
m("tbody.divide-y divide-stone-200", rows),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"div.text-stone-700 px-4 py-3 flex justify-between items-end",
|
||||||
|
m(
|
||||||
|
"span.text-xs",
|
||||||
|
"Displaying ",
|
||||||
|
m("span.font-medium", "" + filteredParams.length),
|
||||||
|
" out of ",
|
||||||
|
m("span.font-medium", "" + count),
|
||||||
|
" parameters",
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
|
||||||
|
{
|
||||||
|
href: `api/devices/${encodeURIComponent(
|
||||||
|
device["DeviceID.ID"],
|
||||||
|
)}.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(
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Reboot device",
|
title: "Reboot device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "reboot",
|
name: "reboot",
|
||||||
device: device["DeviceID.ID"].value[0],
|
device: device["DeviceID.ID"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -29,13 +29,13 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.critical",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Factory reset device",
|
title: "Factory reset device",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.queueTask({
|
taskQueue.queueTask({
|
||||||
name: "factoryReset",
|
name: "factoryReset",
|
||||||
device: device["DeviceID.ID"].value[0],
|
device: device["DeviceID.ID"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -45,13 +45,13 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.critical",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
title: "Push a firmware or a config file",
|
title: "Push a firmware or a config file",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
taskQueue.stageDownload({
|
taskQueue.stageDownload({
|
||||||
name: "download",
|
name: "download",
|
||||||
devices: [device["DeviceID.ID"].value[0]],
|
devices: [device["DeviceID.ID"]],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -61,12 +61,12 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
m(
|
m(
|
||||||
"button.primary",
|
"button.px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
|
||||||
{
|
{
|
||||||
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)
|
||||||
@@ -83,7 +83,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return m(".actions-bar", buttons);
|
return m("div.flex gap-3 mt-4", buttons);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+102
-48
@@ -2,19 +2,25 @@ import { ClosureComponent, Component } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
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 { getIcon } from "../icons.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",
|
||||||
@@ -23,63 +29,111 @@ const component: ClosureComponent = (): Component => {
|
|||||||
"Detail",
|
"Detail",
|
||||||
"Retries",
|
"Retries",
|
||||||
"Timestamp",
|
"Timestamp",
|
||||||
].map((l) => m("th", l));
|
"",
|
||||||
const thead = m("thead", m("tr", headers));
|
].map((l, i) => {
|
||||||
|
let padding: string;
|
||||||
|
if (i === 0) padding = "pl-6 pr-3";
|
||||||
|
else if (i === 6) padding = "pl-3";
|
||||||
|
else padding = "px-3";
|
||||||
|
return m(
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
scope: "col",
|
||||||
|
class:
|
||||||
|
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||||
|
padding,
|
||||||
|
},
|
||||||
|
l,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const thead = m("thead.bg-stone-50", m("tr", headers));
|
||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const f of faults.value) {
|
for (const f of faults.value) {
|
||||||
rows.push([
|
rows.push(
|
||||||
m("td", f["channel"]),
|
|
||||||
m("td", f["code"]),
|
|
||||||
m("td", m("long-text", { text: f["message"] })),
|
|
||||||
m("td", m("long-text", { text: stringify(f["detail"]) })),
|
|
||||||
|
|
||||||
m("td", f["retries"]),
|
|
||||||
m("td", new Date(f["timestamp"]).toLocaleString()),
|
|
||||||
m(
|
m(
|
||||||
"td",
|
"tr",
|
||||||
m(
|
m(
|
||||||
"button",
|
"td.whitespace-nowrap pl-6 pr-3 py-4 text-sm text-stone-900",
|
||||||
{
|
f["channel"],
|
||||||
title: "Delete fault",
|
),
|
||||||
onclick: (e) => {
|
m(
|
||||||
e.redraw = false;
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
store
|
f["code"],
|
||||||
.deleteResource("faults", f["_id"])
|
),
|
||||||
.then(() => {
|
m(
|
||||||
notifications.push("success", "Fault deleted");
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
store.setTimestamp(Date.now());
|
m("long-text", { text: f["message"], class: "max-w-xs" }),
|
||||||
m.redraw();
|
),
|
||||||
})
|
m(
|
||||||
.catch((err) => {
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
notifications.push("error", err.message);
|
m("long-text", {
|
||||||
store.setTimestamp(Date.now());
|
text: stringify(f["detail"]),
|
||||||
});
|
class: "max-w-xs",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
m(
|
||||||
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
|
f["retries"],
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"td.whitespace-nowrap px-3 py-4 text-sm text-stone-900",
|
||||||
|
new Date(f["timestamp"]).toLocaleString(),
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"td.whitespace-nowrap pl-3 pr-6 py-4 text-sm text-stone-900",
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
|
title: "Delete fault",
|
||||||
|
onclick: (e) => {
|
||||||
|
e.redraw = false;
|
||||||
|
store
|
||||||
|
.deleteResource("faults", f["_id"])
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", "Fault deleted");
|
||||||
|
store.setTimestamp(Date.now());
|
||||||
|
m.redraw();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
store.setTimestamp(Date.now());
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
"Delete",
|
||||||
getIcon("remove"),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tbody;
|
if (!rows.length) {
|
||||||
if (rows.length) {
|
rows.push(
|
||||||
tbody = m(
|
m(
|
||||||
"tbody",
|
"tr",
|
||||||
rows.map((r) => m("tr", r)),
|
m(
|
||||||
);
|
"td.bg-stripes text-sm font-medium text-center text-stone-500 p-4",
|
||||||
} else {
|
{ colspan: headers.length },
|
||||||
tbody = m(
|
"No faults",
|
||||||
"tbody",
|
),
|
||||||
m("tr.empty", m("td", { colspan: headers.length }, "No faults")),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [faults] },
|
{ queries: [faults] },
|
||||||
m("table.table", thead, tbody),
|
m(
|
||||||
|
"div.shadow-sm overflow-hidden rounded-lg w-max",
|
||||||
|
m(
|
||||||
|
"table.divide-y divide-stone-200",
|
||||||
|
thead,
|
||||||
|
m("tbody.divide-y divide-stone-200 bg-white", rows),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
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);
|
||||||
});
|
});
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
return m(
|
return m(
|
||||||
"a",
|
"a.text-cyan-700 hover:text-cyan-900 font-medium",
|
||||||
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
|
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
|
||||||
children,
|
children,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
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",
|
"svg.inline",
|
||||||
{
|
{
|
||||||
width: "1em",
|
width: "1em",
|
||||||
height: "1em",
|
height: "1em",
|
||||||
|
style: "margin: 0 0.2em 0.2em",
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||||
},
|
},
|
||||||
m("circle", {
|
m("circle.stroke-stone-200 stroke-1", {
|
||||||
cx: "0.5em",
|
cx: "0.5em",
|
||||||
cy: "0.5em",
|
cy: "0.5em",
|
||||||
r: "0.4em",
|
r: "0.4em",
|
||||||
fill: slice["color"],
|
fill: slice.color,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return m("span.overview-dot", dot, `${slice["label"]}`);
|
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,
|
||||||
@@ -30,7 +34,7 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"tr",
|
"div.py-3 grid grid-cols-3 gap-4 px-6",
|
||||||
{
|
{
|
||||||
oncreate: (vn) => {
|
oncreate: (vn) => {
|
||||||
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||||
@@ -43,15 +47,22 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
: "none";
|
: "none";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
m("th", evaluateExpression(parameter.label, device)),
|
m(
|
||||||
m("td", p),
|
"dt.text-sm font-medium text-stone-500",
|
||||||
|
evaluateExpression(parameter["label"], device).value,
|
||||||
|
),
|
||||||
|
m("dd.text-sm text-stone-900 col-span-2", p),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [vnode.attrs.deviceQuery] },
|
{ queries: [vnode.attrs.deviceQuery] },
|
||||||
m("table.parameter-list", rows),
|
m(
|
||||||
|
"dl.bg-white shadow-sm overflow-hidden rounded-lg w-max py-1",
|
||||||
|
{ class: "[&>*+*]:border-t [&>*+*]:border-stone-200" },
|
||||||
|
rows,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,78 +2,103 @@ 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 { getIcon } from "../icons.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) =>
|
const headers = parameters.map((p, i) => {
|
||||||
m("th", evaluateExpression(p["label"], device)),
|
const padding = i ? "px-3" : "pl-6 pr-3";
|
||||||
);
|
|
||||||
|
|
||||||
const thead = m("thead", m("tr", headers));
|
return m(
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
scope: "col",
|
||||||
|
class:
|
||||||
|
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||||
|
padding,
|
||||||
|
},
|
||||||
|
evaluateExpression(p.label, device).value,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push(m("th.pl-3", { scope: "col" }));
|
||||||
|
|
||||||
|
const thead = m("thead.bg-stone-50", m("tr", headers));
|
||||||
|
|
||||||
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) => {
|
const row = parameters.map((p, j) => {
|
||||||
const param = expressionParser.map(p.parameter, (e) => {
|
const padding = j ? "px-3" : "pl-6 pr-3";
|
||||||
if (Array.isArray(e) && e[0] === "PARAM")
|
|
||||||
return ["PARAM", ["||", i, ".", e[1]]];
|
const param = p.parameter.evaluate((e) => {
|
||||||
|
if (e instanceof Expression.Parameter)
|
||||||
|
return new Expression.Parameter(root.concat(e.path));
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let type = "parameter";
|
||||||
|
if (p["type"] instanceof Expression)
|
||||||
|
type = evaluateExpression(p["type"], device).value + "";
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"td",
|
"td",
|
||||||
|
{
|
||||||
|
class: "whitespace-nowrap py-4 text-sm text-stone-900 " + padding,
|
||||||
|
},
|
||||||
m.context(
|
m.context(
|
||||||
{
|
{
|
||||||
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,
|
||||||
@@ -83,10 +108,14 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (device[i].writable === true) {
|
if (device[i + ":writable"]) {
|
||||||
row.push(
|
row.push(
|
||||||
m(
|
m(
|
||||||
"td",
|
"td",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"whitespace-nowrap pl-3 pr-6 py-4 text-sm text-stone-900",
|
||||||
|
},
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
@@ -94,26 +123,38 @@ 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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("delete-instance"),
|
m(icon, {
|
||||||
|
name: "delete-instance",
|
||||||
|
class: "inline h-4 w-4 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
row.push(m("td"));
|
||||||
}
|
}
|
||||||
rows.push(m("tr", row));
|
rows.push(m("tr", row));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
rows.push(
|
rows.push(
|
||||||
m("tr.empty", m("td", { colspan: headers.length }, "No instances")),
|
m(
|
||||||
|
"tr",
|
||||||
|
m(
|
||||||
|
"td.bg-stripes text-sm font-medium text-center text-stone-500 p-4",
|
||||||
|
{ colspan: headers.length },
|
||||||
|
"No instances",
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device[object].writable === true) {
|
if (device[object.toString() + ":writable"]) {
|
||||||
rows.push(
|
rows.push(
|
||||||
m(
|
m(
|
||||||
"tr",
|
"tr",
|
||||||
@@ -127,12 +168,16 @@ 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(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("add-instance"),
|
m(icon, {
|
||||||
|
name: "add-instance",
|
||||||
|
class:
|
||||||
|
"inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -149,7 +194,14 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [vnode.attrs.deviceQuery] },
|
{ queries: [vnode.attrs.deviceQuery] },
|
||||||
m("table.table", thead, m("tbody", rows)),
|
m(
|
||||||
|
"div.shadow-sm overflow-hidden rounded-lg w-max",
|
||||||
|
m(
|
||||||
|
"table.divide-y divide-stone-200",
|
||||||
|
thead,
|
||||||
|
m("tbody.divide-y divide-stone-200 bg-white", rows),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
+49
-53
@@ -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 { getIcon } from "../icons.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,27 +71,25 @@ 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],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("edit"),
|
m(icon, {
|
||||||
|
name: "edit",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = m("long-text", { text: `${value}` });
|
const el = m("long-text", { text: `${value}` });
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"span",
|
"span.inline-flex overflow-hidden align-top",
|
||||||
{
|
{
|
||||||
class: "parameter-value",
|
|
||||||
onmouseover: (e) => {
|
onmouseover: (e) => {
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
// Don't update any child element
|
// Don't update any child element
|
||||||
@@ -104,7 +100,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
el,
|
m("span.truncate", el),
|
||||||
edit,
|
edit,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
try {
|
try {
|
||||||
const url = new URL(param.value[0]);
|
const url = new URL(param.value[0]);
|
||||||
h = url.hostname;
|
h = url.hostname;
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m("div", host ? `Pinging ${host}:` : "");
|
return m("div.text-sm my-4", host ? `Pinging ${host}:` : "");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,41 @@
|
|||||||
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.primary",
|
"button",
|
||||||
{
|
{
|
||||||
|
class:
|
||||||
|
"px-2.5 py-1.5 border border-transparent text-xs font-medium rounded-sm shadow-xs text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
title: "Initiate session and refresh basic parameters",
|
title: "Initiate session and refresh basic parameters",
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
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
|
||||||
|
|||||||
+45
-13
@@ -2,9 +2,9 @@ import { ClosureComponent } from "mithril";
|
|||||||
import { m } from "../components.ts";
|
import { m } from "../components.ts";
|
||||||
import * as notifications from "../notifications.ts";
|
import * as notifications from "../notifications.ts";
|
||||||
import * as store from "../store.ts";
|
import * as store from "../store.ts";
|
||||||
import { getIcon } from "../icons.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,29 +22,46 @@ 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();
|
||||||
|
|
||||||
if (!writable) {
|
if (!writable) {
|
||||||
return m(
|
return m(
|
||||||
".tags",
|
"div",
|
||||||
tags.map((t) => m("span.tag", t)),
|
tags.map((t) =>
|
||||||
|
m(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 mr-2 -my-0.5 ring-1 ring-yellow-200",
|
||||||
|
},
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
".tags",
|
"div",
|
||||||
tags.map((tag) =>
|
tags.map((tag) =>
|
||||||
m(
|
m(
|
||||||
"span.tag",
|
"span",
|
||||||
|
{
|
||||||
|
class:
|
||||||
|
"inline-flex items-center pl-3 pr-1 py-0.5 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800 mr-2 ring-1 ring-yellow-200",
|
||||||
|
},
|
||||||
tag,
|
tag,
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
|
title: "Remove tag",
|
||||||
|
class:
|
||||||
|
"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(() => {
|
||||||
@@ -64,19 +81,30 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("remove"),
|
m("span.sr-only", "Remove tag"),
|
||||||
|
m(icon, {
|
||||||
|
name: "remove",
|
||||||
|
class: "h-4 w-4",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
m(
|
m(
|
||||||
"span.tag.writable",
|
"span",
|
||||||
m.trust(" "),
|
{
|
||||||
|
class:
|
||||||
|
"inline-flex items-center pl-1 pr-1 py-0.5 rounded-full text-sm font-medium bg-yellow-50 ring-1 ring-yellow-200",
|
||||||
|
},
|
||||||
|
m.trust("​"),
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
|
title: "Add tag",
|
||||||
|
class:
|
||||||
|
"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;
|
||||||
@@ -96,7 +124,11 @@ const component: ClosureComponent<Attrs> = () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("add"),
|
m("span.sr-only", "Add tag"),
|
||||||
|
m(icon, {
|
||||||
|
name: "add",
|
||||||
|
class: "h-4 w-4",
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function structureConfig(config: Config[]): any {
|
|||||||
if (idx >= 0 && idx < MAX_ARRAY_SIZE && String(idx) === key) {
|
if (idx >= 0 && idx < MAX_ARRAY_SIZE && String(idx) === key) {
|
||||||
const pos = Math.floor(idx / MAX_BITS);
|
const pos = Math.floor(idx / MAX_BITS);
|
||||||
if (!indexes[pos]) indexes[pos] = 0;
|
if (!indexes[pos]) indexes[pos] = 0;
|
||||||
indexes[pos] |= 1 << idx % MAX_BITS;
|
indexes[pos] |= 1 << (idx % MAX_BITS);
|
||||||
} else {
|
} else {
|
||||||
indexes = [];
|
indexes = [];
|
||||||
break;
|
break;
|
||||||
|
|||||||
+45
-30
@@ -5,9 +5,9 @@ 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 { getIcon } from "./icons.ts";
|
import { icon } from "./tailwind-utility-components.ts";
|
||||||
|
|
||||||
const attributes = [
|
const attributes = [
|
||||||
{ id: "_id", label: "Key" },
|
{ id: "_id", label: "Key" },
|
||||||
@@ -30,8 +30,8 @@ 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 (err) {
|
} catch {
|
||||||
return void resolve({
|
return void resolve({
|
||||||
value: "Config value must be valid expression",
|
value: "Config value must be valid expression",
|
||||||
});
|
});
|
||||||
@@ -64,6 +64,7 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
|
if (!confirm("Deleting config. Are you sure?")) return void resolve(null);
|
||||||
store
|
store
|
||||||
.deleteResource("config", object["_id"])
|
.deleteResource("config", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -166,7 +167,10 @@ function renderTable(confsResponse, searchString): Children {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("edit"),
|
m(icon, {
|
||||||
|
name: "edit",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const del = m(
|
const del = m(
|
||||||
@@ -181,26 +185,32 @@ function renderTable(confsResponse, searchString): Children {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getIcon("remove"),
|
m(icon, {
|
||||||
|
name: "remove",
|
||||||
|
class: "inline h-4 w-4 ml-1 text-cyan-700 hover:text-cyan-900",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
m(
|
m(
|
||||||
"tr",
|
"tr",
|
||||||
attrs,
|
attrs,
|
||||||
m("td.left", m("long-text", { text: conf._id })),
|
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: conf._id })),
|
||||||
m(
|
m(
|
||||||
"td.right",
|
"td.px-2 py-2 text-right truncate",
|
||||||
m("span", [m("long-text", { text: `${conf.value}` }), edit, del]),
|
m("long-text", { text: `${conf.value}` }),
|
||||||
),
|
),
|
||||||
|
m("td.pl-2 pr-4 py-2 w-max", edit, del),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rows.length)
|
if (!rows.length) rows.push(m("tr", m("td", { colspan: 3 }, "No config")));
|
||||||
rows.push(m("tr.empty", m("td", { colspan: 2 }, "No config")));
|
|
||||||
|
|
||||||
return m("table", m("tbody", rows));
|
return m(
|
||||||
|
"table.w-full table-fixed font-mono text-sm text-stone-700",
|
||||||
|
m("tbody.bg-white divide-y divide-stone-200", rows),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
export const component: ClosureComponent = (): Component => {
|
||||||
@@ -208,25 +218,28 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = "Config - GenieACS";
|
document.title = "Config - GenieACS";
|
||||||
|
|
||||||
const search = m("input", {
|
const search = m(
|
||||||
type: "text",
|
"input.appearance-none block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 sm:text-sm rounded-md mt-1 max-w-screen-sm shadow-sm mb-5",
|
||||||
placeholder: "Search config",
|
{
|
||||||
oninput: (e) => {
|
type: "text",
|
||||||
vnode.state["searchString"] = e.target.value;
|
placeholder: "Search config",
|
||||||
e.redraw = false;
|
oninput: (e) => {
|
||||||
clearTimeout(vnode.state["timeout"]);
|
vnode.state["searchString"] = e.target.value;
|
||||||
vnode.state["timeout"] = setTimeout(m.redraw, 250);
|
e.redraw = false;
|
||||||
|
clearTimeout(vnode.state["timeout"]);
|
||||||
|
vnode.state["timeout"] = setTimeout(m.redraw, 250);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const confs = store.fetch("config", true);
|
const confs = store.fetch("config", new Expression.Literal(true));
|
||||||
|
|
||||||
let newConfig;
|
let newConfig;
|
||||||
const subs = [];
|
const subs = [];
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("config", 3)) {
|
if (window.authorizer.hasAccess("config", 3)) {
|
||||||
newConfig = m(
|
newConfig = m(
|
||||||
"button.primary",
|
"button.mr-4 px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
{
|
{
|
||||||
title: "Create new config",
|
title: "Create new config",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
@@ -295,7 +308,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data };
|
const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data };
|
||||||
subs.push(
|
subs.push(
|
||||||
m(
|
m(
|
||||||
"button",
|
"button.mr-4 px-4 py-2 border border-stone-300 shadow-xs text-sm font-medium rounded-md text-stone-700 bg-white hover:bg-stone-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500",
|
||||||
{
|
{
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
@@ -345,19 +358,21 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing config"),
|
m("h1.text-xl font-medium text-stone-900 mb-5", "Listing config"),
|
||||||
m(
|
m(
|
||||||
"loading",
|
"loading",
|
||||||
{ queries: [confs] },
|
{ queries: [confs] },
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
"div",
|
||||||
search,
|
search,
|
||||||
m(
|
m(
|
||||||
".parameter-list",
|
".shadow-sm overflow-hidden border-b border-stone-200 rounded-lg bg-white",
|
||||||
{ style: "height: 400px" },
|
m(
|
||||||
renderTable(confs, vnode.state["searchString"]),
|
".overflow-y-scroll h-96",
|
||||||
|
renderTable(confs, vnode.state["searchString"]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
m(".actions-bar", [newConfig].concat(subs)),
|
m(".mt-5", [newConfig].concat(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;
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user