refactor: replace array-based expressions with typed class hierarchy

Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+30 -30
View File
@@ -1,5 +1,5 @@
import { PermissionSet, Expression } from "../types.ts";
import { evaluate, or } from "./expression/util.ts";
import { PermissionSet } from "../types.ts";
import Expression from "./expression.ts";
export default class Authorizer {
declare private permissionSets: PermissionSet[];
@@ -43,12 +43,12 @@ export default class Authorizer {
if (this.getFilterCache.has(cacheKey))
return this.getFilterCache.get(cacheKey);
let filter: Expression = null;
let filter: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
if (perm[resourceType]) {
if (perm[resourceType].access >= access)
filter = or(filter, perm[resourceType].filter);
filter = Expression.or(filter, perm[resourceType].filter);
}
}
}
@@ -64,7 +64,7 @@ export default class Authorizer {
if (this.validatorCache.has(resource))
return this.validatorCache.get(resource);
const validators: Expression[] = [];
let validators: Expression = new Expression.Literal(false);
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
@@ -73,7 +73,7 @@ export default class Authorizer {
perm[resourceType].access >= 3 &&
perm[resourceType].validate
)
validators.push(perm[resourceType].validate);
validators = Expression.or(validators, perm[resourceType].validate);
}
}
@@ -82,8 +82,6 @@ export default class Authorizer {
mutation: any,
any: any,
): boolean => {
if (!validators.length) return false;
const object = {
mutationType,
mutation,
@@ -92,31 +90,33 @@ export default class Authorizer {
options: any,
};
const valueFunction = (paramName): any => {
const entry = paramName.split(".", 1)[0];
paramName = paramName.slice(entry.length + 1);
let value = null;
if (["mutation", "options"].includes(entry)) {
value = object[entry];
for (const seg of paramName.split(".")) {
if (value == null) break;
if (typeof value !== "object") value = null;
else value = value[seg];
const now = Date.now();
const res = validators.evaluate((exp) => {
if (exp instanceof Expression.Literal) return exp;
if (exp instanceof Expression.Parameter) {
if (exp.path.colon) return new Expression.Literal(null);
const entry = exp.path.segments[0] as string;
const paramName = exp.path.slice(1);
let value = null;
if (["mutation", "options"].includes(entry)) {
value = object[entry];
for (const seg of paramName.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]) {
if (paramName) value = object[entry][paramName];
else value = object[entry];
return new Expression.Literal(value);
} else if (exp instanceof Expression.FunctionCall) {
if (exp.name === "NOW") return new Expression.Literal(now);
}
return new Expression.Literal(null);
}).value;
return value;
};
const res = evaluate(
validators.length > 1 ? ["OR", validators] : validators[0],
valueFunction,
Date.now(),
);
return !Array.isArray(res) && !!res;
return !!res;
};
this.validatorCache.set(resource, validator);