import type { FunctionMap, QueryExpression } from "../types.ts"; export function evalQueryExpression( val: QueryExpression, obj: any, variables: Record, functionMap: FunctionMap, ): Promise | 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[] = []; 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[] = []; 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[] = []; 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}`); } }