@wener/miniquery
Version:
SQL Where like **safe** filter expression for ORM.
139 lines (132 loc) • 4.19 kB
text/typescript
import type { ActionDict, IterationNode, MatchResult } from 'ohm-js';
import { MiniQueryGrammar, MiniQuerySemantics } from './grammar';
export type MiniQueryASTNode =
| { type: 'identifier'; name: string }
| { type: 'logic'; op: 'and' | 'or'; a: MiniQueryASTNode; b: MiniQueryASTNode }
| {
type: 'rel';
op: 'gt' | 'lt' | 'gte' | 'lte' | 'eq' | 'ne' | 'like' | 'has' | 'in' | 'not in' | 'not like' | 'is' | 'is not';
a: MiniQueryASTNode;
b: MiniQueryASTNode;
}
| { type: 'between'; op: 'between' | 'not between'; a: MiniQueryASTNode; b: MiniQueryASTNode; c: MiniQueryASTNode }
| { type: 'call'; name: string; value: MiniQueryASTNode[] }
| { type: 'unary'; op: 'pos' | 'neg' | 'not'; value: MiniQueryASTNode }
| { type: 'paren'; value: MiniQueryASTNode }
| { type: 'array'; value: MiniQueryASTNode[] }
| { type: 'int'; value: number }
| { type: 'float'; value: number }
| { type: 'string'; value: string }
| { type: 'null' }
| { type: 'bool'; value: boolean }
| { type: 'ref'; name: string[] };
const Ops: Record<string, string> = {
'&&': 'and',
'||': 'or',
'=': 'eq',
'==': 'eq',
'!=': 'ne',
'<>': 'ne',
'>': 'gt',
'>=': 'gte',
'<': 'lt',
'<=': 'lte',
':': 'has',
};
export function getMiniQueryASTOp(v: string) {
const s = v.toLowerCase().trim().replaceAll(/\s+/g, ' ');
return Ops[s] || s;
}
const actions: ActionDict<any> = {
nonEmpty(first, _, rest: IterationNode, _pad) {
return [first].concat(rest.children).map((v) => v.toAST());
},
rel(a, op, b) {
let v = op.sourceString;
if (op.isIteration()) {
v = op.children.map((vv) => vv.sourceString).join(' ');
}
return { type: 'rel', op: getMiniQueryASTOp(v) as any, a: a.toAST(), b: b.toAST(), v: 'bool' };
},
empty() {
return [];
},
};
export function toMiniQueryAST(s: string | MatchResult) {
let match: MatchResult;
if (typeof s === 'string') {
match = MiniQueryGrammar.match(s);
} else {
match = s;
}
if (match.failed()) {
throw new SyntaxError(`Invalid MiniQuery: ${match.message}`);
}
return MiniQuerySemantics(match).toAST();
}
MiniQuerySemantics.addOperation<MiniQueryASTNode>('toAST()', {
Main(expr, _) {
return expr.toAST();
},
LogicExpr_match(a, op, b) {
return { type: 'logic', op: getMiniQueryASTOp(op.sourceString) as any, a: a.toAST(), b: b.toAST(), v: 'bool' };
},
RelExpr_match: actions.rel,
RelExpr_match_eq: actions.rel,
RelExpr_has: actions.rel,
InExpr_match: actions.rel,
PredicateExpr_like: actions.rel,
PredicateExpr_is: actions.rel,
BetweenExpr_match(a, op, b, _, c) {
return { type: 'between', op: op.sourceString as any, a: a.toAST(), b: b.toAST(), c: c.toAST(), v: 'bool' };
},
CallExpr_match(n, _, v, _end) {
return { type: 'call', name: n.sourceString, value: v.toAST() };
},
PriExpr_paren(_, v, _end) {
return { type: 'paren', value: v.toAST() };
},
PriExpr_not(op, v) {
return { type: 'unary', op: getMiniQueryASTOp(op.sourceString) as any, value: v.toAST(), v: 'bool' };
},
PriExpr_pos(op, v) {
return { type: 'unary', op: getMiniQueryASTOp(op.sourceString) as any, value: v.toAST() };
},
PriExpr_neg(op, v) {
return { type: 'unary', op: getMiniQueryASTOp(op.sourceString) as any, value: v.toAST() };
},
Array(_, list, _end) {
return { type: 'array', value: list.toAST(), v: Array };
},
int: (s, _, v) => {
return { type: 'int', value: parseInt(`${s.sourceString || ''}${v.sourceString}`), v: 'int' };
},
float: (i, _, f) => {
return { type: 'float', value: parseFloat(`${i?.sourceString || 0}.${f.sourceString}`), v: Number };
},
string: (_, v, _end) => {
return { type: 'string', value: v.sourceString, v: 'string' };
},
ident: (a, b) => {
return { type: 'identifier', name: [a, b].map((v) => v.sourceString).join('') };
},
null: (_) => {
return { type: 'null', v: 'null' };
},
bool: (v) => {
return { type: 'bool', value: v.sourceString.toLowerCase() === 'true', v: 'bool' };
},
ref(a, _b, c: IterationNode) {
return {
type: 'ref',
name: [a.sourceString].concat(
c.children.map((v) => {
const ast = v.toAST();
return ast.name || ast.value;
}),
),
};
},
TrailNonEmptyListOf: actions.nonEmpty,
EmptyListOf: actions.empty,
});