import dbHandler, { holdEventLoop, resolveClient } from "../db-handler"; import sqlGenerator from "../../utils/sql-generator"; import sanitizeHtmlFields from "../../utils/sanitize-html-fields"; import { quoteIdentifier } from "../../utils/sql-identifier"; import sqlValuePlaceholder from "../../utils/sql-value-placeholder"; import grabTableFields from "../../utils/grab-table-fields"; /** Thrown inside the transaction to roll back and return a response */ class UpdateAbort extends Error { response; constructor(response) { super(response.msg || response.error || "Update aborted"); this.response = response; } } export default async function DbUpdate({ table, data, query, targetId, config, }) { let sqlObj = { string: "", values: [], where: "", whereValues: [], }; try { const finalQuery = query || {}; const sqlQueryObj = sqlGenerator({ tableName: table, genObject: finalQuery, targetId, }); const whereClause = sqlQueryObj.where; if (!whereClause) { return { success: false, msg: `No WHERE clause`, }; } const sanitizedData = sanitizeHtmlFields({ table, data, config, }); const finalData = { updated_at: Date.now(), ...sanitizedData, }; const fieldsMap = new Map(grabTableFields(table).map((f) => [f.fieldName, f])); const setParts = []; const setValues = []; for (const key of Object.keys(finalData)) { const { placeholder, value } = sqlValuePlaceholder({ value: finalData[key], field: fieldsMap.get(key), }); setParts.push(`${quoteIdentifier(key)}=${placeholder}`); setValues.push(value); } const CLIENT = resolveClient(config); if (!CLIENT) { throw new Error(`Couldn't grab MariaDB Client.`); } /** * Lock the matching rows, update them by id, and re-fetch them by id, * all in one transaction. Re-fetching by id still works when the update * changes a column used in the WHERE clause. */ return await holdEventLoop(() => CLIENT.begin(async (tx) => { const matched_res = await dbHandler({ query: `SELECT id FROM ${quoteIdentifier(table)} ${whereClause} FOR UPDATE`, values: sqlQueryObj.whereValues, client: tx, }); if (!matched_res.success) { throw new UpdateAbort({ ...matched_res, success: false, debug: { sqlObj: sqlQueryObj }, }); } const matched_ids = (matched_res.payload || []) .map((row) => row.id) .filter((id) => id !== undefined && id !== null); if (matched_ids.length === 0) { throw new UpdateAbort({ success: false, msg: "No rows matched the update query", payload: [], debug: { sqlObj: sqlQueryObj }, }); } const idPlaceholders = matched_ids.map(() => "?").join(","); const sql = `UPDATE ${quoteIdentifier(table)} SET ${setParts.join(", ")} WHERE id IN (${idPlaceholders})`; const values = [...setValues, ...matched_ids]; sqlObj.string = sql; sqlObj.values = values; const res = await dbHandler({ query: sql, values: values, client: tx, }); if (!res.success) { throw new UpdateAbort({ ...res, debug: { sqlObj } }); } const updated_res = await dbHandler({ query: `SELECT * FROM ${quoteIdentifier(table)} WHERE id IN (${idPlaceholders})`, values: matched_ids, client: tx, }); return { ...updated_res, debug: { sqlObj, }, db_res: res.db_res, }; })); } catch (error) { if (error instanceof UpdateAbort) { return error.response; } return { success: false, error: error.message, debug: { sqlObj, }, }; } }