silverbullet/plug-api/lib/query_expression.ts

289 lines
8.0 KiB
TypeScript

import type { FunctionMap, QueryExpression } from "../types.ts";
export function evalQueryExpression(
val: QueryExpression,
obj: any,
variables: Record<string, any>,
functionMap: FunctionMap,
): Promise<any> | any {
const [type, op1] = val;
switch (type) {
// Logical operators
case "and": {
const op1Val = evalQueryExpression(
op1,
obj,
variables,
functionMap,
);
if (op1Val instanceof Promise) {
return op1Val.then((v) =>
v &&
evalQueryExpression(val[2], obj, variables, functionMap)
);
} else if (op1Val) {
return evalQueryExpression(val[2], obj, variables, functionMap);
} else {
return op1Val;
}
}
case "or": {
const op1Val = evalQueryExpression(
op1,
obj,
variables,
functionMap,
);
if (op1Val instanceof Promise) {
return op1Val.then((v) =>
v ||
evalQueryExpression(val[2], obj, variables, functionMap)
);
} else if (op1Val) {
return op1Val;
} else {
return evalQueryExpression(val[2], obj, variables, functionMap);
}
}
// Value types
case "null":
return null;
// TODO: Add this to the actualy query syntax
case "not": {
const val = evalQueryExpression(op1, obj, variables, functionMap);
if (val instanceof Promise) {
return val.then((v) => !v);
} else {
return !val;
}
}
case "number":
case "string":
case "boolean":
return op1;
case "regexp":
return [op1, val[2]];
case "attr": {
const attributeVal = obj;
if (val.length === 3) {
const attributeVal = evalQueryExpression(
val[1],
obj,
variables,
functionMap,
);
if (attributeVal instanceof Promise) {
return attributeVal.then((v) => v[val[2]]);
} else if (attributeVal) {
return attributeVal[val[2]];
} else {
return null;
}
} else if (!val[1]) {
return obj;
} else {
let attrVal = attributeVal[val[1]];
const func = functionMap[val[1]];
if (attrVal === undefined && func !== undefined) {
// Fallback to function call, if one is defined with this name
attrVal = func(variables);
}
return attrVal;
}
}
case "global": {
return variables[op1];
}
case "array": {
const argValues = op1.map((v) =>
evalQueryExpression(v, obj, variables, functionMap)
);
// Check if any arg value is a promise, and if so wait for it
const waitForPromises: Promise<void>[] = [];
for (let i = 0; i < argValues.length; i++) {
if (argValues[i] instanceof Promise) {
// Wait and replace in-place
waitForPromises.push(argValues[i].then((v: any) => argValues[i] = v));
}
}
if (waitForPromises.length > 0) {
return Promise.all(waitForPromises).then(() => argValues);
} else {
return argValues;
}
}
case "object": {
const objEntries: [string, any][] = op1.map((
[key, expr],
) => [key, evalQueryExpression(expr, obj, variables, functionMap)]);
// Check if any arg value is a promise, and if so wait for it
const waitForPromises: Promise<void>[] = [];
for (let i = 0; i < objEntries.length; i++) {
if (objEntries[i][1] instanceof Promise) {
// Wait and replace in-place
waitForPromises.push(
objEntries[i][1].then((v: any) => objEntries[i] = v),
);
}
}
if (waitForPromises.length > 0) {
return Promise.all(waitForPromises).then(() =>
Object.fromEntries(objEntries)
);
} else {
return Object.fromEntries(objEntries);
}
}
case "pageref": {
const readPageFunction = functionMap.readPage;
if (!readPageFunction) {
throw new Error(`No readPage function defined`);
}
return readPageFunction(op1);
}
case "query": {
const parsedQuery = val[1];
const queryFunction = functionMap.$query;
if (!queryFunction) {
throw new Error(`No $query function defined`);
}
return queryFunction(parsedQuery, variables);
}
case "call": {
const fn = functionMap[op1];
if (!fn) {
throw new Error(`Unknown function: ${op1}`);
}
const argValues = val[2].map((v) =>
evalQueryExpression(v, obj, variables, functionMap)
);
// Check if any arg value is a promise, and if so wait for it
const waitForPromises: Promise<void>[] = [];
for (let i = 0; i < argValues.length; i++) {
if (argValues[i] instanceof Promise) {
// Wait and replace in-place
waitForPromises.push(argValues[i].then((v: any) => argValues[i] = v));
}
}
if (waitForPromises.length > 0) {
return Promise.all(waitForPromises).then(() => fn(...argValues));
} else {
return fn(...argValues);
}
}
case "-": {
if (val.length == 2) { // only unary minus
const val = evalQueryExpression(op1, obj, variables, functionMap);
return -val;
}
}
}
// Binary operators, here we can pre-calculate the two operand values
const val1 = evalQueryExpression(
op1,
obj,
variables,
functionMap,
);
const val2 = evalQueryExpression(
val[2],
obj,
variables,
functionMap,
);
const val3 = val[3]
? evalQueryExpression(
val[3],
obj,
variables,
functionMap,
)
: undefined;
if (
val1 instanceof Promise || val2 instanceof Promise ||
val3 instanceof Promise
) {
return Promise.all([val1, val2, val3]).then(([v1, v2, v3]) =>
evalSimpleExpression(type, v1, v2, v3)
);
} else {
return evalSimpleExpression(type, val1, val2, val3);
}
}
function evalSimpleExpression(type: string, val1: any, val2: any, val3: any) {
switch (type) {
case "+":
return val1 + val2;
case "-":
return val1 - val2;
case "*":
return val1 * val2;
case "/":
return val1 / val2;
case "%":
return val1 % val2;
case "=": {
if (Array.isArray(val1) && !Array.isArray(val2)) {
// Record property is an array, and value is a scalar: find the value in the array
return val1.includes(val2);
} else if (Array.isArray(val1) && Array.isArray(val2)) {
// Record property is an array, and value is an array: compare the arrays
return val1.length === val2.length &&
val1.every((v) => val2.includes(v));
}
return val1 == val2;
}
case "!=": {
if (Array.isArray(val1) && !Array.isArray(val2)) {
// Record property is an array, and value is a scalar: find the value in the array
return !val1.includes(val2);
} else if (Array.isArray(val1) && Array.isArray(val2)) {
// Record property is an array, and value is an array: compare the arrays
return !(val1.length === val2.length &&
val1.every((v) => val2.includes(v)));
}
return val1 != val2;
}
case "=~": {
if (typeof val2 === "string") {
val2 = [val2, "i"];
}
if (!Array.isArray(val2)) {
throw new Error(`Invalid regexp: ${val2}`);
}
const r = new RegExp(val2[0], val2[1]);
return r.test(val1);
}
case "!=~": {
if (typeof val2 === "string") {
val2 = [val2, "i"];
}
if (!Array.isArray(val2)) {
throw new Error(`Invalid regexp: ${val2}`);
}
const r = new RegExp(val2[0], val2[1]);
return !r.test(val1);
}
case "<":
return val1 < val2;
case "<=":
return val1 <= val2;
case ">":
return val1 > val2;
case ">=":
return val1 >= val2;
case "in":
return val2.includes(val1);
case "?":
return val1 ? val2 : val3;
case "??":
return val1 ?? val2;
default:
throw new Error(`Unupported operator: ${type}`);
}
}