UNPKG

pg-sql-helpers

Version:

A set helpers for writing dynamic SQL queries with `pg-sql` in Javascript.

519 lines (440 loc) 10.3 kB
import is from 'is' import isPlainObject from 'is-plain-object' import { sql } from 'pg-sql' /** * A map of operators shorthands to PostgreSQL operators. * * @type {Object} */ const WHERE_OPERATORS = { eq: '=', gt: '>', gte: '>=', lt: '<', lte: '<=', ne: '!=', neq: '!=', like: 'LIKE', ilike: 'ILIKE', } /** * Create a SQL "AND" clause, just like "WHERE". * * @param {String} ident * @param {Object} params * @param {Object} options * @return {sql} */ function AND(ident, params, options = {}) { const query = WHERE(ident, params, { ...options, keyword: 'AND' }) return query } /** * Create a SQL column expression, with optional table reference. * * @param {String} table (optional) * @param {String} column * @return {sql} */ function COLUMN(table, column) { if (column == null) { column = table table = null } const ref = table ? sql`${sql.ident(table, column)}` : sql`${sql.ident(column)}` return ref } /** * Create a list of SQL identifiers from a `value`. * * @param {String} table (optional) * @param {Object|Array<Object>|Array<String>} value * @return {sql} */ function COLUMNS(table, value, options = {}) { if (table != null && !is.string(table)) { value = table table = null } const { delimiter = ', ' } = options let keys if (is.object(value)) { keys = getDefinedKeys(value) } else if (is.array(value) && is.object(value[0])) { keys = getDefinedKeys(value[0]) } else if (is.array(value) && is.string(value[0])) { keys = value } else { throw new Error( `The \`COLUMNS\` SQL helper must be passed an object, an array of objects or an array of strings, but you passed: ${value}` ) } const idents = keys.map((k) => COLUMN(table, k)) const query = sql`${sql.join(idents, delimiter)}` return query } /** * Create a SQL composite value for the values of an `object`. * * @param {Object} object * @return {sql} */ function COMPOSITE(object) { if (!is.object(object)) { throw new Error( `The \`COMPOSITE\` SQL helper must be passed an object, but you passed: ${object}` ) } const keys = getDefinedKeys(object) const vals = keys.map((k) => sql`${object[k]}`) const query = sql`(${sql.join(vals, ', ')})` return query } /** * Create a list SQL composite values for the values of an `array` of objects. * * @param {Array} array * @return {sql} */ function COMPOSITES(array) { if (!Array.isArray(array)) { array = [array] } let columns const composites = array.map((object, i) => { const composite = COMPOSITE(object) const keys = getDefinedKeys(object) const cols = keys.join(',') if (i === 0) { columns = cols } else if (cols !== columns) { throw new Error( `Every entry in a SQL composite expression must have the same columns, but you passed: ${array}` ) } return composite }) const query = sql.join(composites, ', ') return query } /** * Create a SQL "INSERT" statement from a dictionary or list of `values`. * * @param {String} table * @param {Object|Array<Object>} values * @return {sql} */ function INSERT(table, values) { const query = sql`INSERT INTO ${sql.ident(table)} (${COLUMNS( values )}) ${VALUES(values)}` return query } /** * Create a literal SQL "LIMIT" string from `number`. * * @param {Number} number * @param {Object} options * @return {sql} */ function LIMIT(number, options = {}) { if (number == null) { return sql`` } if (options.max) { number = Math.min(number, options.max) } const query = number === Infinity ? sql`LIMIT ALL` : sql`LIMIT ${number}` return query } /** * Create a literal SQL "OFFSET" string from `number`. * * @param {Number} number * @param {Object} options * @return {sql} */ function OFFSET(number, options = {}) { if (number == null) { return sql`` } if (options.max) { number = Math.min(number, options.max) } const query = sql`OFFSET ${number}` return query } /** * Create a SQL "OR" clause, just like "WHERE". * * @param {String} ident * @param {Object} params * @param {Object} options * @return {sql} */ function OR(ident, params, options = {}) { const query = WHERE(ident, params, { ...options, keyword: 'OR' }) return query } /** * Create a SQL "ORDER BY" string from `sorts`. * * @param {String} table (optional) * @param {Array} sorts * @return {sql} */ function ORDER_BY(table, sorts) { if (Array.isArray(table)) { sorts = table table = null } if (!Array.isArray(sorts)) { throw new Error( `The \`ORDER_BY\` SQL helper must be passed an array of sorting parameters, but you passed: ${sorts}` ) } if (!sorts.length) { return sql`` } const values = sorts.map((sort) => SORT(table, sort)) const query = sql`ORDER BY ${sql.join(values, ', ')}` return query } /** * Create a SQL `ROW` expression for the values of an `object`. * * @param {Object} object * @return {sql} */ function ROW(object) { const query = sql`ROW ${COMPOSITE(object)}` return query } /** * Create a SQL "SELECT" clause for `table` with `values`. * * @param {String} table * @param {Object|Array<String>|Array<Object>} values * @return {sql} */ function SELECT(table, values) { if (table != null && !is.string(table)) { values = table table = null } const query = sql`SELECT ${COLUMNS(table, values)}` return query } /** * Create a SQL sort expression. * * @param {String} table (optional) * @param {String} column * @return {sql} */ function SORT(table, column) { if (arguments.length === 1) { column = table table = null } let order = 'ASC' if (column.startsWith('-')) { order = 'DESC' column = column.slice(1) } return sql`${COLUMN(table, column)} ${sql.raw(order)} NULLS LAST` } /** * Create a SQL "UPDATE" clause for `table` with `values`. * * @param {String} table * @param {Object} values * @return {sql} */ function UPDATE(table, values) { if (typeof table != 'string') { values = table table = null } if (!is.object(values)) { throw new Error( `The \`UPDATE\` SQL helper must be passed an object, but you passed: ${values}` ) } const keys = getDefinedKeys(values) const id = table ? sql`${sql.ident(table)}` : sql`` const query = keys.length == 1 ? sql`UPDATE ${id} SET ${COLUMN(keys[0])} = ${values[keys[0]]}` : sql`UPDATE ${id} SET (${COLUMNS(values)}) = ${ROW(values)}` return query } /** * Create a SQL "INSERT ON CONFLICT UPDATE" statement from a dictionary or list of `values`. * * @param {String} table * @param {String|Array<String>} constraint * @param {Object|Array<Object>} values * @param {Object} options * @return {sql} */ function UPSERT(table, constraint, values, options = {}) { if (typeof constraint === 'string') { constraint = [constraint] } const keys = is.object(values) ? getDefinedKeys(values) : getDefinedKeys(values[0]) const others = keys.filter((k) => !constraint.includes(k)) const insert = INSERT(table, values) const conflict = sql`ON CONFLICT (${COLUMNS(constraint)}) DO` const update = others.length > 0 ? sql`UPDATE SET (${COLUMNS(others)}) = (${COLUMNS('excluded', others)})` : sql`NOTHING` const distinct = others.length > 0 && options.distinct !== false ? sql`WHERE (${COLUMNS(table, others)}) IS DISTINCT FROM (${COLUMNS( 'excluded', others )})` : sql`` const query = sql`${insert} ${conflict} ${update} ${distinct}` return query } /** * Create a list of placeholders for the values of an `object`. * * @param {Object} object * @return {sql} */ function VALUES(object) { if (!Array.isArray(object)) { object = [object] } const query = sql`VALUES ${COMPOSITES(object)}` return query } /** * Create a SQL "where" clause with `params` and optional `ident`. * * @param {String} ident * @param {Object} params * @return {sql} */ function WHERE(ident, params, options = {}) { if (is.object(ident)) { params = ident ident = '' } if (params == null) { return sql`` } function handle(keys, obj) { const key = keys[keys.length - 1] let value = obj[key] let operator = WHERE_OPERATORS[key] || '=' if (isPlainObject(value)) { const ks = getDefinedKeys(value) if (ks.length === 0) return ks.forEach((k) => handle([...keys, k], value)) return } if (value === null) { value = sql.raw('NULL') if (operator === '=') operator = 'IS' if (operator === '!=') operator = 'IS NOT' } const ref = key in WHERE_OPERATORS ? keys.slice(0, -1).join('->') : keys.join('->') const id = ident ? sql`${sql.ident(ident)}.${sql.ident(ref)}` : sql`${sql.ident(ref)}` const clause = sql`${id} ${sql.raw(operator)} ${value}` clauses.push(clause) } const { keyword = 'WHERE', delimiter = 'AND' } = options const clauses = [] getDefinedKeys(params).forEach((key) => { handle([key], params) }) if (clauses.length === 0) { return sql`` } const query = sql`${sql.raw(keyword)} ${sql.join(clauses, ` ${delimiter} `)}` return query } /** * Get the keys for an `object` that don't have undefined values. * * @param {Object} object * @return {Array} */ function getDefinedKeys(object) { return Object.keys(object) .filter((k) => object[k] !== undefined) .sort() } /** * Add lowercase aliases for convenience. */ const and = AND const column = COLUMN const columns = COLUMNS const composite = COMPOSITE const composites = COMPOSITES const insert = INSERT const limit = LIMIT const offset = OFFSET const or = OR const orderBy = ORDER_BY const row = ROW const select = SELECT const sort = SORT const update = UPDATE const upsert = UPSERT const values = VALUES const where = WHERE /** * Export. * * @type {Function} */ export { and, AND, column, COLUMN, columns, COLUMNS, composite, COMPOSITE, composites, COMPOSITES, insert, INSERT, limit, LIMIT, offset, OFFSET, or, OR, orderBy, ORDER_BY, row, ROW, select, SELECT, sort, SORT, update, UPDATE, upsert, UPSERT, values, VALUES, where, WHERE, }