client/src/lang/capiQueryString.ts (95 lines of code) (raw):

import { err, ok, Result } from "../utils/result"; import { CqlQuery, CqlBinary, CqlExpr } from "./ast"; import { getCqlFieldsFromCqlBinary } from "./utils"; class CapiCqlStringError extends Error { public constructor(message: string) { super(message); } } const dateFields = ["from-date", "to-date"]; const relativeDateRegex = /(?<polarity>[-+])(?<quantity>\d+)(?<unit>[dmyw])/; const add = (a: number, b: number) => a + b; const substract = (a: number, b: number) => a - b; const parseDateValue = (value: string): string => { const result = relativeDateRegex.exec(value); if (!result) { return value; } const now = new Date(); const { polarity, quantity, unit } = result.groups as { polarity: string; quantity: string; unit: string; }; const op = polarity === "+" ? add : substract; const year = op(now.getFullYear(), unit === "y" ? parseInt(quantity) : 0); // Months are zero indexed in Javascript, ha ha ha const month = op(now.getMonth(), unit === "m" ? parseInt(quantity) : 0); const day = op( now.getDate(), unit === "d" ? parseInt(quantity) : unit === "w" ? parseInt(quantity) * 7 : 0 ); const date = new Date(year, month, day); return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; }; export const queryStrFromQueryList = ( query: CqlQuery ): Result<Error, string> => { const { content } = query; if (!content) { return ok(""); } const searchStrs = strFromBinary(content); try { const otherQueries = getCqlFieldsFromCqlBinary(content).flatMap((expr) => { switch (expr.type) { case "CqlField": { if (expr.value) { const value = dateFields.includes(expr.key.literal ?? "") ? parseDateValue(expr.value?.literal ?? "") : expr.value.literal; return [`${expr.key.literal ?? ""}=${value}`]; } else { throw new CapiCqlStringError( `The field '${expr.key.literal}' needs a value after it (e.g. '${expr.key.literal}:tone/news')` ); } } default: { return []; } } }); const queryStr = searchStrs.length ? `q=${encodeURI(searchStrs.trim())}` : ""; return ok([queryStr, otherQueries].filter(Boolean).flat().join("&")); } catch (e) { return err(e as Error); } }; const strFromContent = (queryContent: CqlExpr): string | undefined => { const { content } = queryContent; switch (content.type) { case "CqlStr": return content.searchExpr; case "CqlGroup": return `(${strFromBinary(content.content).trim()})`; case "CqlBinary": return strFromBinary(content); default: // Ignore fields return; } }; const strFromBinary = (queryBinary: CqlBinary): string => { const leftStr = strFromContent(queryBinary.left); const rightStr = queryBinary.right && // Something of a hack — don't include queryBinary.right.binary.left.content.type !== "CqlField" ? `${queryBinary.right.operator} ${strFromBinary(queryBinary.right.binary)}` : ""; return (leftStr ?? "") + (rightStr ? ` ${rightStr.trim()} ` : ""); };