|
| 1 | +const { Pool } = require('pg') |
| 2 | +const UNIQUE_INDEX_VIOLATION_ERR_CODE = '23505' |
| 3 | +const pool = new Pool({ connectionString: process.env.DATABASE_URL }) |
| 4 | +pool.on('acquire', () => {}) |
| 5 | +pool.on('remove', () => {}) |
| 6 | +pool.on('connect', () => {}) |
| 7 | +pool.on('error', (e) => {}) |
| 8 | + |
| 9 | +const shutdownPool = async () => { |
| 10 | + await pool.end() |
| 11 | +} |
| 12 | + |
| 13 | +const findOne = async (tableName, condition) => { |
| 14 | + let rows = await find(tableName, condition) |
| 15 | + if (rows && rows.length > 0) { |
| 16 | + return rows[0] |
| 17 | + } |
| 18 | + return null |
| 19 | +} |
| 20 | + |
| 21 | +const find = async (tableName, condition) => { |
| 22 | + let parts = [`SELECT * FROM ${tableName}`] |
| 23 | + let conditionQuery = buildConditionSql(condition) |
| 24 | + parts.push(conditionQuery) |
| 25 | + let query = parts.join(' ') |
| 26 | + let dollarValues = Object.keys(condition).map((key) => condition[key]) |
| 27 | + let result = await pool.query(query, dollarValues) |
| 28 | + return result.rows |
| 29 | +} |
| 30 | + |
| 31 | +const insert = async (tr, tableName, columnValues, returning = ['id']) => { |
| 32 | + let parts = [`INSERT INTO ${tableName}`] |
| 33 | + let insertColumns = Object.keys(columnValues) |
| 34 | + parts.push(`(${insertColumns.join(', ')})`) |
| 35 | + parts.push('VALUES') |
| 36 | + let dollars = Object.keys(columnValues).map((_, i) => `$${i + 1}`) |
| 37 | + parts.push(`(${dollars.join(', ')})`) |
| 38 | + parts.push(`RETURNING ${returning.join(', ')}`) |
| 39 | + let dollarValues = Object.keys(columnValues).map((key) => columnValues[key]) |
| 40 | + let query = parts.join(' ') |
| 41 | + try { |
| 42 | + let result = await (tr || pool).query(query, dollarValues) |
| 43 | + if (result.rows && result.rows.length > 0) { |
| 44 | + return result.rows[0] |
| 45 | + } |
| 46 | + } catch (e) { |
| 47 | + if (e.code && e.code === UNIQUE_INDEX_VIOLATION_ERR_CODE) { |
| 48 | + throwUniqueIndexViolationError(e) |
| 49 | + } |
| 50 | + throw e |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +const update = async (tr, tableName, columnValues, condition) => { |
| 55 | + let parts = [`UPDATE ${tableName} SET`] |
| 56 | + let updateParts = [] |
| 57 | + Object.keys(columnValues).forEach((columnName, i) => { |
| 58 | + updateParts.push(`${columnName} = $${i + 1}`) |
| 59 | + }) |
| 60 | + parts.push(updateParts.join(', ')) |
| 61 | + parts.push(buildConditionSql(condition, updateParts.length)) |
| 62 | + let query = parts.join(' ') |
| 63 | + let dollarValues = Object.keys(columnValues).map((key) => columnValues[key]) |
| 64 | + if (condition !== {}) { |
| 65 | + dollarValues = dollarValues.concat(Object.keys(condition).map((key) => condition[key])) |
| 66 | + } |
| 67 | + try { |
| 68 | + await (tr || pool).query(query, dollarValues) |
| 69 | + } catch (e) { |
| 70 | + if (e.code && e.code === UNIQUE_INDEX_VIOLATION_ERR_CODE) { |
| 71 | + throwUniqueIndexViolationError(e) |
| 72 | + } |
| 73 | + throw e |
| 74 | + } |
| 75 | + return true |
| 76 | +} |
| 77 | + |
| 78 | +const del = async (tr, tableName, condition) => { |
| 79 | + let parts = [`DELETE FROM ${tableName}`] |
| 80 | + parts.push(buildConditionSql(condition)) |
| 81 | + let query = parts.join(' ') |
| 82 | + let dollarValues = Object.keys(condition).map((key) => condition[key]) |
| 83 | + let result = await (tr || pool).query(query, dollarValues) |
| 84 | + return result.rows |
| 85 | +} |
| 86 | + |
| 87 | +const query = async (tr, sql, dollarValues) => { |
| 88 | + let result = await (tr || pool).query(sql, dollarValues) |
| 89 | + return result |
| 90 | +} |
| 91 | + |
| 92 | +const buildConditionSql = (condition, startDollarNumbering = 0) => { |
| 93 | + if (!condition || Object.keys(condition).length === 0) { |
| 94 | + return null |
| 95 | + } |
| 96 | + let parts = [] |
| 97 | + let conditionParts = [] |
| 98 | + Object.keys(condition).forEach((key, i) => { |
| 99 | + let value = condition[key] |
| 100 | + // https://bit.ly/2yNyzoe |
| 101 | + if (Array.isArray(value)) { |
| 102 | + conditionParts.push(`${key} = ANY($${startDollarNumbering + i + 1})`) |
| 103 | + } else { |
| 104 | + conditionParts.push(`${key} = $${startDollarNumbering + i + 1}`) |
| 105 | + } |
| 106 | + }) |
| 107 | + if (conditionParts.length > 0) { |
| 108 | + parts.push('WHERE') |
| 109 | + parts.push(conditionParts.join(' AND ')) |
| 110 | + } |
| 111 | + return parts.join(' ') |
| 112 | +} |
| 113 | + |
| 114 | +const withTransaction = async (cb) => { |
| 115 | + const client = await pool.connect() |
| 116 | + try { |
| 117 | + await client.query('BEGIN') |
| 118 | + await cb(client) |
| 119 | + await client.query('COMMIT') |
| 120 | + } catch (e) { |
| 121 | + await client.query('ROLLBACK') |
| 122 | + throw e |
| 123 | + } finally { |
| 124 | + client.release() |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +const mapToColumns = (attrs, map) => { |
| 129 | + if (!attrs) { |
| 130 | + return {} |
| 131 | + } |
| 132 | + return Object.keys(attrs).reduce((obj, key) => { |
| 133 | + const column = map[key] |
| 134 | + if (!column) { |
| 135 | + throw new Error(`Unknown attribute: ${key}`) |
| 136 | + } |
| 137 | + obj[column] = attrs[key] |
| 138 | + return obj |
| 139 | + }, {}) |
| 140 | +} |
| 141 | + |
| 142 | +const mapFromColumns = (row, map) => { |
| 143 | + if (!row) { |
| 144 | + return null |
| 145 | + } |
| 146 | + return Object.keys(map).reduce((obj, key) => { |
| 147 | + const column = map[key] |
| 148 | + obj[key] = row[column] |
| 149 | + return obj |
| 150 | + }, {}) |
| 151 | +} |
| 152 | + |
| 153 | +class UniqueIndexError extends Error { |
| 154 | + constructor({ table, constraint, detail }) { |
| 155 | + super(detail) |
| 156 | + this.table = table |
| 157 | + this.constraint = constraint |
| 158 | + this.columns = this.parseColumns(detail) |
| 159 | + } |
| 160 | + |
| 161 | + parseColumns(detail) { |
| 162 | + // error message usually looks like this (in case of a single-column) index violation |
| 163 | + // Key (email)=([email protected]) already exists. |
| 164 | + // or like this (in case of a multi-column) index violation |
| 165 | + // Key (customer_id, display_name)=(cus_JOgWoKqN6pizGN, sub1) already exists. |
| 166 | + let parts = detail.split('=') |
| 167 | + parts = parts[0].split(/[\(\)]/) |
| 168 | + let rawColumns = parts[1] |
| 169 | + return rawColumns.split(', ') |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +function throwUniqueIndexViolationError(e) { |
| 174 | + throw new UniqueIndexError(e) |
| 175 | +} |
| 176 | + |
| 177 | +module.exports = { |
| 178 | + shutdownPool, |
| 179 | + find, |
| 180 | + findOne, |
| 181 | + insert, |
| 182 | + update, |
| 183 | + del, |
| 184 | + mapToColumns, |
| 185 | + mapFromColumns, |
| 186 | + withTransaction, |
| 187 | + query, |
| 188 | + UniqueIndexError, |
| 189 | +} |
0 commit comments