easy-postgresql-accessor
Version:
A powerful PostgreSQL data access layer with automatic schema discovery, CRUD operations, and SQL injection protection
280 lines (223 loc) • 8.7 kB
JavaScript
// PostgreSQL client is managed by PGClientFactory
const ObjectUtility = require('../utils/ObjectUtility.js');
const PGClientFactory = require('../utils/PGClientFactory.js');
class PostgreSQLAccessor {
constructor() {
this.objectUtility = new ObjectUtility();
this.columnsMap = new Map();
this.uniqueColumnsMap = new Map();
this.primaryKeyColumnsMap = new Map();
}
async initialize() {
this.client = await PGClientFactory.getPGClient();
}
async addTable(tableName) {
if (!this.client) {
await this.initialize();
}
await this.addTableColumns(tableName);
await this.addUniqueTableColumns(tableName);
await this.addPrimaryKeyColumns(tableName);
}
async addTableColumns(tableName) {
try {
const res = await this.client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = $1`, [tableName]);
const tableColumns = res.rows.map(row => row.column_name);
this.columnsMap.set(tableName, tableColumns);
} catch (e) {
console.error('Error retrieving column names:', e);
throw e;
}
}
async addUniqueTableColumns(tableName) {
try {
const res = await this.client.query(`
SELECT c.column_name
FROM information_schema.columns c
JOIN information_schema.key_column_usage kcu
ON c.table_schema = kcu.table_schema
AND c.table_name = kcu.table_name
AND c.column_name = kcu.column_name
JOIN information_schema.table_constraints tc
ON kcu.table_schema = tc.table_schema
AND kcu.table_name = tc.table_name
AND kcu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'UNIQUE'
AND c.table_name = $1`, [tableName]);
const uniqueTableColumns = res.rows.map(row => row.column_name);
this.uniqueColumnsMap.set(tableName, uniqueTableColumns);
} catch (e) {
console.error('Error retrieving unique column names:', e);
throw e;
}
}
async addPrimaryKeyColumns(tableName) {
try {
const res = await this.client.query(`
SELECT c.column_name
FROM information_schema.columns c
JOIN information_schema.key_column_usage kcu
ON c.table_schema = kcu.table_schema
AND c.table_name = kcu.table_name
AND c.column_name = kcu.column_name
JOIN information_schema.table_constraints tc
ON kcu.table_schema = tc.table_schema
AND kcu.table_name = tc.table_name
AND kcu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND c.table_name = $1`, [tableName]);
const primaryKeyColumns = res.rows.map(row => row.column_name);
this.primaryKeyColumnsMap.set(tableName, primaryKeyColumns);
} catch (e) {
console.error('Error retrieving primary key column names:', e);
throw e;
}
}
filterWithTableColumnName(data, tableName) {
const filteredData = {};
if (data) {
const dataMap = this.objectUtility.convertObjectToFlat(data);
const tableColumns = this.columnsMap.get(tableName);
if (!tableColumns) {
throw new Error(`Table ${tableName} columns not found. Call addTable() first.`);
}
for (const key in dataMap) {
if (tableColumns.includes(key) && dataMap[key] !== undefined) {
filteredData[key] = dataMap[key];
}
}
}
return filteredData;
}
toConditionClause(conditions) {
let whereClause = '';
const params = [];
let paramIndex = 1;
if (conditions) {
for (const [key, value] of Object.entries(conditions)) {
if (value !== undefined && value !== null) {
let operator = '=';
let v = value;
if (value && typeof value === 'object' && value.operator) {
operator = value.operator;
v = value.value;
}
whereClause += ` AND ${key} ${operator} $${paramIndex}`;
params.push(v);
paramIndex++;
}
}
}
return { whereClause, params };
}
async create(tableName, data) {
if (!this.client) {
await this.initialize();
}
const filteredData = this.filterWithTableColumnName(data, tableName);
const keys = Object.keys(filteredData);
const values = Object.values(filteredData);
if (keys.length === 0) {
throw new Error('No valid columns found for create operation');
}
const query = `
INSERT INTO ${tableName} (${keys.join(', ')})
VALUES (${values.map((_, i) => `$${i + 1}`).join(', ')})
RETURNING *`;
console.debug(query);
console.debug(values);
const result = await this.client.query(query, values);
return result.rows[0]; // Return single object for create
}
async upsert(tableName, data, conditions) {
if (!this.client) {
await this.initialize();
}
const filteredData = this.filterWithTableColumnName(data, tableName);
const keys = Object.keys(filteredData);
const values = Object.values(filteredData);
if (keys.length === 0) {
throw new Error('No valid columns found for upsert operation');
}
const { params: conditionParams } = this.toConditionClause(conditions);
const allParams = [...values, ...conditionParams];
let uniqueColumns = this.primaryKeyColumnsMap.get(tableName);
if (!uniqueColumns || uniqueColumns.length === 0) {
uniqueColumns = this.uniqueColumnsMap.get(tableName);
}
if (!uniqueColumns || uniqueColumns.length === 0) {
throw new Error(`No unique constraints found for table ${tableName}`);
}
const query = `
INSERT INTO ${tableName} (${keys.join(', ')})
VALUES (${values.map((_, i) => `$${i + 1}`).join(', ')})
ON CONFLICT (${uniqueColumns.join(', ')})
DO UPDATE SET ${keys.map((key, i) => `${key} = $${i + 1}`).join(', ')}
RETURNING *`;
console.debug(query);
console.debug(allParams);
const result = await this.client.query(query, allParams);
return result.rows[0]; // Return single object for upsert
}
async update(tableName, data, conditions) {
if (!this.client) {
await this.initialize();
}
const filteredData = this.filterWithTableColumnName(data, tableName);
if (Object.keys(filteredData).length === 0) {
throw new Error('No valid columns found for update operation');
}
const { whereClause, params: conditionParams } = this.toConditionClause(conditions);
const updateParams = Object.values(filteredData);
const allParams = [...updateParams, ...conditionParams];
const query = `
UPDATE ${tableName}
SET ${Object.keys(filteredData).map((key, i) => `${key} = $${i + 1}`).join(', ')}
WHERE 1 = 1 ${whereClause}
RETURNING *`;
const result = await this.client.query(query, allParams);
return result.rows;
}
async read(tableName, conditions = {}) {
if (!this.client) {
await this.initialize();
}
const { whereClause, params } = this.toConditionClause(conditions);
const query = `
SELECT *
FROM ${tableName}
WHERE 1 = 1 ${whereClause}`;
const result = await this.client.query(query, params);
return result.rows;
}
async delete(tableName, conditions) {
if (!this.client) {
await this.initialize();
}
const { whereClause, params } = this.toConditionClause(conditions);
const query = `
DELETE
FROM ${tableName}
WHERE 1 = 1 ${whereClause}
RETURNING *`;
const result = await this.client.query(query, params);
return result.rows;
}
async query(query, params = []) {
if (!this.client) {
await this.initialize();
}
console.debug(query);
console.debug(params);
const result = await this.client.query(query, params);
console.debug(result.rows);
return result.rows;
}
async disconnect() {
await PGClientFactory.closeConnection();
}
}
module.exports = PostgreSQLAccessor;