2025-12-19 20:44:46 -06:00
|
|
|
const { query } = require("../config/database");
|
|
|
|
|
|
2026-01-04 17:52:37 -06:00
|
|
|
// Whitelist of allowed table names to prevent SQL injection
|
|
|
|
|
const ALLOWED_TABLES = [
|
|
|
|
|
"products",
|
|
|
|
|
"product_images",
|
|
|
|
|
"portfolioprojects",
|
|
|
|
|
"blogposts",
|
|
|
|
|
"pages",
|
|
|
|
|
"adminusers",
|
|
|
|
|
"roles",
|
|
|
|
|
"uploads",
|
|
|
|
|
"media_folders",
|
|
|
|
|
"team_members",
|
|
|
|
|
"site_settings",
|
|
|
|
|
"session",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Validate table name against whitelist
|
|
|
|
|
const validateTableName = (table) => {
|
|
|
|
|
if (!ALLOWED_TABLES.includes(table)) {
|
|
|
|
|
throw new Error(`Invalid table name: ${table}`);
|
|
|
|
|
}
|
|
|
|
|
return table;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
const buildSelectQuery = (
|
|
|
|
|
table,
|
|
|
|
|
conditions = [],
|
|
|
|
|
orderBy = "createdat DESC"
|
|
|
|
|
) => {
|
2026-01-04 17:52:37 -06:00
|
|
|
validateTableName(table);
|
2025-12-19 20:44:46 -06:00
|
|
|
const whereClause =
|
|
|
|
|
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
|
|
|
return `SELECT * FROM ${table} ${whereClause} ORDER BY ${orderBy}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getById = async (table, id) => {
|
2026-01-04 17:52:37 -06:00
|
|
|
validateTableName(table);
|
2025-12-19 20:44:46 -06:00
|
|
|
const result = await query(`SELECT * FROM ${table} WHERE id = $1`, [id]);
|
|
|
|
|
return result.rows[0] || null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
/**
|
|
|
|
|
* Get product with images by ID
|
|
|
|
|
* @param {string} productId - Product ID
|
|
|
|
|
* @returns {Promise<Object|null>} Product with images or null
|
|
|
|
|
*/
|
|
|
|
|
const getProductWithImages = async (productId) => {
|
|
|
|
|
const result = await query(
|
|
|
|
|
`SELECT p.*,
|
|
|
|
|
COALESCE(
|
|
|
|
|
json_agg(
|
|
|
|
|
json_build_object(
|
|
|
|
|
'id', pi.id,
|
|
|
|
|
'image_url', pi.image_url,
|
|
|
|
|
'color_variant', pi.color_variant,
|
|
|
|
|
'color_code', pi.color_code,
|
|
|
|
|
'alt_text', pi.alt_text,
|
|
|
|
|
'display_order', pi.display_order,
|
|
|
|
|
'is_primary', pi.is_primary,
|
|
|
|
|
'variant_price', pi.variant_price,
|
|
|
|
|
'variant_stock', pi.variant_stock
|
|
|
|
|
) ORDER BY pi.display_order
|
|
|
|
|
) FILTER (WHERE pi.id IS NOT NULL),
|
|
|
|
|
'[]'::json
|
|
|
|
|
) as images
|
|
|
|
|
FROM products p
|
|
|
|
|
LEFT JOIN product_images pi ON pi.product_id = p.id
|
|
|
|
|
WHERE p.id = $1
|
|
|
|
|
GROUP BY p.id`,
|
|
|
|
|
[productId]
|
|
|
|
|
);
|
|
|
|
|
return result.rows[0] || null;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
const getAllActive = async (table, orderBy = "createdat DESC") => {
|
2026-01-04 17:52:37 -06:00
|
|
|
validateTableName(table);
|
2025-12-19 20:44:46 -06:00
|
|
|
const result = await query(
|
|
|
|
|
`SELECT * FROM ${table} WHERE isactive = true ORDER BY ${orderBy}`
|
|
|
|
|
);
|
|
|
|
|
return result.rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deleteById = async (table, id) => {
|
2026-01-04 17:52:37 -06:00
|
|
|
validateTableName(table);
|
2025-12-19 20:44:46 -06:00
|
|
|
const result = await query(
|
|
|
|
|
`DELETE FROM ${table} WHERE id = $1 RETURNING id`,
|
|
|
|
|
[id]
|
|
|
|
|
);
|
|
|
|
|
return result.rowCount > 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const countRecords = async (table, condition = "") => {
|
2026-01-04 17:52:37 -06:00
|
|
|
validateTableName(table);
|
2025-12-19 20:44:46 -06:00
|
|
|
const whereClause = condition ? `WHERE ${condition}` : "";
|
|
|
|
|
const result = await query(`SELECT COUNT(*) FROM ${table} ${whereClause}`);
|
|
|
|
|
return parseInt(result.rows[0].count);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 02:22:05 -06:00
|
|
|
/**
|
|
|
|
|
* Check if record exists
|
|
|
|
|
* @param {string} table - Table name
|
|
|
|
|
* @param {string} field - Field name
|
|
|
|
|
* @param {any} value - Field value
|
|
|
|
|
* @returns {Promise<boolean>} True if exists
|
|
|
|
|
*/
|
|
|
|
|
const exists = async (table, field, value) => {
|
|
|
|
|
validateTableName(table);
|
|
|
|
|
const result = await query(
|
|
|
|
|
`SELECT EXISTS(SELECT 1 FROM ${table} WHERE ${field} = $1) as exists`,
|
|
|
|
|
[value]
|
|
|
|
|
);
|
|
|
|
|
return result.rows[0].exists;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Batch insert records
|
|
|
|
|
* @param {string} table - Table name
|
|
|
|
|
* @param {Array<Object>} records - Array of records
|
|
|
|
|
* @param {Array<string>} fields - Field names (must match for all records)
|
|
|
|
|
* @returns {Promise<Array>} Inserted records
|
|
|
|
|
*/
|
|
|
|
|
const batchInsert = async (table, records, fields) => {
|
|
|
|
|
if (!records || records.length === 0) return [];
|
|
|
|
|
validateTableName(table);
|
|
|
|
|
|
|
|
|
|
const values = [];
|
|
|
|
|
const placeholders = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
records.forEach((record) => {
|
|
|
|
|
const rowPlaceholders = fields.map(() => `$${paramIndex++}`);
|
|
|
|
|
placeholders.push(`(${rowPlaceholders.join(", ")})`);
|
|
|
|
|
fields.forEach((field) => values.push(record[field]));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sql = `
|
|
|
|
|
INSERT INTO ${table} (${fields.join(", ")})
|
|
|
|
|
VALUES ${placeholders.join(", ")}
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await query(sql, values);
|
|
|
|
|
return result.rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update multiple records by IDs
|
|
|
|
|
* @param {string} table - Table name
|
|
|
|
|
* @param {Array<string>} ids - Array of record IDs
|
|
|
|
|
* @param {Object} updates - Fields to update
|
|
|
|
|
* @returns {Promise<Array>} Updated records
|
|
|
|
|
*/
|
|
|
|
|
const batchUpdate = async (table, ids, updates) => {
|
|
|
|
|
if (!ids || ids.length === 0) return [];
|
|
|
|
|
validateTableName(table);
|
|
|
|
|
|
|
|
|
|
const updateFields = Object.keys(updates);
|
|
|
|
|
const setClause = updateFields
|
|
|
|
|
.map((field, i) => `${field} = $${i + 1}`)
|
|
|
|
|
.join(", ");
|
|
|
|
|
|
|
|
|
|
const values = [...Object.values(updates), ids];
|
|
|
|
|
const sql = `
|
|
|
|
|
UPDATE ${table}
|
|
|
|
|
SET ${setClause}, updatedat = NOW()
|
|
|
|
|
WHERE id = ANY($${updateFields.length + 1})
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await query(sql, values);
|
|
|
|
|
return result.rows;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute query with transaction
|
|
|
|
|
* @param {Function} callback - Callback function that receives client
|
|
|
|
|
* @returns {Promise<any>} Result from callback
|
|
|
|
|
*/
|
|
|
|
|
const withTransaction = async (callback) => {
|
|
|
|
|
const { pool } = require("../config/database");
|
|
|
|
|
const client = await pool.connect();
|
|
|
|
|
try {
|
|
|
|
|
await client.query("BEGIN");
|
|
|
|
|
const result = await callback(client);
|
|
|
|
|
await client.query("COMMIT");
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await client.query("ROLLBACK");
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
client.release();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 20:44:46 -06:00
|
|
|
module.exports = {
|
|
|
|
|
buildSelectQuery,
|
|
|
|
|
getById,
|
2026-01-18 02:22:05 -06:00
|
|
|
getProductWithImages,
|
2025-12-19 20:44:46 -06:00
|
|
|
getAllActive,
|
|
|
|
|
deleteById,
|
|
|
|
|
countRecords,
|
2026-01-18 02:22:05 -06:00
|
|
|
exists,
|
|
|
|
|
batchInsert,
|
|
|
|
|
batchUpdate,
|
|
|
|
|
withTransaction,
|
2026-01-04 17:52:37 -06:00
|
|
|
validateTableName,
|
2025-12-19 20:44:46 -06:00
|
|
|
};
|