crud-api-express 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  var express = require('express');
4
4
 
5
+ /**
6
+ * A generic CRUD Controller for Express/Mongoose API development.
7
+ *
8
+ * It registers standard CRUD endpoints based on the provided model and endpoint string,
9
+ * along with any custom routes.
10
+ */
5
11
  class CrudController {
6
12
  constructor(model, endpoint, options = {}) {
7
13
  this.model = model;
@@ -10,37 +16,58 @@ class CrudController {
10
16
  this.routes = [];
11
17
  this.configureRoutes(options);
12
18
  }
19
+ /**
20
+ * Applies middleware to the route handler.
21
+ * @param routeHandler The original route handler.
22
+ * @param middlewareList Optional array of middleware functions.
23
+ * @returns Array of middleware functions including the route handler.
24
+ */
25
+ applyMiddleware(handler, middleware) {
26
+ return [...middleware, handler];
27
+ }
28
+ /**
29
+ * Registers route definitions to the internal list.
30
+ * @param method HTTP method.
31
+ * @param path URL path.
32
+ * @param params Optional route parameter names.
33
+ */
34
+ registerRoute(method, path, params) {
35
+ this.routes.push({ method, path, params });
36
+ }
37
+ /**
38
+ * Configures all endpoints based on given options.
39
+ * @param options CrudOptions to setup CRUD behaviors.
40
+ */
13
41
  configureRoutes(options) {
42
+ // Destructure and set default middleware and callbacks
14
43
  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;
15
- const applyMiddleware = (routeHandler) => {
16
- return [...middleware, routeHandler];
17
- };
18
- // Helper to register a route
19
- const registerRoute = (method, path, params) => {
20
- this.routes.push({ method, path, params });
21
- };
22
- // Create
44
+ // CREATE operation - POST /endpoint
23
45
  if (methods.includes('POST')) {
24
46
  const path = `/${this.endpoint}`;
25
- this.router.post(path, applyMiddleware(async (req, res) => {
47
+ this.router.post(path, this.applyMiddleware(async (req, res) => {
26
48
  const method = 'POST';
27
49
  try {
28
50
  const result = await this.model.create(req.body);
51
+ // If a related model is defined and supports POST, create related entry
29
52
  if (options.relatedModel && options.relatedMethods?.includes('POST')) {
30
- await options.relatedModel.create({ [options.relatedField]: result._id, ...req.body });
53
+ await options.relatedModel.create({
54
+ [options.relatedField]: result._id,
55
+ ...req.body
56
+ });
31
57
  }
32
- onSuccess(res, method, result);
58
+ // Return 201 Created status if successful
59
+ onSuccess(res.status(201), method, result);
33
60
  }
34
61
  catch (error) {
35
62
  onError(res, method, error);
36
63
  }
37
- }));
38
- registerRoute('POST', path);
64
+ }, middleware));
65
+ this.registerRoute('POST', path);
39
66
  }
40
- // Read all
67
+ // READ ALL operation - GET /endpoint
41
68
  if (methods.includes('GET')) {
42
69
  const path = `/${this.endpoint}`;
43
- this.router.get(path, applyMiddleware(async (req, res) => {
70
+ this.router.get(path, this.applyMiddleware(async (req, res) => {
44
71
  const method = 'GET';
45
72
  try {
46
73
  const { filter, sort, page, limit } = req.query;
@@ -51,6 +78,7 @@ class CrudController {
51
78
  const skip = (pageNumber - 1) * pageSize;
52
79
  let items;
53
80
  if (options.relatedModel && options.relatedMethods?.includes('GET')) {
81
+ // Use aggregation with lookup if related model exists
54
82
  items = await this.model.aggregate([
55
83
  { $match: query },
56
84
  {
@@ -74,13 +102,13 @@ class CrudController {
74
102
  catch (error) {
75
103
  onError(res, method, error);
76
104
  }
77
- }));
78
- registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
105
+ }, middleware));
106
+ this.registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
79
107
  }
80
- // Read one
108
+ // READ ONE operation - GET /endpoint/:id
81
109
  if (methods.includes('GET')) {
82
110
  const path = `/${this.endpoint}/:id`;
83
- this.router.get(path, applyMiddleware(async (req, res) => {
111
+ this.router.get(path, this.applyMiddleware(async (req, res) => {
84
112
  const method = 'GET';
85
113
  try {
86
114
  let item;
@@ -96,32 +124,33 @@ class CrudController {
96
124
  }
97
125
  }
98
126
  ]);
99
- item = aggregateResult[0];
127
+ item = aggregateResult[0] || null;
100
128
  }
101
129
  else {
102
130
  item = await this.model.findById(req.params.id);
103
131
  }
104
132
  if (!item) {
105
- return res.status(404).send();
133
+ return res.status(404).send({ message: 'Item not found' });
106
134
  }
107
135
  onSuccess(res, method, item);
108
136
  }
109
137
  catch (error) {
110
138
  onError(res, method, error);
111
139
  }
112
- }));
113
- registerRoute('GET', path, ['id']);
140
+ }, middleware));
141
+ this.registerRoute('GET', path, ['id']);
114
142
  }
115
- // Update
143
+ // UPDATE operation - PUT /endpoint/:id
116
144
  if (methods.includes('PUT')) {
117
145
  const path = `/${this.endpoint}/:id`;
118
- this.router.put(path, applyMiddleware(async (req, res) => {
146
+ this.router.put(path, this.applyMiddleware(async (req, res) => {
119
147
  const method = 'PUT';
120
148
  try {
121
149
  const item = await this.model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
122
150
  if (!item) {
123
- return res.status(404).send();
151
+ return res.status(404).send({ message: 'Item not found' });
124
152
  }
153
+ // Update related model entries if configured
125
154
  if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
126
155
  await options.relatedModel.updateMany({ [options.relatedField]: item._id }, req.body);
127
156
  }
@@ -130,19 +159,19 @@ class CrudController {
130
159
  catch (error) {
131
160
  onError(res, method, error);
132
161
  }
133
- }));
134
- registerRoute('PUT', path, ['id']);
162
+ }, middleware));
163
+ this.registerRoute('PUT', path, ['id']);
135
164
  }
136
- // Delete multiple
165
+ // DELETE MULTIPLE operation - DELETE /endpoint?filter=...
137
166
  if (methods.includes('DELETE')) {
138
167
  const path = `/${this.endpoint}`;
139
- this.router.delete(path, applyMiddleware(async (req, res) => {
168
+ this.router.delete(path, this.applyMiddleware(async (req, res) => {
140
169
  const method = 'DELETE';
141
170
  try {
142
171
  const query = req.query.filter ? JSON.parse(req.query.filter) : {};
143
172
  const deleteResult = await this.model.deleteMany(query);
144
173
  if (deleteResult.deletedCount === 0) {
145
- return res.status(404).send();
174
+ return res.status(404).send({ message: 'No matching items found to delete' });
146
175
  }
147
176
  if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
148
177
  await options.relatedModel.deleteMany({ [options.relatedField]: { $in: query } });
@@ -152,18 +181,18 @@ class CrudController {
152
181
  catch (error) {
153
182
  onError(res, method, error);
154
183
  }
155
- }));
156
- registerRoute('DELETE', path, ['filter']);
184
+ }, middleware));
185
+ this.registerRoute('DELETE', path, ['filter']);
157
186
  }
158
- // Delete one
187
+ // DELETE ONE operation - DELETE /endpoint/:id
159
188
  if (methods.includes('DELETE')) {
160
189
  const path = `/${this.endpoint}/:id`;
161
- this.router.delete(path, applyMiddleware(async (req, res) => {
190
+ this.router.delete(path, this.applyMiddleware(async (req, res) => {
162
191
  const method = 'DELETE';
163
192
  try {
164
193
  const item = await this.model.findByIdAndDelete(req.params.id);
165
194
  if (!item) {
166
- return res.status(404).send();
195
+ return res.status(404).send({ message: 'Item not found' });
167
196
  }
168
197
  if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
169
198
  await options.relatedModel.deleteMany({ [options.relatedField]: item._id });
@@ -173,39 +202,46 @@ class CrudController {
173
202
  catch (error) {
174
203
  onError(res, method, error);
175
204
  }
176
- }));
177
- registerRoute('DELETE', path, ['id']);
205
+ }, middleware));
206
+ this.registerRoute('DELETE', path, ['id']);
178
207
  }
179
- // Aggregate
208
+ // AGGREGATE operation - GET /endpoint/aggregate
180
209
  if (methods.includes('GET') && options.aggregatePipeline) {
181
210
  const path = `/${this.endpoint}/aggregate`;
182
- this.router.get(path, applyMiddleware(async (req, res) => {
211
+ this.router.get(path, this.applyMiddleware(async (req, res) => {
183
212
  const method = 'GET (Aggregate)';
184
213
  try {
185
- const pipeline = options.aggregatePipeline ?? [];
214
+ const pipeline = options.aggregatePipeline || [];
186
215
  const results = await this.model.aggregate(pipeline);
187
216
  onSuccess(res, method, results);
188
217
  }
189
218
  catch (error) {
190
219
  onError(res, method, error);
191
220
  }
192
- }));
193
- registerRoute('GET', path);
221
+ }, middleware));
222
+ this.registerRoute('GET', path);
194
223
  }
195
- // Custom routes
224
+ // CUSTOM ROUTES
196
225
  if (options.customRoutes) {
197
226
  options.customRoutes.forEach(route => {
198
227
  const { method, path, handler } = route;
199
228
  if (methods.includes(method.toUpperCase())) {
200
- this.router[method](`/${this.endpoint}${path}`, applyMiddleware(handler));
201
- registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
229
+ // Prepend base endpoint to custom route path
230
+ this.router[method](`/${this.endpoint}${path}`, this.applyMiddleware(handler, middleware));
231
+ this.registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
202
232
  }
203
233
  });
204
234
  }
205
235
  }
236
+ /**
237
+ * Returns the configured Express router.
238
+ */
206
239
  getRouter() {
207
240
  return this.router;
208
241
  }
242
+ /**
243
+ * Returns an array of registered route definitions.
244
+ */
209
245
  getRoutes() {
210
246
  return this.routes;
211
247
  }
package/dist/index.d.ts CHANGED
@@ -25,9 +25,12 @@
25
25
  /// <reference types="mongoose/types/inferrawdoctype" />
26
26
  import { Request, Response, Router, NextFunction } from 'express';
27
27
  import { Document, Model } from 'mongoose';
28
- interface CrudOptions<T extends Document> {
28
+ /**
29
+ * Options for configuring the CRUD controller behavior.
30
+ */
31
+ export interface CrudOptions<T extends Document> {
29
32
  middleware?: ((req: Request, res: Response, next: NextFunction) => void)[];
30
- onSuccess?: (res: Response, method: string, result: T | T[]) => void;
33
+ onSuccess?: (res: Response, method: string, result: T | T[] | any) => void;
31
34
  onError?: (res: Response, method: string, error: Error) => void;
32
35
  methods?: ('POST' | 'GET' | 'PUT' | 'DELETE')[];
33
36
  relatedModel?: Model<any>;
@@ -40,14 +43,44 @@ interface CrudOptions<T extends Document> {
40
43
  handler: (req: Request, res: Response) => void;
41
44
  }[];
42
45
  }
46
+ /**
47
+ * A generic CRUD Controller for Express/Mongoose API development.
48
+ *
49
+ * It registers standard CRUD endpoints based on the provided model and endpoint string,
50
+ * along with any custom routes.
51
+ */
43
52
  declare class CrudController<T extends Document> {
44
53
  private model;
45
54
  private endpoint;
46
55
  private router;
47
56
  private routes;
48
57
  constructor(model: Model<T>, endpoint: string, options?: CrudOptions<T>);
58
+ /**
59
+ * Applies middleware to the route handler.
60
+ * @param routeHandler The original route handler.
61
+ * @param middlewareList Optional array of middleware functions.
62
+ * @returns Array of middleware functions including the route handler.
63
+ */
64
+ private applyMiddleware;
65
+ /**
66
+ * Registers route definitions to the internal list.
67
+ * @param method HTTP method.
68
+ * @param path URL path.
69
+ * @param params Optional route parameter names.
70
+ */
71
+ private registerRoute;
72
+ /**
73
+ * Configures all endpoints based on given options.
74
+ * @param options CrudOptions to setup CRUD behaviors.
75
+ */
49
76
  private configureRoutes;
77
+ /**
78
+ * Returns the configured Express router.
79
+ */
50
80
  getRouter(): Router;
81
+ /**
82
+ * Returns an array of registered route definitions.
83
+ */
51
84
  getRoutes(): {
52
85
  method: string;
53
86
  path: string;
package/dist/index.mjs CHANGED
@@ -1,5 +1,11 @@
1
1
  import { Router } from 'express';
2
2
 
3
+ /**
4
+ * A generic CRUD Controller for Express/Mongoose API development.
5
+ *
6
+ * It registers standard CRUD endpoints based on the provided model and endpoint string,
7
+ * along with any custom routes.
8
+ */
3
9
  class CrudController {
4
10
  constructor(model, endpoint, options = {}) {
5
11
  this.model = model;
@@ -8,37 +14,58 @@ class CrudController {
8
14
  this.routes = [];
9
15
  this.configureRoutes(options);
10
16
  }
17
+ /**
18
+ * Applies middleware to the route handler.
19
+ * @param routeHandler The original route handler.
20
+ * @param middlewareList Optional array of middleware functions.
21
+ * @returns Array of middleware functions including the route handler.
22
+ */
23
+ applyMiddleware(handler, middleware) {
24
+ return [...middleware, handler];
25
+ }
26
+ /**
27
+ * Registers route definitions to the internal list.
28
+ * @param method HTTP method.
29
+ * @param path URL path.
30
+ * @param params Optional route parameter names.
31
+ */
32
+ registerRoute(method, path, params) {
33
+ this.routes.push({ method, path, params });
34
+ }
35
+ /**
36
+ * Configures all endpoints based on given options.
37
+ * @param options CrudOptions to setup CRUD behaviors.
38
+ */
11
39
  configureRoutes(options) {
40
+ // Destructure and set default middleware and callbacks
12
41
  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;
13
- const applyMiddleware = (routeHandler) => {
14
- return [...middleware, routeHandler];
15
- };
16
- // Helper to register a route
17
- const registerRoute = (method, path, params) => {
18
- this.routes.push({ method, path, params });
19
- };
20
- // Create
42
+ // CREATE operation - POST /endpoint
21
43
  if (methods.includes('POST')) {
22
44
  const path = `/${this.endpoint}`;
23
- this.router.post(path, applyMiddleware(async (req, res) => {
45
+ this.router.post(path, this.applyMiddleware(async (req, res) => {
24
46
  const method = 'POST';
25
47
  try {
26
48
  const result = await this.model.create(req.body);
49
+ // If a related model is defined and supports POST, create related entry
27
50
  if (options.relatedModel && options.relatedMethods?.includes('POST')) {
28
- await options.relatedModel.create({ [options.relatedField]: result._id, ...req.body });
51
+ await options.relatedModel.create({
52
+ [options.relatedField]: result._id,
53
+ ...req.body
54
+ });
29
55
  }
30
- onSuccess(res, method, result);
56
+ // Return 201 Created status if successful
57
+ onSuccess(res.status(201), method, result);
31
58
  }
32
59
  catch (error) {
33
60
  onError(res, method, error);
34
61
  }
35
- }));
36
- registerRoute('POST', path);
62
+ }, middleware));
63
+ this.registerRoute('POST', path);
37
64
  }
38
- // Read all
65
+ // READ ALL operation - GET /endpoint
39
66
  if (methods.includes('GET')) {
40
67
  const path = `/${this.endpoint}`;
41
- this.router.get(path, applyMiddleware(async (req, res) => {
68
+ this.router.get(path, this.applyMiddleware(async (req, res) => {
42
69
  const method = 'GET';
43
70
  try {
44
71
  const { filter, sort, page, limit } = req.query;
@@ -49,6 +76,7 @@ class CrudController {
49
76
  const skip = (pageNumber - 1) * pageSize;
50
77
  let items;
51
78
  if (options.relatedModel && options.relatedMethods?.includes('GET')) {
79
+ // Use aggregation with lookup if related model exists
52
80
  items = await this.model.aggregate([
53
81
  { $match: query },
54
82
  {
@@ -72,13 +100,13 @@ class CrudController {
72
100
  catch (error) {
73
101
  onError(res, method, error);
74
102
  }
75
- }));
76
- registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
103
+ }, middleware));
104
+ this.registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
77
105
  }
78
- // Read one
106
+ // READ ONE operation - GET /endpoint/:id
79
107
  if (methods.includes('GET')) {
80
108
  const path = `/${this.endpoint}/:id`;
81
- this.router.get(path, applyMiddleware(async (req, res) => {
109
+ this.router.get(path, this.applyMiddleware(async (req, res) => {
82
110
  const method = 'GET';
83
111
  try {
84
112
  let item;
@@ -94,32 +122,33 @@ class CrudController {
94
122
  }
95
123
  }
96
124
  ]);
97
- item = aggregateResult[0];
125
+ item = aggregateResult[0] || null;
98
126
  }
99
127
  else {
100
128
  item = await this.model.findById(req.params.id);
101
129
  }
102
130
  if (!item) {
103
- return res.status(404).send();
131
+ return res.status(404).send({ message: 'Item not found' });
104
132
  }
105
133
  onSuccess(res, method, item);
106
134
  }
107
135
  catch (error) {
108
136
  onError(res, method, error);
109
137
  }
110
- }));
111
- registerRoute('GET', path, ['id']);
138
+ }, middleware));
139
+ this.registerRoute('GET', path, ['id']);
112
140
  }
113
- // Update
141
+ // UPDATE operation - PUT /endpoint/:id
114
142
  if (methods.includes('PUT')) {
115
143
  const path = `/${this.endpoint}/:id`;
116
- this.router.put(path, applyMiddleware(async (req, res) => {
144
+ this.router.put(path, this.applyMiddleware(async (req, res) => {
117
145
  const method = 'PUT';
118
146
  try {
119
147
  const item = await this.model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
120
148
  if (!item) {
121
- return res.status(404).send();
149
+ return res.status(404).send({ message: 'Item not found' });
122
150
  }
151
+ // Update related model entries if configured
123
152
  if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
124
153
  await options.relatedModel.updateMany({ [options.relatedField]: item._id }, req.body);
125
154
  }
@@ -128,19 +157,19 @@ class CrudController {
128
157
  catch (error) {
129
158
  onError(res, method, error);
130
159
  }
131
- }));
132
- registerRoute('PUT', path, ['id']);
160
+ }, middleware));
161
+ this.registerRoute('PUT', path, ['id']);
133
162
  }
134
- // Delete multiple
163
+ // DELETE MULTIPLE operation - DELETE /endpoint?filter=...
135
164
  if (methods.includes('DELETE')) {
136
165
  const path = `/${this.endpoint}`;
137
- this.router.delete(path, applyMiddleware(async (req, res) => {
166
+ this.router.delete(path, this.applyMiddleware(async (req, res) => {
138
167
  const method = 'DELETE';
139
168
  try {
140
169
  const query = req.query.filter ? JSON.parse(req.query.filter) : {};
141
170
  const deleteResult = await this.model.deleteMany(query);
142
171
  if (deleteResult.deletedCount === 0) {
143
- return res.status(404).send();
172
+ return res.status(404).send({ message: 'No matching items found to delete' });
144
173
  }
145
174
  if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
146
175
  await options.relatedModel.deleteMany({ [options.relatedField]: { $in: query } });
@@ -150,18 +179,18 @@ class CrudController {
150
179
  catch (error) {
151
180
  onError(res, method, error);
152
181
  }
153
- }));
154
- registerRoute('DELETE', path, ['filter']);
182
+ }, middleware));
183
+ this.registerRoute('DELETE', path, ['filter']);
155
184
  }
156
- // Delete one
185
+ // DELETE ONE operation - DELETE /endpoint/:id
157
186
  if (methods.includes('DELETE')) {
158
187
  const path = `/${this.endpoint}/:id`;
159
- this.router.delete(path, applyMiddleware(async (req, res) => {
188
+ this.router.delete(path, this.applyMiddleware(async (req, res) => {
160
189
  const method = 'DELETE';
161
190
  try {
162
191
  const item = await this.model.findByIdAndDelete(req.params.id);
163
192
  if (!item) {
164
- return res.status(404).send();
193
+ return res.status(404).send({ message: 'Item not found' });
165
194
  }
166
195
  if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
167
196
  await options.relatedModel.deleteMany({ [options.relatedField]: item._id });
@@ -171,39 +200,46 @@ class CrudController {
171
200
  catch (error) {
172
201
  onError(res, method, error);
173
202
  }
174
- }));
175
- registerRoute('DELETE', path, ['id']);
203
+ }, middleware));
204
+ this.registerRoute('DELETE', path, ['id']);
176
205
  }
177
- // Aggregate
206
+ // AGGREGATE operation - GET /endpoint/aggregate
178
207
  if (methods.includes('GET') && options.aggregatePipeline) {
179
208
  const path = `/${this.endpoint}/aggregate`;
180
- this.router.get(path, applyMiddleware(async (req, res) => {
209
+ this.router.get(path, this.applyMiddleware(async (req, res) => {
181
210
  const method = 'GET (Aggregate)';
182
211
  try {
183
- const pipeline = options.aggregatePipeline ?? [];
212
+ const pipeline = options.aggregatePipeline || [];
184
213
  const results = await this.model.aggregate(pipeline);
185
214
  onSuccess(res, method, results);
186
215
  }
187
216
  catch (error) {
188
217
  onError(res, method, error);
189
218
  }
190
- }));
191
- registerRoute('GET', path);
219
+ }, middleware));
220
+ this.registerRoute('GET', path);
192
221
  }
193
- // Custom routes
222
+ // CUSTOM ROUTES
194
223
  if (options.customRoutes) {
195
224
  options.customRoutes.forEach(route => {
196
225
  const { method, path, handler } = route;
197
226
  if (methods.includes(method.toUpperCase())) {
198
- this.router[method](`/${this.endpoint}${path}`, applyMiddleware(handler));
199
- registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
227
+ // Prepend base endpoint to custom route path
228
+ this.router[method](`/${this.endpoint}${path}`, this.applyMiddleware(handler, middleware));
229
+ this.registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
200
230
  }
201
231
  });
202
232
  }
203
233
  }
234
+ /**
235
+ * Returns the configured Express router.
236
+ */
204
237
  getRouter() {
205
238
  return this.router;
206
239
  }
240
+ /**
241
+ * Returns an array of registered route definitions.
242
+ */
207
243
  getRoutes() {
208
244
  return this.routes;
209
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crud-api-express",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -1,23 +1,36 @@
1
1
  import { Request, Response, Router, NextFunction } from 'express';
2
2
  import { Document, Model, Aggregate } from 'mongoose';
3
3
 
4
- interface CrudOptions<T extends Document> {
4
+ /**
5
+ * Options for configuring the CRUD controller behavior.
6
+ */
7
+ export interface CrudOptions<T extends Document> {
5
8
  middleware?: ((req: Request, res: Response, next: NextFunction) => void)[];
6
- onSuccess?: (res: Response, method: string, result: T | T[]) => void;
9
+ onSuccess?: (res: Response, method: string, result: T | T[] | any) => void;
7
10
  onError?: (res: Response, method: string, error: Error) => void;
8
11
  methods?: ('POST' | 'GET' | 'PUT' | 'DELETE')[];
9
12
  relatedModel?: Model<any>;
10
13
  relatedField?: string;
11
14
  relatedMethods?: ('POST' | 'GET' | 'PUT' | 'DELETE')[];
12
15
  aggregatePipeline?: object[];
13
- customRoutes?: { method: 'post' | 'get' | 'put' | 'delete', path: string, handler: (req: Request, res: Response) => void }[];
16
+ customRoutes?: {
17
+ method: 'post' | 'get' | 'put' | 'delete',
18
+ path: string,
19
+ handler: (req: Request, res: Response) => void
20
+ }[];
14
21
  }
15
22
 
23
+ /**
24
+ * A generic CRUD Controller for Express/Mongoose API development.
25
+ *
26
+ * It registers standard CRUD endpoints based on the provided model and endpoint string,
27
+ * along with any custom routes.
28
+ */
16
29
  class CrudController<T extends Document> {
17
30
  private model: Model<T>;
18
31
  private endpoint: string;
19
32
  private router: Router;
20
- private routes: { method: string, path: string, params?: string[] }[];
33
+ private routes: { method: string; path: string; params?: string[] }[];
21
34
 
22
35
  constructor(model: Model<T>, endpoint: string, options: CrudOptions<T> = {}) {
23
36
  this.model = model;
@@ -27,7 +40,33 @@ class CrudController<T extends Document> {
27
40
  this.configureRoutes(options);
28
41
  }
29
42
 
43
+ /**
44
+ * Applies middleware to the route handler.
45
+ * @param routeHandler The original route handler.
46
+ * @param middlewareList Optional array of middleware functions.
47
+ * @returns Array of middleware functions including the route handler.
48
+ */
49
+ private applyMiddleware(handler: (req: Request, res: Response) => void, middleware: ((req: Request, res: Response, next: NextFunction) => void)[]) {
50
+ return [...middleware, handler];
51
+ }
52
+
53
+
54
+ /**
55
+ * Registers route definitions to the internal list.
56
+ * @param method HTTP method.
57
+ * @param path URL path.
58
+ * @param params Optional route parameter names.
59
+ */
60
+ private registerRoute(method: string, path: string, params?: string[]) {
61
+ this.routes.push({ method, path, params });
62
+ }
63
+
64
+ /**
65
+ * Configures all endpoints based on given options.
66
+ * @param options CrudOptions to setup CRUD behaviors.
67
+ */
30
68
  private configureRoutes(options: CrudOptions<T>) {
69
+ // Destructure and set default middleware and callbacks
31
70
  const {
32
71
  middleware = [],
33
72
  onSuccess = (res, method, result) => res.status(200).send(result),
@@ -35,204 +74,230 @@ class CrudController<T extends Document> {
35
74
  methods = ['POST', 'GET', 'PUT', 'DELETE']
36
75
  } = options;
37
76
 
38
- const applyMiddleware = (routeHandler: (req: Request, res: Response) => void) => {
39
- return [...middleware, routeHandler];
40
- };
41
-
42
- // Helper to register a route
43
- const registerRoute = (method: string, path: string, params?: string[]) => {
44
- this.routes.push({ method, path, params });
45
- };
46
-
47
- // Create
77
+ // CREATE operation - POST /endpoint
48
78
  if (methods.includes('POST')) {
49
79
  const path = `/${this.endpoint}`;
50
- this.router.post(path, applyMiddleware(async (req, res) => {
51
- const method = 'POST';
52
- try {
53
- const result: any = await this.model.create(req.body);
54
- if (options.relatedModel && options.relatedMethods?.includes('POST')) {
55
- await options.relatedModel.create({ [options.relatedField!]: result._id, ...req.body });
80
+ this.router.post(
81
+ path,
82
+ this.applyMiddleware(async (req, res) => {
83
+ const method = 'POST';
84
+ try {
85
+ const result: any = await this.model.create(req.body);
86
+ // If a related model is defined and supports POST, create related entry
87
+ if (options.relatedModel && options.relatedMethods?.includes('POST')) {
88
+ await options.relatedModel.create({
89
+ [options.relatedField!]: result._id,
90
+ ...req.body
91
+ });
92
+ }
93
+ // Return 201 Created status if successful
94
+ onSuccess(res.status(201), method, result);
95
+ } catch (error: any) {
96
+ onError(res, method, error);
56
97
  }
57
- onSuccess(res, method, result);
58
- } catch (error: any) {
59
- onError(res, method, error);
60
- }
61
- }));
62
- registerRoute('POST', path);
98
+ }, middleware)
99
+ );
100
+ this.registerRoute('POST', path);
63
101
  }
64
102
 
65
- // Read all
103
+ // READ ALL operation - GET /endpoint
66
104
  if (methods.includes('GET')) {
67
105
  const path = `/${this.endpoint}`;
68
- this.router.get(path, applyMiddleware(async (req, res) => {
69
- const method = 'GET';
70
- try {
71
- const { filter, sort, page, limit } = req.query;
72
- const query = filter ? JSON.parse(filter as string) : {};
73
- const sortOrder = sort ? JSON.parse(sort as string) : {};
74
- const pageNumber = parseInt(page as string, 10) || 1;
75
- const pageSize = parseInt(limit as string, 10) || 10;
76
- const skip = (pageNumber - 1) * pageSize;
77
-
78
- let items: T[] | Aggregate<any[]>;
79
- if (options.relatedModel && options.relatedMethods?.includes('GET')) {
80
- items = await this.model.aggregate([
81
- { $match: query },
82
- {
83
- $lookup: {
84
- from: options.relatedModel.collection.name,
85
- localField: options.relatedField!,
86
- foreignField: '_id',
87
- as: 'relatedData'
88
- }
89
- },
90
- { $sort: sortOrder },
91
- { $skip: skip },
92
- { $limit: pageSize }
93
- ]);
94
- } else {
95
- items = await this.model.find(query).sort(sortOrder).skip(skip).limit(pageSize);
106
+ this.router.get(
107
+ path,
108
+ this.applyMiddleware(async (req, res) => {
109
+ const method = 'GET';
110
+ try {
111
+ const { filter, sort, page, limit } = req.query;
112
+ const query = filter ? JSON.parse(filter as string) : {};
113
+ const sortOrder = sort ? JSON.parse(sort as string) : {};
114
+ const pageNumber = parseInt(page as string, 10) || 1;
115
+ const pageSize = parseInt(limit as string, 10) || 10;
116
+ const skip = (pageNumber - 1) * pageSize;
117
+
118
+ let items: T[] | Aggregate<any[]>;
119
+ if (options.relatedModel && options.relatedMethods?.includes('GET')) {
120
+ // Use aggregation with lookup if related model exists
121
+ items = await this.model.aggregate([
122
+ { $match: query },
123
+ {
124
+ $lookup: {
125
+ from: options.relatedModel.collection.name,
126
+ localField: options.relatedField!,
127
+ foreignField: '_id',
128
+ as: 'relatedData'
129
+ }
130
+ },
131
+ { $sort: sortOrder },
132
+ { $skip: skip },
133
+ { $limit: pageSize }
134
+ ]);
135
+ } else {
136
+ items = await this.model.find(query).sort(sortOrder).skip(skip).limit(pageSize);
137
+ }
138
+ onSuccess(res, method, items);
139
+ } catch (error: any) {
140
+ onError(res, method, error);
96
141
  }
97
- onSuccess(res, method, items);
98
- } catch (error: any) {
99
- onError(res, method, error);
100
- }
101
- }));
102
- registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
142
+ }, middleware)
143
+ );
144
+ this.registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
103
145
  }
104
146
 
105
- // Read one
147
+ // READ ONE operation - GET /endpoint/:id
106
148
  if (methods.includes('GET')) {
107
149
  const path = `/${this.endpoint}/:id`;
108
- this.router.get(path, applyMiddleware(async (req, res) => {
109
- const method = 'GET';
110
- try {
111
- let item: T | null;
112
- if (options.relatedModel && options.relatedMethods?.includes('GET')) {
113
- const aggregateResult = await this.model.aggregate([
114
- { $match: { _id: req.params.id } },
115
- {
116
- $lookup: {
117
- from: options.relatedModel.collection.name,
118
- localField: options.relatedField!,
119
- foreignField: '_id',
120
- as: 'relatedData'
150
+ this.router.get(
151
+ path,
152
+ this.applyMiddleware(async (req, res) => {
153
+ const method = 'GET';
154
+ try {
155
+ let item: T | null;
156
+ if (options.relatedModel && options.relatedMethods?.includes('GET')) {
157
+ const aggregateResult = await this.model.aggregate([
158
+ { $match: { _id: req.params.id } },
159
+ {
160
+ $lookup: {
161
+ from: options.relatedModel.collection.name,
162
+ localField: options.relatedField!,
163
+ foreignField: '_id',
164
+ as: 'relatedData'
165
+ }
121
166
  }
122
- }
123
- ]);
124
- item = aggregateResult[0] as T;
125
- } else {
126
- item = await this.model.findById(req.params.id);
167
+ ]);
168
+ item = aggregateResult[0] as T || null;
169
+ } else {
170
+ item = await this.model.findById(req.params.id);
171
+ }
172
+ if (!item) {
173
+ return res.status(404).send({ message: 'Item not found' });
174
+ }
175
+ onSuccess(res, method, item);
176
+ } catch (error: any) {
177
+ onError(res, method, error);
127
178
  }
128
- if (!item) {
129
- return res.status(404).send();
130
- }
131
- onSuccess(res, method, item);
132
- } catch (error: any) {
133
- onError(res, method, error);
134
- }
135
- }));
136
- registerRoute('GET', path, ['id']);
179
+ }, middleware)
180
+ );
181
+ this.registerRoute('GET', path, ['id']);
137
182
  }
138
183
 
139
- // Update
184
+ // UPDATE operation - PUT /endpoint/:id
140
185
  if (methods.includes('PUT')) {
141
186
  const path = `/${this.endpoint}/:id`;
142
- this.router.put(path, applyMiddleware(async (req, res) => {
143
- const method = 'PUT';
144
- try {
145
- const item = await this.model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
146
- if (!item) {
147
- return res.status(404).send();
148
- }
149
- if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
150
- await options.relatedModel.updateMany({ [options.relatedField!]: item._id }, req.body);
187
+ this.router.put(
188
+ path,
189
+ this.applyMiddleware(async (req, res) => {
190
+ const method = 'PUT';
191
+ try {
192
+ const item = await this.model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
193
+ if (!item) {
194
+ return res.status(404).send({ message: 'Item not found' });
195
+ }
196
+ // Update related model entries if configured
197
+ if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
198
+ await options.relatedModel.updateMany({ [options.relatedField!]: item._id }, req.body);
199
+ }
200
+ onSuccess(res, method, item);
201
+ } catch (error: any) {
202
+ onError(res, method, error);
151
203
  }
152
- onSuccess(res, method, item);
153
- } catch (error: any) {
154
- onError(res, method, error);
155
- }
156
- }));
157
- registerRoute('PUT', path, ['id']);
204
+ }, middleware)
205
+ );
206
+ this.registerRoute('PUT', path, ['id']);
158
207
  }
159
208
 
160
- // Delete multiple
209
+ // DELETE MULTIPLE operation - DELETE /endpoint?filter=...
161
210
  if (methods.includes('DELETE')) {
162
211
  const path = `/${this.endpoint}`;
163
- this.router.delete(path, applyMiddleware(async (req, res) => {
164
- const method = 'DELETE';
165
- try {
166
- const query = req.query.filter ? JSON.parse(req.query.filter as string) : {};
167
- const deleteResult: any = await this.model.deleteMany(query);
168
- if (deleteResult.deletedCount === 0) {
169
- return res.status(404).send();
170
- }
171
- if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
172
- await options.relatedModel.deleteMany({ [options.relatedField!]: { $in: query } });
212
+ this.router.delete(
213
+ path,
214
+ this.applyMiddleware(async (req, res) => {
215
+ const method = 'DELETE';
216
+ try {
217
+ const query = req.query.filter ? JSON.parse(req.query.filter as string) : {};
218
+ const deleteResult: any = await this.model.deleteMany(query);
219
+ if (deleteResult.deletedCount === 0) {
220
+ return res.status(404).send({ message: 'No matching items found to delete' });
221
+ }
222
+ if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
223
+ await options.relatedModel.deleteMany({ [options.relatedField!]: { $in: query } });
224
+ }
225
+ onSuccess(res, method, deleteResult);
226
+ } catch (error: any) {
227
+ onError(res, method, error);
173
228
  }
174
- onSuccess(res, method, deleteResult);
175
- } catch (error: any) {
176
- onError(res, method, error);
177
- }
178
- }));
179
- registerRoute('DELETE', path, ['filter']);
229
+ }, middleware)
230
+ );
231
+ this.registerRoute('DELETE', path, ['filter']);
180
232
  }
181
233
 
182
- // Delete one
234
+ // DELETE ONE operation - DELETE /endpoint/:id
183
235
  if (methods.includes('DELETE')) {
184
236
  const path = `/${this.endpoint}/:id`;
185
- this.router.delete(path, applyMiddleware(async (req, res) => {
186
- const method = 'DELETE';
187
- try {
188
- const item = await this.model.findByIdAndDelete(req.params.id);
189
- if (!item) {
190
- return res.status(404).send();
191
- }
192
- if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
193
- await options.relatedModel.deleteMany({ [options.relatedField!]: item._id });
237
+ this.router.delete(
238
+ path,
239
+ this.applyMiddleware(async (req, res) => {
240
+ const method = 'DELETE';
241
+ try {
242
+ const item = await this.model.findByIdAndDelete(req.params.id);
243
+ if (!item) {
244
+ return res.status(404).send({ message: 'Item not found' });
245
+ }
246
+ if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
247
+ await options.relatedModel.deleteMany({ [options.relatedField!]: item._id });
248
+ }
249
+ onSuccess(res, method, item);
250
+ } catch (error: any) {
251
+ onError(res, method, error);
194
252
  }
195
- onSuccess(res, method, item);
196
- } catch (error: any) {
197
- onError(res, method, error);
198
- }
199
- }));
200
- registerRoute('DELETE', path, ['id']);
253
+ }, middleware)
254
+ );
255
+ this.registerRoute('DELETE', path, ['id']);
201
256
  }
202
257
 
203
- // Aggregate
258
+ // AGGREGATE operation - GET /endpoint/aggregate
204
259
  if (methods.includes('GET') && options.aggregatePipeline) {
205
260
  const path = `/${this.endpoint}/aggregate`;
206
- this.router.get(path, applyMiddleware(async (req, res) => {
207
- const method = 'GET (Aggregate)';
208
- try {
209
- const pipeline: any = options.aggregatePipeline ?? [];
210
- const results = await this.model.aggregate(pipeline);
211
- onSuccess(res, method, results);
212
- } catch (error: any) {
213
- onError(res, method, error);
214
- }
215
- }));
216
- registerRoute('GET', path);
261
+ this.router.get(
262
+ path,
263
+ this.applyMiddleware(async (req, res) => {
264
+ const method = 'GET (Aggregate)';
265
+ try {
266
+ const pipeline: any[] = options.aggregatePipeline || [];
267
+ const results = await this.model.aggregate(pipeline);
268
+ onSuccess(res, method, results);
269
+ } catch (error: any) {
270
+ onError(res, method, error);
271
+ }
272
+ }, middleware)
273
+ );
274
+ this.registerRoute('GET', path);
217
275
  }
218
276
 
219
- // Custom routes
277
+ // CUSTOM ROUTES
220
278
  if (options.customRoutes) {
221
279
  options.customRoutes.forEach(route => {
222
280
  const { method, path, handler } = route;
223
281
  if (methods.includes(method.toUpperCase() as 'POST' | 'GET' | 'PUT' | 'DELETE')) {
224
- this.router[method](`/${this.endpoint}${path}`, applyMiddleware(handler));
225
- registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
282
+ // Prepend base endpoint to custom route path
283
+ this.router[method](`/${this.endpoint}${path}`, this.applyMiddleware(handler, middleware));
284
+ this.registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
226
285
  }
227
286
  });
228
287
  }
229
288
  }
230
289
 
290
+ /**
291
+ * Returns the configured Express router.
292
+ */
231
293
  public getRouter(): Router {
232
294
  return this.router;
233
295
  }
234
296
 
235
- public getRoutes(): { method: string, path: string, params?: string[] }[] {
297
+ /**
298
+ * Returns an array of registered route definitions.
299
+ */
300
+ public getRoutes(): { method: string; path: string; params?: string[] }[] {
236
301
  return this.routes;
237
302
  }
238
303
  }