@sergio9929/pb-query
Version:
A type-safe PocketBase query builder
301 lines (298 loc) • 7.59 kB
JavaScript
//#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 };