UNPKG

javascript-rql

Version:

Javascript RQL lib to transform js object to rql query string

475 lines (408 loc) 11.4 kB
/* * Copyright © 2020 Ingram Micro Inc. All rights reserved. */ import { curry, forEachObjIndexed, flatten, ifElse, includes, identity, is, join, keys, objOf, of, pipe, map, test, replace, reject, unless, __, } from 'ramda'; import { alt, escapeStars, isObjectStrict, isNilOrEmpty, } from '@/utils'; export const RQL_EXPRESSIONS = { AND: '$and', EQ: '$eq', NE: '$ne', GT: '$gt', GTE: '$ge', LT: '$lt', LTE: '$le', LIKE: '$like', ILIKE: '$ilike', IN: '$in', OUT: '$out', RANGE: '$range', NOT: '$not', OR: '$or', SELECT: '$select', ORDERING: '$ordering', SORT: '$sort', LIMIT: '$limit', }; /** Checks that expression type is control kind * * @type {Function} * @param {String} type - expression type * @returns {Boolean} * * @summary String -> String * @example ('$select') -> true */ const isControlRqlExp = includes(__, [ RQL_EXPRESSIONS.SELECT, RQL_EXPRESSIONS.ORDERING, RQL_EXPRESSIONS.SORT, ]); /** Wraps primitive expression value in quotation marks * * @type {Function} * @param {String|Number} value - expression value * @returns {String} * * @summary String -> String * @example ('a b') -> '"a b"' */ const quotize = unless(test(/^([\w\-*+\\][\w.\-:+@*\\]*|null\(\)|empty\(\))$/g), (v) => `"${v}"`); /** Transform tech operation name to rql operation name (removes $) * * @type {Function} * @param {String} operation * @returns {String} * * @summary String -> String * @example ('$select') -> 'select' */ const $operation = replace(/^\$/g, ''); /** Wraps any expression value in quotation marks * * @type {Function} * @param {String|Number|Array} value - expression value * @returns {String} * * @summary String|Number|Array -> String * @example (['some test', 'test']) -> '"some test",test' */ const $value = ifElse( Array.isArray, pipe( map(quotize), join(','), ), quotize, ); /** Prepares query string applying formatter function * * @type {Function} * @param {Function} formatter - function to expression value * @param {String} field - field name * @param {String} operation - operation name * @param {String} value - expression value * @return {String} * * @summary Function -> String -> String -> String -> String * @example (v=>`*${v}*`, 'name', '$eq', 'Jone') -> 'eq(name,*Jone*)' */ const $query = curry((valueFormatter, field, operation, value) => alt( '', `${$operation(operation)}(${field},${valueFormatter(value)})`, isNilOrEmpty(value), )); /** Enhances function to preparing expression value * * @type {Function} * @param {String} field name * @param {String} operation name * @param {String} expression value * @return {String} * * @summary String -> String -> String -> String * @example ('name', '$eq', 'Jone Lone') -> 'eq(name,"Jone Lone")' */ const qRel = $query($value); /** Prepares default query param string * * @type {Function} * @param {String} field - param name * @param {String|Number|Array} value - param value * @returns {String} * * @summary (String, String) -> String * @example * ('name', 'Jone Lone') -> 'name="Jone Lone"' * ('name', undefined) -> '' */ const qEq = (field, value) => alt('', `${field}=${$value(value)}`, isNilOrEmpty(value)); /** Prepares array value to list searching * * @type {Function} * @param {Array} value - array expression value * @returns {String} * * @summary (String, String) -> String * @example * ('Jone') -> '*Jone*' * ('name', undefined) -> '' */ const qList = $query((v) => `(${$value(v)})`); /** Prepares range value to range searching * * @type {Function} * @param {Object} value - object with min and max * @returns {String} * * @summary {min: Number, max: Number} -> String * @example ({min:2,max:4}) -> '2,4' */ const qRange = $query(({ min, max }) => `${min},${max}`); /** Transforms array value to OR RQL expression string * * @type {Function} * @param {Array} value - array value to OR expression * @returns {String} * * @summary Array -> String * @example (['a','b', undefined, 'c','d']) -> '(((a|b)|c)|d)' */ const qOr = (values) => values.reduce((acc, v) => alt( acc, alt(`(${v})`, `(${acc}|${v})`, isNilOrEmpty(acc)), isNilOrEmpty(v), )); /** Wraps expression value to expression operator * * @type {Function} * @param {String} opration - expression operator * @param {String} value - expression value * @param {Function} formatter - function formatter for value * @returns {String} * * @summary (String, String, Function) -> String * @example * ('$ne', 'abc') -> 'ne(abc)' * ('$select', 'my field', snakeCase) -> 'select(my_field)' */ const qWrap = (operation, value, formatter = identity) => alt( '', `${$operation(operation)}(${formatter(value)})`, isNilOrEmpty(value), ); /** Prepares value to AND RQL expression * * @type {Function} * @param {Array} value - array value to AND expression * @returns {String} * * @summary Array -> String * @example (['a','b','c','d']) -> '(((a|b)|c)|d)' */ const qAnd = pipe( reject(isNilOrEmpty), join('&'), ); /** Prepares value to Limit RQL expression * * @type {Function} * @param {Object} limitParams - limit params (count and start) * @returns {String} * * @summary Array -> String * @example ({start: 10, count: 20}) -> 'limit(10,20)' */ const qLimit = ({ start = 0, count = 1000 }) => `limit(${start},${count})`; /** Prepares text value to non-strict searching * * @type {Function} * @param {String} value - text expression value * @returns {String} * * @summary (String, String) -> String * @example * ('Jone') -> '*Jone*' */ const qFullsearchText = $query(unless(isNilOrEmpty, (v) => $value(`*${escapeStars(v)}*`))); /** Prepares text value to starts with searching * * @type {Function} * @param {String} value - text expression value * @returns {String} * * @summary (Object) -> String * @example * ({start: 'Jone'}) -> 'Jone*' */ const qStartText = $query((v) => $value(`${escapeStars(v.start)}*`)); /** Prepares text value to end with searching * * @type {Function} * @param {String} value - text expression value * @returns {Object} * * @summary (Object) -> String * @example * ({end: 'Jone'}) -> '*Jone' */ const qEndText = $query((v) => $value(`*${escapeStars(v.end)}`)); /** Prepares text value to end/start searching * * @type {Function} * @param {String} value - text expression value * @returns {String} * * @summary (Object) -> String * @example * ({start: 'M', end: 'w'}) -> 'M*w' */ const qStartAndEndText = $query((v) => $value(`${escapeStars(v.start)}*${escapeStars(v.end)}`)); /** Prepares text value to pattern searching * * @type {Function} * @param {String} value - text expression value * @returns {String} * * @summary (Object) -> String * @example * ({pattern: 'C*u*t*y'}) -> 'C*u*t*y' */ const qPatternText = $query((v) => $value(v.pattern)); /* eslint-disable no-use-before-define */ /** Applies expression operation to the other rql expressions of field * * @type {Function} * @param {String} field - field (param name, entity prop) * @param {String} operation - expression operation * @param {Object|Array} rqlFilters - rql expressions which will be applied to the operation * @returns {String} * * @summary (String, String, Array|Object) -> String * @example * ('name', '$not', {$eq: 'Jon'}) -> ['not(eq(name, Jon))'] * ('name', '$not', [[{$eq:'Jon'}, {$eq: 'Mark'}]) -> ['not(eq(name,Jon))', 'not(eq(name,Mark))'] */ const toWrappedQueries = (field, operation, rqlFilters) => pipe( unless(is(Array), of), reject(isNilOrEmpty), map((rqlFilter) => map((subKey) => { const subRql = objOf(subKey, rqlFilter[subKey]); const subQuery = rqlToQuery(subRql, field); return qWrap(operation, subQuery); }, keys(rqlFilter))), flatten, )(rqlFilters); /** Transform rql object to query string and wraps it in brackets * * @type {Function} * @param {Object} rqlObject - rql object * @returns {String|undefined|null} * * @summary Object -> String|undefined|null * @example ({ age: { $in: [3, 4, 5] } }) -> '(in(age,(3,4,5)))' */ const toWrappedQuery = pipe( rql, unless(isNilOrEmpty, (v) => `(${v})`), ); /** Transforms rql expression of field to query string * * @type {Function} * @param {Object} rqlObject - rql object * @param {String} field - field (param name, entity prop) * @returns {String} * * @summary (Object, String) -> String * @example ({ $like: 'Jo', $ne: 'Joe' }) -> 'like(name, *Jo*)&ne(name, Joe)' */ export function rqlToQuery(rqlExp, field) { const rqlFilter = []; const toSubQueries = map(pipe( objOf(field), toWrappedQuery, )); forEachObjIndexed((value, operation) => { if (isNilOrEmpty(value)) return; switch (operation) { // Text matching case RQL_EXPRESSIONS.LIKE: case RQL_EXPRESSIONS.ILIKE: if (is(String, value)) { rqlFilter.push(qFullsearchText(field, operation, value)); } else if (isObjectStrict(value)) { if (value.start && value.end) { rqlFilter.push(qStartAndEndText(field, operation, value)); } else if (value.start) { rqlFilter.push(qStartText(field, operation, value)); } else if (value.end) { rqlFilter.push(qEndText(field, operation, value)); } if (value.pattern) { rqlFilter.push(qPatternText(field, operation, value)); } } break; // List matching case RQL_EXPRESSIONS.IN: case RQL_EXPRESSIONS.OUT: rqlFilter.push(qList(field, operation, value)); break; // Range matching case RQL_EXPRESSIONS.RANGE: rqlFilter.push(qRange(field, operation, value)); break; // Logical OR case RQL_EXPRESSIONS.OR: rqlFilter.push(qOr(toSubQueries(value))); break; // Logical AND case RQL_EXPRESSIONS.AND: rqlFilter.push(qAnd(toSubQueries(value))); break; // Logical NOT case RQL_EXPRESSIONS.NOT: rqlFilter.push(...toWrappedQueries(field, operation, value)); break; // Relationals default: rqlFilter.push(qRel(field, operation, value)); } })(rqlExp); return qAnd(rqlFilter); } /** Transforms rql object to query string * * @type {Function} * @param {Object} rqlObject - rql object * @param {String} field - field (param name, entity prop) * @returns {String} * * @summary Object -> String * @example ({ name: { $like: 'Jo', $ne: 'Joe'} }) -> 'like(name, *Jo*)&ne(name, Joe)' */ export function rql(rqlObj) { const result = []; const toSubQueries = map(toWrappedQuery); forEachObjIndexed((value, key) => { if (isNilOrEmpty(value)) return; if (key === RQL_EXPRESSIONS.LIMIT) { result.push(qLimit(value)); } else if (isObjectStrict(value)) { result.push(rqlToQuery(value, key)); } else if (key === RQL_EXPRESSIONS.OR) { result.push(qOr(toSubQueries(value))); } else if (key === RQL_EXPRESSIONS.AND) { result.push(qAnd(toSubQueries(value))); } else if (isControlRqlExp(key)) { result.push(qWrap(key, value, $value)); } else { result.push(qEq(key, value)); } }, rqlObj); return qAnd(result); } export default rql;