UNPKG

massive

Version:

A small query tool for Postgres that embraces json and makes life simpler

187 lines (161 loc) 5.47 kB
'use strict'; /** @module operations */ const _ = require('lodash'); const castTimestamp = (value, type) => { if (_.isDate(value)) { switch (type) { case 'date': return '::date'; case 'timestamp without time zone': return '::timestamp'; default: return '::timestamptz'; } } return ''; }; /** * Build a BETWEEN (a, b) predicate. * * @param {Object} condition - A condition object from {@link module:where~getCondition}. * @return {Object} The modified condition. */ const buildBetween = condition => { condition.params = condition.value; condition.value = `$${condition.offset}${castTimestamp(condition.value[0], condition.type)} AND $${condition.offset + 1}${castTimestamp(condition.value[1], condition.type)}`; condition.offset += 2; return condition; }; /** * Build an IN (x, y, z) predicate. * * @param {Object} condition - A condition object from {@link module:where~getCondition}. * @return {Object} The modified condition. */ const buildIn = condition => { if (condition.value.length === 0) { condition.value = condition.appended.operator === '=' ? `ANY ('{}')` : `ALL ('{}')`; return condition; } condition.appended.operator = condition.appended.operator === '=' ? 'IN' : 'NOT IN'; condition.params = condition.params.concat(condition.value); const inList = condition.value.map((v, index) => [`$${condition.offset + index}${castTimestamp(v, condition.type)}`]); condition.offset += condition.value.length; condition.value = `(${inList.join(',')})`; return condition; }; /** * Interpolate values into a predicate with IS/IS NOT. * * @param {Object} condition - A condition object from {@link module:where~getCondition}. * @return {Object} The modified condition. */ const buildIs = function (condition) { if (condition.appended.operator === '=' || condition.appended.operator === 'IS') { condition.appended.operator = 'IS'; } else { condition.appended.operator = 'IS NOT'; } return condition; }; /** * Handle the overloads for equality tests: interpolating null and boolean * values and building IN lists. * * @param {Object} condition - A condition object from {@link module:where~getCondition}. * @return {Object} The modified condition. */ const equality = function (condition) { if (condition.value === null || _.isBoolean(condition.value)) { return buildIs(condition); } else if (_.isArray(condition.value)) { return buildIn(condition); } condition.params.push(condition.value); condition.value = `$${condition.offset}${castTimestamp(condition.value, condition.type)}`; return condition; }; /** * Transform an array into a safe comma-delimited string literal. * * @param {Object} condition - A condition object from {@link module:where~getCondition}. * @return {Object} The modified condition. */ const literalizeArray = condition => { if (_.isArray(condition.value)) { const sanitizedValues = condition.value.map(function (v) { if (_.isString(v) && (v === '' || v === 'null' || v.search(/[,{}\s\\"]/) !== -1)) { return `"${v.replace(/([\\"])/g, '\\$1')}"`; } else if (v === null) { return 'null'; } return v; }); condition.params.push(`{${sanitizedValues.join(',')}}`); } else { condition.params.push(condition.value); } condition.value = `$${condition.offset}${castTimestamp(condition.value, condition.type)}`; return condition; }; /** * Operation definitions for parsing criteria objects. * * Keys are search strings in criteria keys. Values define an output SQL * operator and an optional mutator which will be applied to the appropriate * parameter value for the prepared statement. * * @enum * @readonly */ const map = { // basic comparison '=': {operator: '=', mutator: equality}, '!': {operator: '<>', mutator: equality}, '>': {operator: '>'}, '<': {operator: '<'}, '>=': {operator: '>='}, '<=': {operator: '<='}, '!=': {operator: '<>', mutator: equality}, '<>': {operator: '<>', mutator: equality}, 'between': {operator: 'BETWEEN', mutator: buildBetween}, // array '@>': {operator: '@>', mutator: literalizeArray}, '<@': {operator: '<@', mutator: literalizeArray}, '&&': {operator: '&&', mutator: literalizeArray}, // json '?': {operator: '?'}, '?|': {operator: '?|', mutator: literalizeArray}, '?&': {operator: '?&', mutator: literalizeArray}, '@?': {operator: '@?'}, '@@': {operator: '@@'}, // pattern matching '~~': {operator: 'LIKE'}, 'like': {operator: 'LIKE'}, '!~~': {operator: 'NOT LIKE'}, 'not like': {operator: 'NOT LIKE'}, '~~*': {operator: 'ILIKE'}, 'ilike': {operator: 'ILIKE'}, '!~~*': {operator: 'NOT ILIKE'}, 'not ilike': {operator: 'NOT ILIKE'}, // regex 'similar to': {operator: 'SIMILAR TO'}, 'not similar to': {operator: 'NOT SIMILAR TO'}, '~': {operator: '~'}, '!~': {operator: '!~'}, '~*': {operator: '~*'}, '!~*': {operator: '!~*'}, // comparison predicates 'is': {operator: 'IS', mutator: buildIs}, 'is not': {operator: 'IS NOT', mutator: buildIs}, 'is distinct from': {operator: 'IS DISTINCT FROM'}, 'is not distinct from': {operator: 'IS NOT DISTINCT FROM'} }; exports = module.exports = key => { return _.clone(map[key]); }; exports.buildBetween = buildBetween; exports.buildIn = buildIn; exports.buildIs = buildIs; exports.equality = equality; exports.literalizeArray = literalizeArray;