UNPKG

crud-api-express

Version:

![npm](https://img.shields.io/npm/v/crud-api-express) ![downloads](https://img.shields.io/npm/dm/crud-api-express) ![license](https://img.shields.io/npm/l/crud-api-express) ![made-with-node](https://img.shields.io/badge/Made%20with-Node.js-green) ![ma

251 lines (247 loc) 11 kB
'use strict'; var express = require('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 = express.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; } } module.exports = CrudController;