/** * CRUD Route Factory * Generates standardized CRUD routes with consistent patterns */ const { query } = require("../config/database"); const { asyncHandler } = require("../middleware/errorHandler"); const { requireAuth } = require("../middleware/auth"); const { sendSuccess, sendNotFound } = require("../utils/responseHelpers"); const { getById, deleteById, countRecords } = require("./queryHelpers"); const { validateRequiredFields, generateSlug } = require("./validation"); const { HTTP_STATUS } = require("../config/constants"); /** * Create standardized CRUD routes for a resource * @param {Object} config - Configuration object * @param {string} config.table - Database table name * @param {string} config.resourceName - Resource name (plural, e.g., 'products') * @param {string} config.singularName - Singular resource name (e.g., 'product') * @param {string[]} config.listFields - Fields to select in list endpoint * @param {string[]} config.requiredFields - Required fields for creation * @param {Function} config.beforeCreate - Hook before creation * @param {Function} config.afterCreate - Hook after creation * @param {Function} config.beforeUpdate - Hook before update * @param {Function} config.afterUpdate - Hook after update * @param {Function} config.cacheInvalidate - Function to invalidate cache * @returns {Object} Object with route handlers */ const createCRUDHandlers = (config) => { const { table, resourceName, singularName, listFields = "*", requiredFields = [], beforeCreate, afterCreate, beforeUpdate, afterUpdate, cacheInvalidate, } = config; return { /** * List all resources * GET /:resource */ list: asyncHandler(async (req, res) => { const result = await query( `SELECT ${listFields} FROM ${table} ORDER BY createdat DESC` ); sendSuccess(res, { [resourceName]: result.rows }); }), /** * Get single resource by ID * GET /:resource/:id */ getById: asyncHandler(async (req, res) => { const item = await getById(table, req.params.id); if (!item) { return sendNotFound(res, singularName); } sendSuccess(res, { [singularName]: item }); }), /** * Create new resource * POST /:resource */ create: asyncHandler(async (req, res) => { // Validate required fields if (requiredFields.length > 0) { validateRequiredFields(req.body, requiredFields); } // Run beforeCreate hook if provided let data = { ...req.body }; if (beforeCreate) { data = await beforeCreate(data, req); } // Build insert query dynamically const fields = Object.keys(data); const placeholders = fields.map((_, i) => `$${i + 1}`).join(", "); const values = fields.map((key) => data[key]); const result = await query( `INSERT INTO ${table} (${fields.join(", ")}, createdat) VALUES (${placeholders}, NOW()) RETURNING *`, values ); let created = result.rows[0]; // Run afterCreate hook if provided if (afterCreate) { created = await afterCreate(created, req); } // Invalidate cache if function provided if (cacheInvalidate) { cacheInvalidate(); } sendSuccess( res, { [singularName]: created, message: `${singularName} created successfully`, }, HTTP_STATUS.CREATED ); }), /** * Update resource by ID * PUT /:resource/:id */ update: asyncHandler(async (req, res) => { // Check if resource exists const existing = await getById(table, req.params.id); if (!existing) { return sendNotFound(res, singularName); } // Run beforeUpdate hook if provided let data = { ...req.body }; if (beforeUpdate) { data = await beforeUpdate(data, req, existing); } // Build update query dynamically const updates = []; const values = []; let paramIndex = 1; Object.entries(data).forEach(([key, value]) => { if (value !== undefined) { updates.push(`${key} = $${paramIndex}`); values.push(value); paramIndex++; } }); if (updates.length === 0) { return sendSuccess(res, { [singularName]: existing, message: "No changes to update", }); } updates.push(`updatedat = NOW()`); values.push(req.params.id); const result = await query( `UPDATE ${table} SET ${updates.join( ", " )} WHERE id = $${paramIndex} RETURNING *`, values ); let updated = result.rows[0]; // Run afterUpdate hook if provided if (afterUpdate) { updated = await afterUpdate(updated, req, existing); } // Invalidate cache if function provided if (cacheInvalidate) { cacheInvalidate(); } sendSuccess(res, { [singularName]: updated, message: `${singularName} updated successfully`, }); }), /** * Delete resource by ID * DELETE /:resource/:id */ delete: asyncHandler(async (req, res) => { const deleted = await deleteById(table, req.params.id); if (!deleted) { return sendNotFound(res, singularName); } // Invalidate cache if function provided if (cacheInvalidate) { cacheInvalidate(); } sendSuccess(res, { message: `${singularName} deleted successfully`, }); }), }; }; /** * Attach CRUD handlers to a router * @param {Router} router - Express router * @param {string} path - Base path for routes * @param {Object} handlers - CRUD handlers object * @param {Function} authMiddleware - Authentication middleware (default: requireAuth) */ const attachCRUDRoutes = ( router, path, handlers, authMiddleware = requireAuth ) => { router.get(`/${path}`, authMiddleware, handlers.list); router.get(`/${path}/:id`, authMiddleware, handlers.getById); router.post(`/${path}`, authMiddleware, handlers.create); router.put(`/${path}/:id`, authMiddleware, handlers.update); router.delete(`/${path}/:id`, authMiddleware, handlers.delete); }; module.exports = { createCRUDHandlers, attachCRUDRoutes, };