2026-10-19 14:25:15 +00:00
|
|
|
const { R } = require("redbean-node");
|
|
|
|
|
|
2026-10-19 17:17:33 +00:00
|
|
|
const PG_BOOLEAN_COLUMNS = [
|
|
|
|
|
"active",
|
|
|
|
|
"pin",
|
|
|
|
|
"default",
|
|
|
|
|
"important",
|
|
|
|
|
"public",
|
|
|
|
|
"ignore_tls",
|
|
|
|
|
"upside_down",
|
|
|
|
|
"expiry_notification",
|
|
|
|
|
"domain_expiry_notification",
|
|
|
|
|
"save_response",
|
|
|
|
|
"save_error_response",
|
|
|
|
|
"published",
|
|
|
|
|
"twofa_status",
|
|
|
|
|
"is_default",
|
|
|
|
|
"invert_keyword",
|
|
|
|
|
"kafka_producer_ssl",
|
|
|
|
|
"kafka_producer_allow_auto_topic_creation",
|
|
|
|
|
"show_certificate_expiry",
|
|
|
|
|
"gamedig_given_port_only",
|
|
|
|
|
"ws_ignore_sec_websocket_accept_header",
|
|
|
|
|
"ping_numeric",
|
|
|
|
|
"show_only_last_heartbeat",
|
|
|
|
|
"retry_only_on_status_code_failure",
|
|
|
|
|
"cache_bust",
|
|
|
|
|
"email_verified",
|
|
|
|
|
"banned",
|
|
|
|
|
"two_factor_enabled",
|
|
|
|
|
"verified",
|
|
|
|
|
"enabled",
|
|
|
|
|
"rate_limit_enabled",
|
|
|
|
|
];
|
|
|
|
|
|
2026-10-19 14:25:15 +00:00
|
|
|
function patchRedBeanPostgres() {
|
|
|
|
|
const RedBeanNode = R.constructor;
|
|
|
|
|
if (RedBeanNode.prototype._pgPatched) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
RedBeanNode.prototype._pgPatched = true;
|
|
|
|
|
|
|
|
|
|
function cleanPgSql(sql) {
|
|
|
|
|
if (typeof sql === "string") {
|
2026-10-19 17:17:33 +00:00
|
|
|
// Replace MySQL backticks with standard quotes
|
|
|
|
|
sql = sql.replace(/`([^`]+)`/g, '"$1"');
|
|
|
|
|
// Replace bracket identifier escaping [col] with "col"
|
|
|
|
|
sql = sql.replace(/\[([^\]]+)\]/g, '"$1"');
|
|
|
|
|
|
|
|
|
|
// PostgreSQL is strictly typed for booleans: convert integer assignments and comparisons
|
|
|
|
|
const boolPattern = PG_BOOLEAN_COLUMNS.join("|");
|
|
|
|
|
sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*=\\s*1\\b`, "gi"), "$1 = true");
|
|
|
|
|
sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*=\\s*0\\b`, "gi"), "$1 = false");
|
|
|
|
|
sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*(?:!=|<>)\\s*1\\b`, "gi"), "$1 != true");
|
|
|
|
|
sql = sql.replace(new RegExp(`(["\`]?\\b(?:${boolPattern})\\b["\`]?)\\s*(?:!=|<>)\\s*0\\b`, "gi"), "$1 != false");
|
|
|
|
|
sql = sql.replace(new RegExp(`\\b1\\s*=\\s*(["\`]?\\b(?:${boolPattern})\\b["\`]?)`, "gi"), "true = $1");
|
|
|
|
|
sql = sql.replace(new RegExp(`\\b0\\s*=\\s*(["\`]?\\b(?:${boolPattern})\\b["\`]?)`, "gi"), "false = $1");
|
2026-10-19 14:25:15 +00:00
|
|
|
}
|
|
|
|
|
return sql;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 17:17:33 +00:00
|
|
|
function sanitizePgBeanBooleans(bean) {
|
|
|
|
|
if (!bean) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const col of PG_BOOLEAN_COLUMNS) {
|
|
|
|
|
if (bean[col] === 0 || bean[col] === "0") {
|
|
|
|
|
bean[col] = false;
|
|
|
|
|
} else if (bean[col] === 1 || bean[col] === "1") {
|
|
|
|
|
bean[col] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const origExec = RedBeanNode.prototype.exec;
|
|
|
|
|
if (origExec) {
|
|
|
|
|
RedBeanNode.prototype.exec = function (sql, data) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
sql = cleanPgSql(sql);
|
|
|
|
|
}
|
|
|
|
|
return origExec.call(this, sql, data);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 14:25:15 +00:00
|
|
|
const origNormalizeRawCore = RedBeanNode.prototype.normalizeRawCore;
|
|
|
|
|
RedBeanNode.prototype.normalizeRawCore = function (sql, data) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
sql = cleanPgSql(sql);
|
|
|
|
|
}
|
|
|
|
|
return origNormalizeRawCore.call(this, sql, data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const origNormalizeRaw = RedBeanNode.prototype.normalizeRaw;
|
|
|
|
|
RedBeanNode.prototype.normalizeRaw = async function (sql, data) {
|
|
|
|
|
let result = await this.normalizeRawCore(sql, data);
|
|
|
|
|
this.queryLog(sql);
|
|
|
|
|
if (this.dbType === "mysql") {
|
|
|
|
|
result = result[0];
|
|
|
|
|
} else if (this.dbType === "pg" && result && result.rows) {
|
|
|
|
|
result = result.rows;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RedBeanNode.prototype.getRow = async function (sql, data = [], autoLimit = false) {
|
|
|
|
|
if (autoLimit) {
|
|
|
|
|
if (this.dbType === "mssql") {
|
|
|
|
|
if (sql.trim().toLowerCase().startsWith("select ")) {
|
|
|
|
|
sql = sql.replace(/select/i, "$& TOP 1");
|
|
|
|
|
}
|
|
|
|
|
} else if (this.dbType === "pg") {
|
|
|
|
|
let limitTemplate = this.knex.limit(1).toSQL();
|
|
|
|
|
sql = cleanPgSql(sql) + limitTemplate.sql.replace("select *", "");
|
|
|
|
|
data = data.concat(limitTemplate.bindings);
|
|
|
|
|
} else {
|
|
|
|
|
let limitTemplate = this.knex.limit(1).toSQL().toNative();
|
|
|
|
|
sql = sql + limitTemplate.sql.replace("select *", "");
|
|
|
|
|
data = data.concat(limitTemplate.bindings);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.queryLog(sql);
|
|
|
|
|
let result = await this.normalizeRaw(sql, data);
|
|
|
|
|
if (result && result.length > 0) {
|
|
|
|
|
return result[0];
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-19 17:17:33 +00:00
|
|
|
const origGetAll = RedBeanNode.prototype.getAll;
|
|
|
|
|
if (origGetAll) {
|
|
|
|
|
RedBeanNode.prototype.getAll = function (sql, data) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
sql = cleanPgSql(sql);
|
|
|
|
|
}
|
|
|
|
|
return origGetAll.call(this, sql, data);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const origGetCell = RedBeanNode.prototype.getCell;
|
|
|
|
|
if (origGetCell) {
|
|
|
|
|
RedBeanNode.prototype.getCell = function (sql, data) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
sql = cleanPgSql(sql);
|
|
|
|
|
}
|
|
|
|
|
return origGetCell.call(this, sql, data);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const origGetCol = RedBeanNode.prototype.getCol;
|
|
|
|
|
if (origGetCol) {
|
|
|
|
|
RedBeanNode.prototype.getCol = function (sql, data) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
sql = cleanPgSql(sql);
|
|
|
|
|
}
|
|
|
|
|
return origGetCol.call(this, sql, data);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 14:25:15 +00:00
|
|
|
const origFindCore = RedBeanNode.prototype.findCore;
|
|
|
|
|
RedBeanNode.prototype.findCore = function (type, clause, data = []) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
clause = cleanPgSql(clause);
|
|
|
|
|
}
|
|
|
|
|
return origFindCore.call(this, type, clause, data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RedBeanNode.prototype.findOne = async function (type, clause = "", data = []) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
clause = cleanPgSql(clause);
|
|
|
|
|
}
|
|
|
|
|
let queryPromise = this.knex.table(type).whereRaw(clause, data).first();
|
|
|
|
|
this.queryLog(queryPromise);
|
|
|
|
|
let obj;
|
|
|
|
|
try {
|
|
|
|
|
obj = await queryPromise;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.checkAllowedError(error);
|
|
|
|
|
}
|
|
|
|
|
if (!obj) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return this.convertToBean(type, obj);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const origCount = RedBeanNode.prototype.count;
|
|
|
|
|
RedBeanNode.prototype.count = async function (type, clause = "", data = [], autoLimit = true) {
|
|
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
clause = cleanPgSql(clause);
|
|
|
|
|
}
|
|
|
|
|
return origCount.call(this, type, clause, data, autoLimit);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const origStoreCore = RedBeanNode.prototype.storeCore;
|
|
|
|
|
RedBeanNode.prototype.storeCore = async function (bean, changedFieldsOnly = true) {
|
2026-10-19 17:17:33 +00:00
|
|
|
if (this.dbType === "pg") {
|
|
|
|
|
sanitizePgBeanBooleans(bean);
|
|
|
|
|
}
|
2026-10-19 14:25:15 +00:00
|
|
|
if (this.dbType === "pg" && !bean.id) {
|
|
|
|
|
this.devLog("Store PG insert", bean.beanMeta.type);
|
|
|
|
|
await bean.storeTypeBeanList();
|
|
|
|
|
if (typeof bean.onUpdate === "function") {
|
|
|
|
|
bean.onUpdate();
|
|
|
|
|
}
|
|
|
|
|
let obj = bean.export(false);
|
|
|
|
|
delete obj.id;
|
2026-10-19 17:17:33 +00:00
|
|
|
for (const col of PG_BOOLEAN_COLUMNS) {
|
|
|
|
|
if (obj[col] === 0 || obj[col] === "0") {
|
|
|
|
|
obj[col] = false;
|
|
|
|
|
} else if (obj[col] === 1 || obj[col] === "1") {
|
|
|
|
|
obj[col] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-19 14:25:15 +00:00
|
|
|
let queryPromise = this.knex(bean.getType()).insert(obj).returning("id");
|
|
|
|
|
this.queryLog(queryPromise);
|
|
|
|
|
let result = await queryPromise;
|
|
|
|
|
bean.id = result && result[0] ? (typeof result[0] === "object" ? result[0].id : result[0]) : null;
|
|
|
|
|
await bean.storeSharedList();
|
|
|
|
|
await bean.storeOwnList();
|
|
|
|
|
bean.beanMeta.old = {};
|
|
|
|
|
if (typeof bean.onAfterUpdate === "function") {
|
|
|
|
|
bean.onAfterUpdate();
|
|
|
|
|
}
|
|
|
|
|
return bean.id;
|
|
|
|
|
}
|
|
|
|
|
return origStoreCore.call(this, bean, changedFieldsOnly);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
patchRedBeanPostgres();
|
|
|
|
|
|
|
|
|
|
module.exports = { patchRedBeanPostgres };
|