@wener/miniquery
Version:
SQL Where like **safe** filter expression for ORM.
122 lines (110 loc) • 3.17 kB
text/typescript
import { DisableKey, IgnoreKeyPrefix } from './const';
import type { AnyDocumentQuery } from './types';
export function formatDocumentQuery(o: AnyDocumentQuery): string | undefined {
if (!o || Object.keys(o).length === 0) {
return undefined;
}
const conditions = _format(o, { path: [], out: [] });
if (conditions.length === 0) {
return undefined;
}
return conditions.join(' AND ');
}
function formatValue(v: any): string {
if (Array.isArray(v)) {
return `(${v.map(formatValue).join(', ')})`;
}
return JSON.stringify(v);
}
function _format(o: any, ctx: { path: string[]; out: string[] }): string[] {
if (o === undefined || o === null || (typeof o === 'object' && o[DisableKey] === true)) {
return ctx.out;
}
if (typeof o !== 'object' || Array.isArray(o)) {
throw new Error(`Invalid query segment: ${JSON.stringify(o)} at path ${ctx.path.join('.')}`);
}
const { path, out } = ctx;
for (const [k, v] of Object.entries(o).sort((a, b) => a[0].localeCompare(b[0]))) {
if (v === undefined || k.startsWith(IgnoreKeyPrefix)) {
continue;
}
if (k.startsWith('$')) {
const field = path.join('.');
switch (k) {
case '$or':
case '$and':
case '$nor': {
const queries = Array.isArray(v) ? v : Object.values(v || {});
const all = queries.map((vv: any) => formatDocumentQuery(vv)).filter(Boolean);
if (all.length > 0) {
const op = k === '$and' ? 'AND' : 'OR';
const joined = all.length > 1 ? `(${all.join(` ${op} `)})` : all[0];
out.push(k === '$nor' ? `NOT (${joined})` : (joined as string));
}
break;
}
case '$not': {
const subQuery = formatDocumentQuery(v!);
if (subQuery) {
out.push(`NOT (${subQuery})`);
}
break;
}
case '$exists':
out.push(`${field} IS ${v ? 'NOT NULL' : 'NULL'}`);
break;
case '$size':
out.push(`LENGTH(${field}) = ${v}`);
break;
case '$all':
if (!Array.isArray(v)) throw new Error('$all requires an array');
v.forEach((item) => out.push(`CONTAINS(${field}, ${formatValue(item)})`));
break;
case '$elemMatch': {
const subQuery = formatDocumentQuery(v!);
if (subQuery) {
out.push(`ELEM_MATCH(${field}, '${subQuery.replace(/'/g, "''")}')`);
}
break;
}
default: {
const op = InfixOperator[k.slice(1)];
if (op && field) {
out.push(`${field} ${op} ${formatValue(v)}`);
} else {
throw new Error(`Unsupported operator: ${k} at path ${field}`);
}
}
}
continue;
}
const segments = k.split('.');
const currentPath = [...path, ...segments];
const field = currentPath.join('.');
if (v === null) {
out.push(`${field} IS NULL`);
} else if (Array.isArray(v)) {
out.push(`${field} IN ${formatValue(v)}`);
} else if (typeof v === 'object') {
_format(v, { ...ctx, path: currentPath });
} else {
out.push(`${field} = ${formatValue(v)}`);
}
}
return out;
}
const InfixOperator: Record<string, string> = {
eq: '=',
ne: '!=',
gt: '>',
gte: '>=',
lt: '<',
lte: '<=',
in: 'IN',
nin: 'NOT IN',
like: 'LIKE',
nlike: 'NOT LIKE',
regex: 'RLIKE',
type: 'TYPE',
expr: 'EXPR',
};