Files
bun-mariadb/dist/lib/mariadb/db-update.js
T

134 lines
4.7 KiB
JavaScript

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";
import { assertNoProtectedQueueFields, isQueueTable, QUEUE_NOT_CLAIMED_SQL, } from "../queue/queue-utils";
/** 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,
});
let whereClause = sqlQueryObj.where;
if (!whereClause) {
return {
success: false,
msg: `No WHERE clause`,
};
}
if (isQueueTable(table)) {
assertNoProtectedQueueFields(table, [data]);
whereClause += ` AND ${QUEUE_NOT_CLAIMED_SQL}`;
}
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,
},
};
}
}