node-red-contrib-postgres-variable
Version:
PostgreSQL module for Node-RED with dynamic configuration from contexts (flow, global, environment)
293 lines (259 loc) • 11.9 kB
JavaScript
const { Pool } = require('pg');
const named = require('node-postgres-named');
const fs = require('fs');
module.exports = function (RED) {
/**
* Helper function to get value from different contexts
* @param {Object} node - Node instance
* @param {string} value - Value to get
* @param {string} type - Type of value (str, flow, global, env)
* @param {Object} msg - Message object
* @returns {string} Retrieved value
*/
function getValueFromContext(node, value, type, msg) {
if (value === null || value === undefined) return null;
try {
let result;
switch (type) {
case 'flow':
// For config nodes, we need to get flow context differently
const flowContext = node.context().flow;
if (!flowContext) {
return null;
}
result = getNestedValue(flowContext, value);
break;
case 'global':
const globalContext = node.context().global;
if (!globalContext) {
return null;
}
result = getNestedValue(globalContext, value);
break;
case 'env':
result = process.env[value];
break;
case 'msg':
result = RED.util.getMessageProperty(msg, value);
break;
default:
result = value;
}
return result !== undefined ? result : null;
} catch (err) {
throw new Error(`Failed to get value for type: ${type}, value: ${value}. Error: ${err.message}`);
}
}
// Helper function to get nested values like "all_vars.host"
function getNestedValue(context, path) {
if (!context) return undefined;
if (path.includes('.')) {
const parts = path.split('.');
let result = context.get(parts[0]);
for (let i = 1; i < parts.length; i++) {
if (result && typeof result === 'object') {
result = result[parts[i]];
} else {
return undefined;
}
}
return result;
} else {
return context.get(path);
}
}
// Definition of a Postgres database configuration node
function PostgresDatabaseNode(n) {
RED.nodes.createNode(this, n);
// Store the configuration values and types
this.hostname = n.hostname;
this.hostnameType = n.hostnameType || 'str';
this.port = n.port;
this.portType = n.portType || 'str';
this.db = n.db;
this.dbType = n.dbType || 'str';
this.ssl = n.ssl;
this.sslType = n.sslType || 'bool';
this.ignore_check_ssl = n.ignore_check_ssl;
this.ignore_check_sslType = n.ignore_check_sslType || 'bool';
this.ssl_path = n.ssl_path;
this.ssl_pathType = n.ssl_pathType || 'str';
this.user = n.user;
this.userType = n.userType || 'str';
this.password = n.password;
this.passwordType = n.passwordType || 'str';
this.passwordContext = n.passwordContext || '';
// Get credentials for string passwords
const credentials = this.credentials || {};
// Method to get configuration values with context resolution
this.getConfig = function(msg, executingNode) {
const config = {};
// Parse values using separate type fields
config.hostname = getValueFromContext(executingNode || this, this.hostname, this.hostnameType, msg) || 'localhost';
config.port = parseInt(getValueFromContext(executingNode || this, this.port, this.portType, msg)) || 5432;
config.db = getValueFromContext(executingNode || this, this.db, this.dbType, msg) || 'postgres';
config.ssl = this.parseBooleanValue(this.ssl, this.sslType, msg, executingNode);
config.ignore_check_ssl = this.parseBooleanValue(this.ignore_check_ssl, this.ignore_check_sslType, msg, executingNode);
config.ssl_path = getValueFromContext(executingNode || this, this.ssl_path, this.ssl_pathType, msg);
config.user = getValueFromContext(executingNode || this, this.user, this.userType, msg) || '';
config.password = this.parsePasswordValue(msg, executingNode) || '';
return config;
};
// Helper method to parse boolean values
this.parseBooleanValue = function(inputValue, type, msg, executingNode) {
if (!inputValue && inputValue !== false) return false;
// If it's already a boolean
if (typeof inputValue === 'boolean') {
return inputValue;
}
// If it's a string representation of boolean
if (typeof inputValue === 'string') {
if (type === 'str') {
return inputValue === 'true';
} else {
// Context type - resolve from context
const value = getValueFromContext(executingNode || this, inputValue, type, msg);
if (typeof value === 'boolean') return value;
if (typeof value === 'string') return value === 'true';
return !!value;
}
}
// Fallback
return !!inputValue;
};
// Helper method to parse password values with hybrid approach
this.parsePasswordValue = function(msg, executingNode) {
let password = '';
if (this.passwordType === 'str' || !this.passwordType) {
// String password - get from credentials
const credentials = this.credentials || {};
password = credentials.password || '';
} else {
// Context password - resolve from context
password = getValueFromContext(executingNode || this, this.passwordContext, this.passwordType, msg);
}
// Ensure password is always a string
if (password === null || password === undefined) {
password = '';
}
return String(password);
};
}
RED.nodes.registerType('postgresdb', PostgresDatabaseNode, {
credentials: {
password: { type: 'password' }
}
});
// Helper function to execute the database query
async function executeQuery(pool, msg, node) {
try {
const client = await pool.connect();
named.patch(client); // allows named query parameters with node-postgres-named
const queryParams = msg.queryParameters || {};
const result = await client.query(msg.payload, queryParams);
msg.payload = result.rows; // output the result of the query
node.send(msg);
client.release();
} catch (err) {
handleError(err, msg, node);
}
}
// Helper function to handle errors and send error messages
function handleError(err, msg, node) {
node.error(`PostgreSQL error: ${err.message}`, msg);
msg.payload = { error: err.message };
node.send(msg);
}
// Node responsible for executing SQL queries
function PostgresNode(n) {
RED.nodes.createNode(this, n);
const node = this;
const configNode = RED.nodes.getNode(n.postgresdb);
if (!configNode) {
node.error('Postgres database config node is missing');
return;
}
node.on('input', async (msg) => {
try {
// Get configuration with context resolution
const config = configNode.getConfig(msg, node);
// Validate password
if (config.password === null || config.password === undefined) {
node.error('Password is null or undefined', msg);
return;
}
// Configure SSL
let ssl_conf = false;
if (config.ssl === true) {
if (config.ignore_check_ssl === true) {
// SSL enabled but ignore certificate validation
ssl_conf = { rejectUnauthorized: false };
} else {
// SSL enabled with certificate validation
ssl_conf = { rejectUnauthorized: true };
// Add certificate if path is provided
if (config.ssl_path) {
try {
ssl_conf.ca = fs.readFileSync(config.ssl_path).toString();
} catch (certErr) {
node.error(`Failed to read SSL certificate from path: ${config.ssl_path}. Error: ${certErr.message}`, msg);
return;
}
}
}
}
const defaultConfig = {
user: config.user,
password: config.password,
host: config.hostname,
port: config.port,
database: config.db,
ssl: ssl_conf,
idleTimeoutMillis: 500,
connectionTimeoutMillis: 3000
};
const allConnectings = RED.settings.get('pgConnects') || {};
const connectName = msg.connectName;
let pool;
if (connectName && allConnectings[connectName]) {
const customConfig = allConnectings[connectName];
let customSslConf = false;
if (customConfig.ssl === true) {
if (customConfig.ignore_check_ssl === true) {
customSslConf = { rejectUnauthorized: false };
} else {
customSslConf = { rejectUnauthorized: true };
if (customConfig.ssl_path) {
try {
customSslConf.ca = fs.readFileSync(customConfig.ssl_path).toString();
} catch (certErr) {
node.error(`Failed to read SSL certificate from custom config path: ${customConfig.ssl_path}. Error: ${certErr.message}`, msg);
return;
}
}
}
}
pool = new Pool({
user: customConfig.user,
password: customConfig.password,
host: customConfig.host,
port: customConfig.port,
database: customConfig.database,
ssl: customSslConf,
idleTimeoutMillis: 500,
connectionTimeoutMillis: 3000
});
} else {
pool = new Pool(defaultConfig);
}
await executeQuery(pool, msg, node);
} catch (err) {
handleError(err, msg, node);
}
});
node.on('close', () => {
if (pool) pool.end();
});
}
RED.nodes.registerType('postgres', PostgresNode);
};