@automate-crud/mongoose 0.1.0

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/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @automate-crud/mongoose
2
+
3
+ Auto-generate CRUD APIs for Express using a Mongoose model.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @automate-crud/mongoose express mongoose
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import express from "express";
15
+ import mongoose from "mongoose";
16
+ import { createCrudRouter, crudErrorHandler } from "@automate-crud/mongoose";
17
+
18
+ await mongoose.connect(process.env.MONGO_URI);
19
+
20
+ const User = mongoose.model(
21
+ "User",
22
+ new mongoose.Schema({ name: String, email: String }, { timestamps: true })
23
+ );
24
+
25
+ const app = express();
26
+ app.use(express.json());
27
+
28
+ app.use(
29
+ "/users",
30
+ createCrudRouter({
31
+ model: User,
32
+ searchFields: ["name", "email"],
33
+ allowedIncludes: []
34
+ })
35
+ );
36
+
37
+ app.use(crudErrorHandler);
38
+ app.listen(4000);
39
+ ```
40
+
41
+ ## Generated Routes
42
+
43
+ - `GET /users`
44
+ - `GET /users/:id`
45
+ - `POST /users`
46
+ - `PATCH /users/:id`
47
+ - `DELETE /users/:id`
48
+
49
+ ## List Query Params
50
+
51
+ - `page`, `limit`
52
+ - `sort`
53
+ - `select`
54
+ - `q` (search)
55
+ - `include`
56
+ - `filter` (JSON string)
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@automate-crud/mongoose",
3
+ "version": "0.1.0",
4
+ "description": "Auto CRUD for Express + Mongoose",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "peerDependencies": {
9
+ "express": "^4.18.0 || ^5.0.0",
10
+ "mongoose": "^8.0.0"
11
+ },
12
+ "devDependencies": {
13
+ "supertest": "^7.1.0",
14
+ "mongodb-memory-server": "^10.1.0"
15
+ },
16
+ "scripts": {
17
+ "test": "node test/run-tests.js"
18
+ }
19
+ }
@@ -0,0 +1,189 @@
1
+ import express from 'express';
2
+
3
+ function parseListQuery(query = {}) {
4
+ const page = Math.max(1, Number(query.page) || 1);
5
+ const limit = Math.min(100, Math.max(1, Math.max(1, Number(query.limit) || 20)));
6
+ const skip = (page - 1) * limit;
7
+
8
+ return {
9
+ page,
10
+ limit,
11
+ skip,
12
+ sort: query.sort || "-createdAt",
13
+ select: query.select || "",
14
+ q: query.q || "",
15
+ include: query.include || ""
16
+ };
17
+ }
18
+
19
+ function normalizeInclude(include) {
20
+ if (!include) return [];
21
+
22
+ return String(include).split(",").map((v) => v.trim()).filter(Boolean);
23
+ }
24
+
25
+ function pickAllowedIncludes(includes, allowedIncludes = []) {
26
+ if (!allowedIncludes.length) return includes;
27
+ const allowed = new Set(allowedIncludes);
28
+ return includes.filter((i) => allowed.has(i))
29
+ }
30
+
31
+ function parseFilter(query = {}) {
32
+ const reserved = new Set(["page", "limit", "sort", "select", "q", "include", "filter"]);
33
+
34
+ const fromQuery = {};
35
+
36
+ for (const [key, value] of Object.entries(query)) {
37
+ if (reserved.has(key)) continue;
38
+ fromQuery[key] = value;
39
+ }
40
+
41
+ if (!query.filter) return fromQuery;
42
+
43
+ try {
44
+ const parsed = JSON.parse(query.filter);
45
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
46
+ return { ...fromQuery, ...parsed };
47
+ }
48
+ } catch (error) {
49
+ // ignore invalid filter JSON
50
+ }
51
+
52
+ return fromQuery;
53
+ }
54
+
55
+ function applySearch(filter, q, searchFields = []) {
56
+ if (!q || !searchFields.length) return filter;
57
+
58
+ const regex = new RegExp(q, "i");
59
+
60
+ return {
61
+ ...filter,
62
+ $or: searchFields.map((field) => ({ [field]: regex }))
63
+ };
64
+ }
65
+
66
+ export function createCrudRouter({
67
+ model,
68
+ searchFields = [],
69
+ allowedIncludes = [],
70
+ idParam = "id"
71
+ }) {
72
+ if (!model) throw new Error("createCrudRouter requires a Mongoose model");
73
+
74
+ const router = express.Router();
75
+ const idPath = `/:${idParam}`;
76
+
77
+ // LIST
78
+ router.get("/", async (req, res, next) => {
79
+ try {
80
+ const listQuery = parseListQuery(req.query);
81
+ const filter = applySearch(parseFilter(req.query), listQuery.q, searchFields);
82
+
83
+ let query = model.find(filter);
84
+
85
+ if (listQuery.select) query = query.select(listQuery.select);
86
+ if (listQuery.sort) query = query.sort(listQuery.sort);
87
+
88
+ const includes = pickAllowedIncludes(
89
+ normalizeInclude(listQuery.include),
90
+ allowedIncludes
91
+ );
92
+
93
+ for (const path of includes) {
94
+ query = query.populate(path);
95
+ }
96
+
97
+ const [items, total] = await Promise.all([
98
+ query.skip(listQuery.skip).limit(listQuery.limit).lean().exec(),
99
+ model.countDocuments(filter)
100
+ ]);
101
+
102
+ res.json({
103
+ data: items,
104
+ meta: {
105
+ total,
106
+ page: listQuery.page,
107
+ limit: listQuery.limit,
108
+ pages: Math.max(1, Math.ceil(total / listQuery.limit))
109
+ }
110
+ });
111
+ } catch (error) {
112
+ next(error);
113
+ }
114
+ });
115
+
116
+ // GET ONE
117
+ router.get(idPath, async (req, res, next) => {
118
+ try {
119
+ let query = model.findById(req.params[idParam]);
120
+
121
+ if (req.query.select) query = query.select(String(req.query.select));
122
+
123
+ const includes = pickAllowedIncludes(
124
+ normalizeInclude(req.query.include),
125
+ allowedIncludes
126
+ );
127
+
128
+ for (const path of includes) {
129
+ query = query.populate(path);
130
+ }
131
+
132
+ const doc = await query.lean().exec();
133
+
134
+ if (!doc) {
135
+ return res.status(404).json({ error: { message: "Not found" } });
136
+ }
137
+
138
+ res.json({ data: doc });
139
+
140
+ } catch (error) {
141
+ next(error);
142
+ }
143
+ });
144
+
145
+ // CREATE
146
+ router.post("/", async (req, res, next) => {
147
+ try {
148
+ const created = await model.create(req.body);
149
+ res.status(201).json({ data: created.toObject() });
150
+ } catch (error) {
151
+ next(error);
152
+ }
153
+ });
154
+
155
+ // UPDATE
156
+ router.patch(idPath, async (req, res, next) => {
157
+ try {
158
+ const updated = await model.findByIdAndUpdate(req.params[idParam], req.body, {
159
+ new: true,
160
+ runValidators: true
161
+ }).lean().exec()
162
+
163
+ if (!updated) {
164
+ return res.status(404).json({ error: { message: "Not found" } });
165
+ }
166
+
167
+ res.json({ data: updated });
168
+ } catch (error) {
169
+ next(error);
170
+ }
171
+ });
172
+
173
+ // DELETE
174
+ router.delete(idPath, async (req, res, next) => {
175
+ try {
176
+ const deleted = await model.findByIdAndDelete(req.params[idParam]).lean().exec();
177
+
178
+ if (!deleted) {
179
+ return res.status(404).json({ error: { message: "Not found" } });
180
+ }
181
+
182
+ res.json({ data: deleted });
183
+ } catch (error) {
184
+ next(error);
185
+ }
186
+ });
187
+
188
+ return router;
189
+ }
@@ -0,0 +1,8 @@
1
+ export function crudErrorHandler(err, req, res, next) {
2
+ const status = err.status || 500;
3
+ res.status(status).json({
4
+ error: {
5
+ message: err.message || "Internal server error"
6
+ }
7
+ });
8
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createCrudRouter } from "./createCrudRouter.js";
2
+ export { crudErrorHandler } from './crudErrorHandler.js'
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { createCrudRouter } from "../src/index.js";
3
+
4
+ function makeFindChain(result = []) {
5
+ const chain = {
6
+ select: () => chain,
7
+ sort: () => chain,
8
+ populate: () => chain,
9
+ skip: () => chain,
10
+ limit: () => chain,
11
+ lean: () => chain,
12
+ exec: async () => result
13
+ };
14
+ return chain;
15
+ }
16
+
17
+ function makeFindOneChain(result = null) {
18
+ const chain = {
19
+ select: () => chain,
20
+ populate: () => chain,
21
+ lean: () => chain,
22
+ exec: async () => result
23
+ };
24
+ return chain;
25
+ }
26
+
27
+ function makeUpdateDeleteChain(result = null) {
28
+ const chain = {
29
+ lean: () => chain,
30
+ exec: async () => result
31
+ };
32
+ return chain;
33
+ }
34
+
35
+ function testThrowsWithoutModel() {
36
+ assert.throws(() => createCrudRouter({}), /requires a Mongoose model/);
37
+ }
38
+
39
+ function testReturnsRouter() {
40
+ const fakeModel = {
41
+ find: () => makeFindChain([]),
42
+ findById: () => makeFindOneChain(null),
43
+ create: async (body) => ({ toObject: () => body }),
44
+ findByIdAndUpdate: () => makeUpdateDeleteChain(null),
45
+ findByIdAndDelete: () => makeUpdateDeleteChain(null),
46
+ countDocuments: async () => 0
47
+ };
48
+
49
+ const router = createCrudRouter({ model: fakeModel });
50
+ assert.equal(typeof router, "function");
51
+ assert.equal(typeof router.handle, "function");
52
+ }
53
+
54
+ testThrowsWithoutModel();
55
+ testReturnsRouter();
56
+ console.log("mongoose tests: ok");
package/test/smoke.js ADDED
@@ -0,0 +1,33 @@
1
+ import express from 'express';
2
+ import mongoose from 'mongoose';
3
+ import { createCrudRouter } from '../src/index.js';
4
+
5
+ const app = express();
6
+ app.use(express.json());
7
+
8
+ await mongoose.connect(process.env.MONGO_URI);
9
+
10
+ const User = mongoose.model("User", new mongoose.Schema(
11
+ {
12
+ name: { type: String, required: true, index: true },
13
+ email: { type: String, required: true, unique: true, index: true }
14
+ },
15
+ { timestamps: true }
16
+ ));
17
+
18
+ app.use(
19
+ "/users",
20
+ createCrudRouter({
21
+ model: User,
22
+ searchFields: ["name", "email"],
23
+ allowedIncludes: []
24
+ })
25
+ )
26
+
27
+ app.use((err, req, res, next) => {
28
+ res.status(500).json({ error: { message: err.message || "Internal error" } });
29
+ });
30
+
31
+ app.listen(4000, () => {
32
+ console.log("Running at http://localhost:4000");
33
+ });