228 lines
6.2 KiB
JavaScript
228 lines
6.2 KiB
JavaScript
|
|
/**
|
||
|
|
* 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,
|
||
|
|
};
|