UNPKG

@opra/common

Version:
147 lines (146 loc) 6.16 kB
import '../polifils/array-find-last.js'; import { omitUndefined } from '@jsopen/objects'; import { isString } from 'valgen'; import { OpraException } from '../exception/index.js'; import { ResponsiveMap } from '../helpers/index.js'; import { ArithmeticExpression, ArrayExpression, ComparisonExpression, Literal, LogicalExpression, ParenthesizedExpression, QualifiedIdentifier, } from './ast/index.js'; import { parse } from './parse.js'; export class FilterRules { constructor(rules, options) { this._rules = new ResponsiveMap(); this._decoderCache = new WeakMap(); Object.defineProperty(this, '_rules', { value: new ResponsiveMap(null, { caseSensitive: options?.caseSensitive }), enumerable: false, }); if (rules) { for (const [k, v] of Object.entries(rules)) { this.set(k, v); } } } set(fieldName, options) { const operators = typeof options?.operators === 'string' ? options.operators.split(/\s*[,| ]\s*/) : options?.operators; this._rules.set(fieldName, omitUndefined({ ...options, operators, })); } normalizeFilter(filter, currentType, scope) { const ast = typeof filter === 'string' ? parse(filter) : filter; return this.normalizeFilterAst(ast, [], currentType, scope); } normalizeFilterAst(ast, stack, currentType, scope) { if (ast instanceof ComparisonExpression) { stack.push(ast); this.normalizeFilterAst(ast.left, stack, currentType); if (!(ast.left instanceof QualifiedIdentifier && ast.left.field)) { throw new TypeError(`Invalid filter query. Left side should be a data field.`); } // Check if filtering accepted for given field const rule = this._rules.get(ast.left.value); if (!rule) { throw new OpraException({ message: `Field '${ast.left.value}' is not available for filter operation`, code: 'UNACCEPTED_FILTER_FIELD', details: { field: ast.left.value, }, }); } // Check if filtering endpoint accepted for given field if (rule.operators && !rule.operators.includes(ast.op)) { throw new OpraException({ message: `'${ast.left.value}' field do not accept '${ast.op}' filter operator`, code: 'UNACCEPTED_FILTER_OPERATION', details: { field: ast.left.value, operator: ast.op, }, }); } if (rule.mappedField) ast.left.value = rule.mappedField; if (rule.prepare) ast.prepare = rule.prepare; this.normalizeFilterAst(ast.right, stack, currentType); stack.pop(); return ast; } if (ast instanceof LogicalExpression) { stack.push(ast); ast.items.forEach(item => this.normalizeFilterAst(item, stack, currentType)); stack.pop(); return ast; } if (ast instanceof ArithmeticExpression) { stack.push(ast); ast.items.forEach(item => this.normalizeFilterAst(item.expression, stack, currentType)); stack.pop(); return ast; } if (ast instanceof ArrayExpression) { stack.push(ast); ast.items.forEach(item => this.normalizeFilterAst(item, stack, currentType)); stack.pop(); return ast; } if (ast instanceof ParenthesizedExpression) { stack.push(ast); this.normalizeFilterAst(ast.expression, stack, currentType); stack.pop(); return ast; } if (ast instanceof QualifiedIdentifier && currentType) { ast.value = currentType.normalizeFieldPath(ast.value, { scope }); ast.field = currentType.getField(ast.value, scope); ast.dataType = ast.field.type; return ast; } if (ast instanceof Literal) { /** Check if comparison expression has in stack */ const compIdx = stack.findLastIndex(x => x instanceof ComparisonExpression); if (compIdx >= 0) { const comp = stack[compIdx]; /** If calling for right side of comparison */ if (ast === comp.right || stack[compIdx + 1] === comp.right) { /** Check if comparison expression left side is a field */ if (comp && comp.left instanceof QualifiedIdentifier && comp.left.field) { if (ast.value == null && !comp.left.field.required) return ast.value; let decoder; if (comp.op === 'like' || comp.op === '!like' || comp.op === 'ilike' || comp.op === '!ilike') { decoder = isString; } else decoder = this._decoderCache.get(comp.left.field); if (!decoder) { decoder = comp.left.field.type.generateCodec('decode', { scope, projection: '*', ignoreWriteonlyFields: true, coerce: true, }); this._decoderCache.set(comp.left.field, decoder); } ast.value = decoder(ast.value); } } } } return ast; } toJSON() { return this._rules.toObject(); } [Symbol.iterator]() { return this._rules.entries(); } }