UNPKG

slonik-utilities

Version:

Utilities for manipulating data in PostgreSQL database using Slonik.

205 lines (170 loc) 4.59 kB
import { difference, uniq, mapKeys, snakeCase, } from 'lodash'; import { sql, type CommonQueryMethods, type ValueExpression, } from 'slonik'; import { Logger, } from '../Logger'; type NamedValueBindingsType = { readonly [key: string]: ValueExpression, }; type UpsertConfigurationType = { readonly identifierName: string, }; const log = Logger.child({ namespace: 'upsert', }); const normalizeNamedValueBindingName = (name: string): string => { return snakeCase(name); }; const defaultConfiguration = { identifierName: 'id', }; export const upsert = async ( connection: CommonQueryMethods, tableName: string, namedValueBindings: NamedValueBindingsType, inputUniqueConstraintColumnNames: readonly string[] | null = null, inputConfiguration: UpsertConfigurationType | null = null, ) => { const configuration = { ...defaultConfiguration, ...inputConfiguration, }; const namedValueBindingNamesWithUndefinedValues: string[] = []; const boundValues: Array<boolean | number | string | null> = []; const normalizedNamedValueBindings = mapKeys( namedValueBindings, (value, key) => { if (value === undefined) { namedValueBindingNamesWithUndefinedValues.push(key); } boundValues.push(value); return normalizeNamedValueBindingName(key); }, ); if (namedValueBindingNamesWithUndefinedValues.length > 0) { log.warn( { namedValueBindingNamesWithUndefinedValues, }, 'named value bindings with undefined values', ); throw new Error('Named value binding values must be defined.'); } const columnNames = Object.keys(normalizedNamedValueBindings); const uniqueConstraintColumnNames = inputUniqueConstraintColumnNames ?? columnNames; if (difference(uniqueConstraintColumnNames, columnNames).length > 0) { throw new Error( 'Unique constraint column names must not contain column names not present in named value bindings.', ); } const updateColumnNames = difference( columnNames, uniqueConstraintColumnNames, ); if (columnNames.length === 0) { throw new Error('Named value bindings object must have properties.'); } const columnIdentifiers = sql.join( columnNames.map((columnName) => { return sql.identifier([ columnName, ]); }), sql.fragment`, `, ); const conflictColumnIdentifiers = sql.join( uniqueConstraintColumnNames.map((uniqueConstraintColumnName) => { return sql.identifier([ uniqueConstraintColumnName, ]); }), sql.fragment`, `, ); let updateClause; if (updateColumnNames.length) { updateClause = sql.join( updateColumnNames.map((updateColumnName) => { return sql.fragment`${sql.identifier([ updateColumnName, ])} = ${sql.identifier([ 'excluded', updateColumnName, ])}`; }), sql.fragment`, `, ); } const targetColumnNames = uniq([ ...uniqueConstraintColumnNames, ...updateColumnNames, ]); const whereClause = sql.join( targetColumnNames.map((targetColumnName) => { const value = normalizedNamedValueBindings[ normalizeNamedValueBindingName(targetColumnName) ]; if (value === null) { return sql.fragment`${sql.identifier([ targetColumnName, ])} IS NULL`; } return sql.fragment`${sql.identifier([ targetColumnName, ])} = ${value}`; }), sql.fragment` AND `, ); const selectQuery = sql.unsafe` SELECT ${sql.identifier([ configuration.identifierName, ])} FROM ${sql.identifier([ tableName, ])} WHERE ${whereClause} `; let maybeId; maybeId = await connection.maybeOneFirst(selectQuery); if (maybeId) { return maybeId; } if (updateClause) { return await connection.oneFirst(sql.unsafe` INSERT INTO ${sql.identifier([ tableName, ])} (${columnIdentifiers}) VALUES (${sql.join(boundValues, sql.fragment`, `)}) ON CONFLICT (${conflictColumnIdentifiers}) DO UPDATE SET ${updateClause} RETURNING ${sql.identifier([ configuration.identifierName, ])} `); } maybeId = await connection.maybeOneFirst(sql.unsafe` INSERT INTO ${sql.identifier([ tableName, ])} (${columnIdentifiers}) VALUES (${sql.join(boundValues, sql.fragment`, `)}) ON CONFLICT (${conflictColumnIdentifiers}) DO NOTHING `); if (maybeId) { return maybeId; } return await connection.oneFirst(selectQuery); };