models/Node.ts
3.5 KB · sha256:b8ef4860c1806e435a60fbeb2a54c93928ed504d1c66390b0bd944bce1fd3861
import "reflect-metadata";
import { field, model } from "../db/odm";
export type NodeType =
| "permission"
| "inheritance"
| "prefix"
| "suffix"
| "weight"
| "meta";
export interface NodeContext {
server?: string;
world?: string;
extra?: Record<string, string>;
}
@model("Node")
export class Node {
@field({ type: String, required: true, index: true })
key!: string;
@field({ type: Boolean, default: true })
value!: boolean;
@field({
type: String,
enum: ["permission", "inheritance", "prefix", "suffix", "weight", "meta"],
default: "permission",
index: true,
})
type!: NodeType;
@field({ type: Object, default: () => ({}) })
context!: NodeContext;
@field({ type: Number, default: null, index: true })
expiry!: number | null;
@field({ type: Number, default: 0 })
priority!: number;
}
export function inferNodeType(key: string): NodeType {
if (key.startsWith("group.")) return "inheritance";
if (key.startsWith("prefix.")) return "prefix";
if (key.startsWith("suffix.")) return "suffix";
if (key.startsWith("weight.")) return "weight";
if (key.startsWith("meta.")) return "meta";
return "permission";
}
export function parseAffix(
key: string,
): { priority: number; text: string } | null {
const m = /^(?:prefix|suffix)\.(-?\d+)\.(.*)$/s.exec(key);
if (!m) return null;
return { priority: Number(m[1]), text: m[2] };
}
export function parseWeight(key: string): number | null {
const m = /^weight\.(-?\d+)$/.exec(key);
return m ? Number(m[1]) : null;
}
export function parseMeta(
key: string,
): { metaKey: string; metaValue: string } | null {
const m = /^meta\.([^.]+)\.(.*)$/s.exec(key);
if (!m) return null;
return { metaKey: m[1], metaValue: m[2] };
}
export function inheritedGroupName(key: string): string | null {
return key.startsWith("group.") ? key.slice("group.".length) : null;
}
export const nodeKey = {
perm: (perm: string) => perm,
group: (name: string) => `group.${name.toLowerCase()}`,
prefix: (priority: number, text: string) => `prefix.${priority}.${text}`,
suffix: (priority: number, text: string) => `suffix.${priority}.${text}`,
weight: (n: number) => `weight.${n}`,
meta: (k: string, v: string) => `meta.${k}.${v}`,
};
/** True if a node is currently live (not expired). */
export function isActive(n: Pick<Node, "expiry">, now = Date.now()): boolean {
return n.expiry == null || n.expiry > now;
}
/** Context equality used when adding/removing to avoid duplicate nodes. */
export function sameContext(a: NodeContext = {}, b: NodeContext = {}): boolean {
if ((a.server ?? null) !== (b.server ?? null)) return false;
if ((a.world ?? null) !== (b.world ?? null)) return false;
const ae = a.extra ?? {};
const be = b.extra ?? {};
const ak = Object.keys(ae);
const bk = Object.keys(be);
if (ak.length !== bk.length) return false;
return ak.every((k) => ae[k] === be[k]);
}
/** Does a node's context apply within a query context? (node ⊆ query) */
export function contextApplies(
node: NodeContext = {},
query: NodeContext = {},
): boolean {
if (node.server && node.server !== query.server) return false;
if (node.world && node.world !== query.world) return false;
if (node.extra) {
for (const [k, v] of Object.entries(node.extra)) {
if ((query.extra ?? {})[k] !== v) return false;
}
}
return true;
}