crud-api-express
Version:
    ![ma
249 lines (246 loc) • 11 kB
JavaScript
import { Router } from 'express';
/**
* A generic CRUD Controller for Express/Mongoose API development.
*
* It registers standard CRUD endpoints based on the provided model and endpoint string,
* along with any custom routes.
*/
class CrudController {
constructor(model, endpoint, options = {}) {
this.model = model;
this.endpoint = endpoint;
this.router = Router();
this.routes = [];
this.configureRoutes(options);
}
/**
* Applies middleware to the route handler.
* @param routeHandler The original route handler.
* @param middlewareList Optional array of middleware functions.
* @returns Array of middleware functions including the route handler.
*/
applyMiddleware(handler, middleware) {
return [...middleware, handler];
}
/**
* Registers route definitions to the internal list.
* @param method HTTP method.
* @param path URL path.
* @param params Optional route parameter names.
*/
registerRoute(method, path, params) {
this.routes.push({ method, path, params });
}
/**
* Configures all endpoints based on given options.
* @param options CrudOptions to setup CRUD behaviors.
*/
configureRoutes(options) {
// Destructure and set default middleware and callbacks
const { middleware = [], onSuccess = (res, method, result) => res.status(200).send(result), onError = (res, method, error) => res.status(400).send(error), methods = ['POST', 'GET', 'PUT', 'DELETE'] } = options;
// CREATE operation - POST /endpoint
if (methods.includes('POST')) {
const path = `/${this.endpoint}`;
this.router.post(path, this.applyMiddleware(async (req, res) => {
const method = 'POST';
try {
const result = await this.model.create(req.body);
// If a related model is defined and supports POST, create related entry
if (options.relatedModel && options.relatedMethods?.includes('POST')) {
await options.relatedModel.create({
[options.relatedField]: result._id,
...req.body
});
}
// Return 201 Created status if successful
onSuccess(res.status(201), method, result);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('POST', path);
}
// READ ALL operation - GET /endpoint
if (methods.includes('GET')) {
const path = `/${this.endpoint}`;
this.router.get(path, this.applyMiddleware(async (req, res) => {
const method = 'GET';
try {
const { filter, sort, page, limit } = req.query;
const query = filter ? JSON.parse(filter) : {};
const sortOrder = sort ? JSON.parse(sort) : {};
const pageNumber = parseInt(page, 10) || 1;
const pageSize = parseInt(limit, 10) || 10;
const skip = (pageNumber - 1) * pageSize;
let items;
if (options.relatedModel && options.relatedMethods?.includes('GET')) {
// Use aggregation with lookup if related model exists
items = await this.model.aggregate([
{ $match: query },
{
$lookup: {
from: options.relatedModel.collection.name,
localField: options.relatedField,
foreignField: '_id',
as: 'relatedData'
}
},
{ $sort: sortOrder },
{ $skip: skip },
{ $limit: pageSize }
]);
}
else {
items = await this.model.find(query).sort(sortOrder).skip(skip).limit(pageSize);
}
onSuccess(res, method, items);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
}
// READ ONE operation - GET /endpoint/:id
if (methods.includes('GET')) {
const path = `/${this.endpoint}/:id`;
this.router.get(path, this.applyMiddleware(async (req, res) => {
const method = 'GET';
try {
let item;
if (options.relatedModel && options.relatedMethods?.includes('GET')) {
const aggregateResult = await this.model.aggregate([
{ $match: { _id: req.params.id } },
{
$lookup: {
from: options.relatedModel.collection.name,
localField: options.relatedField,
foreignField: '_id',
as: 'relatedData'
}
}
]);
item = aggregateResult[0] || null;
}
else {
item = await this.model.findById(req.params.id);
}
if (!item) {
return res.status(404).send({ message: 'Item not found' });
}
onSuccess(res, method, item);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('GET', path, ['id']);
}
// UPDATE operation - PUT /endpoint/:id
if (methods.includes('PUT')) {
const path = `/${this.endpoint}/:id`;
this.router.put(path, this.applyMiddleware(async (req, res) => {
const method = 'PUT';
try {
const item = await this.model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
if (!item) {
return res.status(404).send({ message: 'Item not found' });
}
// Update related model entries if configured
if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
await options.relatedModel.updateMany({ [options.relatedField]: item._id }, req.body);
}
onSuccess(res, method, item);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('PUT', path, ['id']);
}
// DELETE MULTIPLE operation - DELETE /endpoint?filter=...
if (methods.includes('DELETE')) {
const path = `/${this.endpoint}`;
this.router.delete(path, this.applyMiddleware(async (req, res) => {
const method = 'DELETE';
try {
const query = req.query.filter ? JSON.parse(req.query.filter) : {};
const deleteResult = await this.model.deleteMany(query);
if (deleteResult.deletedCount === 0) {
return res.status(404).send({ message: 'No matching items found to delete' });
}
if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
await options.relatedModel.deleteMany({ [options.relatedField]: { $in: query } });
}
onSuccess(res, method, deleteResult);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('DELETE', path, ['filter']);
}
// DELETE ONE operation - DELETE /endpoint/:id
if (methods.includes('DELETE')) {
const path = `/${this.endpoint}/:id`;
this.router.delete(path, this.applyMiddleware(async (req, res) => {
const method = 'DELETE';
try {
const item = await this.model.findByIdAndDelete(req.params.id);
if (!item) {
return res.status(404).send({ message: 'Item not found' });
}
if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
await options.relatedModel.deleteMany({ [options.relatedField]: item._id });
}
onSuccess(res, method, item);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('DELETE', path, ['id']);
}
// AGGREGATE operation - GET /endpoint/aggregate
if (methods.includes('GET') && options.aggregatePipeline) {
const path = `/${this.endpoint}/aggregate`;
this.router.get(path, this.applyMiddleware(async (req, res) => {
const method = 'GET (Aggregate)';
try {
const pipeline = options.aggregatePipeline || [];
const results = await this.model.aggregate(pipeline);
onSuccess(res, method, results);
}
catch (error) {
onError(res, method, error);
}
}, middleware));
this.registerRoute('GET', path);
}
// CUSTOM ROUTES
if (options.customRoutes) {
options.customRoutes.forEach(route => {
const { method, path, handler } = route;
if (methods.includes(method.toUpperCase())) {
// Prepend base endpoint to custom route path
this.router[method](`/${this.endpoint}${path}`, this.applyMiddleware(handler, middleware));
this.registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
}
});
}
}
/**
* Returns the configured Express router.
*/
getRouter() {
return this.router;
}
/**
* Returns an array of registered route definitions.
*/
getRoutes() {
return this.routes;
}
}
export { CrudController as default };