import type { FunctionMap, KV, Query, QueryExpression } from "../types.ts"; import { evalQueryExpression } from "./query_expression.ts"; /** * Looks for an attribute assignment in the expression, and returns the expression assigned to the attribute or throws an error when not found * Side effect: effectively removes the attribute assignment from the expression (by replacing it with true = true) */ export function liftAttributeFilter( expression: QueryExpression | undefined, attributeName: string, ): QueryExpression { if (!expression) { throw new Error(`Cannot find attribute assignment for ${attributeName}`); } switch (expression[0]) { case "=": { if (expression[1][0] === "attr" && expression[1][1] === attributeName) { const val = expression[2]; // Remove the filter by changing it to true = true expression[1] = ["boolean", true]; expression[2] = ["boolean", true]; return val; } break; } case "and": case "or": { const newOp1 = liftAttributeFilter(expression[1], attributeName); if (newOp1) { return newOp1; } const newOp2 = liftAttributeFilter(expression[2], attributeName); if (newOp2) { return newOp2; } throw new Error(`Cannot find attribute assignment for ${attributeName}`); } } throw new Error(`Cannot find attribute assignment for ${attributeName}`); } export async function applyQuery( query: Query, allItems: T[], variables: Record, functionMap: FunctionMap, ): Promise { // Filter if (query.filter) { const filteredItems: T[] = []; for (const item of allItems) { if ( await evalQueryExpression( query.filter, item, variables, functionMap, ) ) { filteredItems.push(item); } } allItems = filteredItems; } // Add dummy keys, then remove them return (await applyQueryNoFilterKV( query, allItems.map((v) => ({ key: [], value: v })), variables, functionMap, )).map((v) => v.value); } export async function applyQueryNoFilterKV( query: Query, allItems: KV[], variables: Record, functionMap: FunctionMap, ): Promise { // Order by if (query.orderBy) { allItems = allItems.sort((a, b) => { const aVal = a.value; const bVal = b.value; for (const { expr, desc } of query.orderBy!) { const evalA = evalQueryExpression( expr, aVal, variables, functionMap, ); if (evalA instanceof Promise) { throw new Error("Cannot order by a promise"); } const evalB = evalQueryExpression( expr, bVal, variables, functionMap, ); if (evalB instanceof Promise) { throw new Error("Cannot order by a promise"); } if ( evalA < evalB || evalA === undefined ) { return desc ? 1 : -1; } if ( evalA > evalB || evalB === undefined ) { return desc ? -1 : 1; } } // Consider them equal. This helps with comparing arrays (like tags) return 0; }); } if (query.select) { for (let i = 0; i < allItems.length; i++) { const rec = allItems[i].value; const newRec: any = {}; for (const { name, expr } of query.select) { newRec[name] = expr ? await evalQueryExpression(expr, rec, variables, functionMap) : rec[name]; } allItems[i].value = newRec; } } if (query.distinct) { // Remove duplicates const valueSet = new Set(); const uniqueItems: KV[] = []; for (const item of allItems) { const value = JSON.stringify(item.value); if (!valueSet.has(value)) { valueSet.add(value); uniqueItems.push(item); } } allItems = uniqueItems; } if (query.limit) { const limit = await evalQueryExpression( query.limit, {}, variables, functionMap, ); if (allItems.length > limit) { allItems = allItems.slice(0, limit); } } return allItems; }