const { R } = require("redbean-node"); 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", ]; function patchRedBeanPostgres() { const RedBeanNode = R.constructor; if (RedBeanNode.prototype._pgPatched) { return; } RedBeanNode.prototype._pgPatched = true; function cleanPgSql(sql) { if (typeof sql === "string") { // 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"); } return sql; } function sanitizePgBeanBooleans(bean) { if (!bean || typeof bean !== "object") { return; } for (const col of PG_BOOLEAN_COLUMNS) { try { if (col in bean) { if (bean[col] === 0 || bean[col] === "0") { bean[col] = false; } else if (bean[col] === 1 || bean[col] === "1") { bean[col] = true; } } } catch (e) { // ignore } } } 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); }; } 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; } }; 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) { if (this.dbType === "pg") { sanitizePgBeanBooleans(bean); } 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; 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; } } 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 };