UNPKG

@sergio9929/pb-query

Version:

A type-safe PocketBase query builder

301 lines (298 loc) 7.59 kB
//#region src/constants.ts const OPERATORS = { equal: "=", notEqual: "!=", greaterThan: ">", greaterThanOrEqual: ">=", lessThan: "<", lessThanOrEqual: "<=", like: "~", notLike: "!~", anyEqual: "?=", anyNotEqual: "?!=", anyGreaterThan: "?>", anyGreaterThanOrEqual: "?>=", anyLessThan: "?<", anyLessThanOrEqual: "?<=", anyLike: "?~", anyNotLike: "?!~" }; const DATETIME_MACROS = [ "@now", "@second", "@minute", "@hour", "@weekday", "@day", "@month", "@year", "@yesterday", "@tomorrow", "@todayStart", "@todayEnd", "@monthStart", "@monthEnd", "@yearStart", "@yearEnd" ]; //#endregion //#region src/utils.ts /** * We expose a filter function, but we recommend using the native `pb.filter()` function instead. * @deprecated Use native `pb.filter()`, not this. */ function filter(raw, params) { if (!params) return raw; let sanitizedQuery = raw; for (const key in params) { let val = params[key]; switch (typeof val) { case "boolean": case "number": val = `${val}`; break; case "string": val = `'${val.replace(/'/g, "\\'")}'`; break; default: if (val === null) val = "null"; else if (val instanceof Date) val = `'${val.toISOString().replace("T", " ")}'`; else val = `'${JSON.stringify(val).replace(/'/g, "\\'")}'`; } sanitizedQuery = sanitizedQuery.replaceAll(`{:${key}}`, val); } return sanitizedQuery; } function isDateMacro(value) { if (!isMacro(value)) return false; return DATETIME_MACROS.includes(value); } function isMacro(value) { if (typeof value !== "string") return false; return value.length > 1 && value.startsWith("@"); } function generateFields(keys) { const uniqueKeys = [...new Set(keys)]; return uniqueKeys.join(","); } function prepareFieldsForExpand(keys) { const uniqueKeys = [...new Set(keys)]; const preparedKeys = uniqueKeys.map((key) => { const words = key.split("expand."); if (words.length > 1) return words.map((word) => { const dotIndex = word.indexOf("."); return word.slice(0, dotIndex < 0 ? word.length : dotIndex); }).filter(Boolean).join("."); return ""; }); return [...new Set(preparedKeys)]; } function generateExpand(keys) { const uniqueKeys = [...new Set(keys)]; return uniqueKeys.reduce((acc, word, wordIndex, arr) => { const canBeIgnored = arr.some((x, xIndex) => { if (wordIndex === xIndex) return false; if (x?.startsWith(word)) return true; return false; }); if (!canBeIgnored) acc.push(word); return acc; }, []).join(","); } function generateSort(keys) { const uniqueKeys = [...new Set(keys)]; return uniqueKeys.join(","); } function cleanQuery(query) { if (!query?.trim()) return query || ""; const steps = [ removeOperatorsAfterOpeningParenthesis, removeOperatorsBeforeClosingParenthesis, removeStackedOperators, removeTrailingOperators, normalize ]; return steps.reduce((result, step) => step(result), query); } const AND = "&&"; const OR = "\\|\\|"; const OP = `(?:${AND}|${OR})`; const OP_SEQ = `${OP}(?:\\s*${OP})*`; function normalize(str) { return str.replace(/\s+/g, " ").trim(); } function removeOperatorsAfterOpeningParenthesis(str) { return str.replace(new RegExp(`\\(\\s*${OP_SEQ}\\s*`, "g"), "("); } function removeOperatorsBeforeClosingParenthesis(str) { return str.replace(new RegExp(`\\s*${OP_SEQ}\\s*\\)`, "g"), ")"); } function removeStackedOperators(str) { return str.replace(new RegExp(`${OP}\\s+${OP}`, "g"), ""); } function removeTrailingOperators(str) { return str.replace(new RegExp(`${OP}\\s*$`, "g"), ""); } //#endregion //#region src/query.ts function pbQuery() { let query = ""; let fields = ""; let expand = ""; let sort = ""; const keyCounter = /* @__PURE__ */ new Map(); const valueMap = /* @__PURE__ */ new Map(); const incrementKeyCounter = (key) => { const count = keyCounter.get(key) || 0; const newCount = count + 1; keyCounter.set(key, newCount); return newCount; }; const saveValue = (key, value) => { const count = incrementKeyCounter(key); const newName = `${String(key)}${count}`; valueMap.set(newName, value); return newName; }; const expression = (key, operator, value) => { if (isDateMacro(value)) query += `${String(key)}${operator}${value}`; else { const newName = saveValue(key, value); query += `${String(key)}${operator}{:${newName}}`; } }; const builderFunctions = {}; for (const [name, operator] of Object.entries(OPERATORS)) { const key = name; builderFunctions[key] = (key$1, value) => { expression(key$1, operator, value); return restrictedQueryBuilder; }; } function build(filter$1) { const cleanedQuery = cleanQuery(query); if (typeof filter$1 === "function") return { expand, fields, filter: filter$1(cleanedQuery, Object.fromEntries(valueMap)), sort }; return { expand, fields, filter: { raw: cleanedQuery, values: Object.fromEntries(valueMap) }, sort }; } function applySort(keys) { if (sort) console.warn("Overriding previous sort:", sort); const normalizedKeys = Array.isArray(keys) ? keys : [keys]; sort = generateSort(normalizedKeys); } const queryBuilder = { ...builderFunctions, search(keys, value) { query += "("; const cleanedPaths = keys.filter((key) => key); cleanedPaths.forEach((key, index) => { expression(key, "~", value); query += index < cleanedPaths.length - 1 ? " || " : ""; }); query += ")"; return restrictedQueryBuilder; }, in(key, values) { query += "("; values.forEach((value, index) => { expression(key, "=", value); query += index < values.length - 1 ? " || " : ""; }); query += ")"; return restrictedQueryBuilder; }, notIn(key, values) { query += "("; values.forEach((value, index) => { expression(key, "!=", value); query += index < values.length - 1 ? " && " : ""; }); query += ")"; return restrictedQueryBuilder; }, between(key, from, to) { query += "("; expression(key, ">=", from); query += " && "; expression(key, "<=", to); query += ")"; return restrictedQueryBuilder; }, notBetween(key, from, to) { query += "("; expression(key, "<", from); query += " || "; expression(key, ">", to); query += ")"; return restrictedQueryBuilder; }, isNull(key) { query += `${String(key)}=''`; return restrictedQueryBuilder; }, isNotNull(key) { query += `${String(key)}!=''`; return restrictedQueryBuilder; }, custom(raw) { query += raw; return restrictedQueryBuilder; }, group(callback) { query += "("; callback(queryBuilder); query += ")"; return restrictedQueryBuilder; }, sort(keys) { applySort(keys); return queryBuilder; }, build }; const queryBuilderStart = { ...queryBuilder, fields(keys) { if (fields) console.warn("Overriding previous fields:", fields); const normalizedKeys = Array.isArray(keys) ? keys : [keys]; fields = generateFields(normalizedKeys); expand ||= generateExpand(prepareFieldsForExpand(normalizedKeys)); return queryBuilderStart; }, expand(keys) { if (expand) console.warn("Overriding previous expand:", expand); const normalizedKeys = Array.isArray(keys) ? keys : [keys]; expand = generateExpand(normalizedKeys); return queryBuilderStart; } }; const restrictedQueryBuilder = { and() { query += " && "; return queryBuilder; }, or() { query += " || "; return queryBuilder; }, sort(keys) { applySort(keys); return restrictedQueryBuilder; }, build }; return queryBuilderStart; } //#endregion export { OPERATORS, filter, pbQuery };