offbyt 1.0.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 +2 -0
- package/cli/index.js +2 -0
- package/cli.js +206 -0
- package/core/detector/detectAxios.js +107 -0
- package/core/detector/detectFetch.js +148 -0
- package/core/detector/detectForms.js +55 -0
- package/core/detector/detectSocket.js +341 -0
- package/core/generator/generateControllers.js +17 -0
- package/core/generator/generateModels.js +25 -0
- package/core/generator/generateRoutes.js +17 -0
- package/core/generator/generateServer.js +18 -0
- package/core/generator/generateSocket.js +160 -0
- package/core/index.js +14 -0
- package/core/ir/IRTypes.js +25 -0
- package/core/ir/buildIR.js +83 -0
- package/core/parser/parseJS.js +26 -0
- package/core/parser/parseTS.js +27 -0
- package/core/rules/relationRules.js +38 -0
- package/core/rules/resourceRules.js +32 -0
- package/core/rules/schemaInference.js +26 -0
- package/core/scanner/scanProject.js +58 -0
- package/deploy/cloudflare.js +41 -0
- package/deploy/cloudflareWorker.js +122 -0
- package/deploy/connect.js +198 -0
- package/deploy/flyio.js +51 -0
- package/deploy/index.js +322 -0
- package/deploy/netlify.js +29 -0
- package/deploy/railway.js +215 -0
- package/deploy/render.js +195 -0
- package/deploy/utils.js +383 -0
- package/deploy/vercel.js +29 -0
- package/index.js +18 -0
- package/lib/generator/advancedCrudGenerator.js +475 -0
- package/lib/generator/crudCodeGenerator.js +486 -0
- package/lib/generator/irBasedGenerator.js +360 -0
- package/lib/ir-builder/index.js +16 -0
- package/lib/ir-builder/irBuilder.js +330 -0
- package/lib/ir-builder/rulesEngine.js +353 -0
- package/lib/ir-builder/templateEngine.js +193 -0
- package/lib/ir-builder/templates/index.js +14 -0
- package/lib/ir-builder/templates/model.template.js +47 -0
- package/lib/ir-builder/templates/routes-generic.template.js +66 -0
- package/lib/ir-builder/templates/routes-user.template.js +105 -0
- package/lib/ir-builder/templates/routes.template.js +102 -0
- package/lib/ir-builder/templates/validation.template.js +15 -0
- package/lib/ir-integration.js +349 -0
- package/lib/modes/benchmark.js +162 -0
- package/lib/modes/configBasedGenerator.js +2258 -0
- package/lib/modes/connect.js +1125 -0
- package/lib/modes/doctorAi.js +172 -0
- package/lib/modes/generateApi.js +435 -0
- package/lib/modes/interactiveSetup.js +548 -0
- package/lib/modes/offline.clean.js +14 -0
- package/lib/modes/offline.enhanced.js +787 -0
- package/lib/modes/offline.js +295 -0
- package/lib/modes/offline.v2.js +13 -0
- package/lib/modes/sync.js +629 -0
- package/lib/scanner/apiEndpointExtractor.js +387 -0
- package/lib/scanner/authPatternDetector.js +54 -0
- package/lib/scanner/frontendScanner.js +642 -0
- package/lib/utils/apiClientGenerator.js +242 -0
- package/lib/utils/apiScanner.js +95 -0
- package/lib/utils/codeInjector.js +350 -0
- package/lib/utils/doctor.js +381 -0
- package/lib/utils/envGenerator.js +36 -0
- package/lib/utils/loadTester.js +61 -0
- package/lib/utils/performanceAnalyzer.js +298 -0
- package/lib/utils/resourceDetector.js +281 -0
- package/package.json +20 -0
- package/templates/.env.template +31 -0
- package/templates/advanced.model.template.js +201 -0
- package/templates/advanced.route.template.js +341 -0
- package/templates/auth.middleware.template.js +87 -0
- package/templates/auth.routes.template.js +238 -0
- package/templates/auth.user.model.template.js +78 -0
- package/templates/cache.middleware.js +34 -0
- package/templates/chat.models.template.js +260 -0
- package/templates/chat.routes.template.js +478 -0
- package/templates/compression.middleware.js +19 -0
- package/templates/database.config.js +74 -0
- package/templates/errorHandler.middleware.js +54 -0
- package/templates/express/controller.ejs +26 -0
- package/templates/express/model.ejs +9 -0
- package/templates/express/route.ejs +18 -0
- package/templates/express/server.ejs +16 -0
- package/templates/frontend.env.template +14 -0
- package/templates/model.template.js +86 -0
- package/templates/package.production.json +51 -0
- package/templates/package.template.json +41 -0
- package/templates/pagination.utility.js +110 -0
- package/templates/production.server.template.js +233 -0
- package/templates/rateLimiter.middleware.js +36 -0
- package/templates/requestLogger.middleware.js +19 -0
- package/templates/response.helper.js +179 -0
- package/templates/route.template.js +130 -0
- package/templates/security.middleware.js +78 -0
- package/templates/server.template.js +91 -0
- package/templates/socket.server.template.js +433 -0
- package/templates/utils.helper.js +157 -0
- package/templates/validation.middleware.js +63 -0
- package/templates/validation.schema.js +128 -0
- package/utils/fileWriter.js +15 -0
- package/utils/logger.js +18 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced CRUD Code Generator
|
|
3
|
+
* Generates production-ready CRUD operations with:
|
|
4
|
+
* - Pagination, filtering, sorting, search
|
|
5
|
+
* - Bulk operations
|
|
6
|
+
* - Advanced validation
|
|
7
|
+
* - Proper error handling
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function generateAdvancedCrudModel(resourceName, fields = [], hasAuth = false, relations = []) {
|
|
11
|
+
const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).replace(/s$/, '');
|
|
12
|
+
const collectionName = resourceName.toLowerCase();
|
|
13
|
+
|
|
14
|
+
// Debug logging
|
|
15
|
+
console.log(`\n[DEBUG] Generating model for: ${resourceName}`);
|
|
16
|
+
console.log(`[DEBUG] Model name: ${modelName}`);
|
|
17
|
+
console.log(`[DEBUG] Fields: ${JSON.stringify(fields)}`);
|
|
18
|
+
console.log(`[DEBUG] Relations: ${JSON.stringify(relations)}`);
|
|
19
|
+
|
|
20
|
+
const fieldDefinitions = generateAdvancedFieldDefinitions(fields, relations);
|
|
21
|
+
console.log(`[DEBUG] Field definitions generated (length: ${fieldDefinitions.length} chars)`);
|
|
22
|
+
|
|
23
|
+
const indexes = generateIndexes(fields, modelName);
|
|
24
|
+
|
|
25
|
+
return `import mongoose from 'mongoose';
|
|
26
|
+
|
|
27
|
+
const ${modelName}Schema = new mongoose.Schema(
|
|
28
|
+
{
|
|
29
|
+
${fieldDefinitions},
|
|
30
|
+
// Metadata
|
|
31
|
+
isActive: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: true,
|
|
34
|
+
index: true
|
|
35
|
+
},
|
|
36
|
+
isDeleted: {
|
|
37
|
+
type: Boolean,
|
|
38
|
+
default: false,
|
|
39
|
+
index: true
|
|
40
|
+
},
|
|
41
|
+
metadata: {
|
|
42
|
+
createdBy: mongoose.Schema.Types.ObjectId,
|
|
43
|
+
updatedBy: mongoose.Schema.Types.ObjectId,
|
|
44
|
+
version: { type: Number, default: 1 },
|
|
45
|
+
tags: [String]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
timestamps: true,
|
|
50
|
+
collection: '${collectionName}'
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// INDEXES FOR PERFORMANCE
|
|
56
|
+
// ============================================================
|
|
57
|
+
${indexes}
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// HOOKS
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
${modelName}Schema.pre('save', function(next) {
|
|
64
|
+
if (this.isModified()) {
|
|
65
|
+
this.metadata.version = (this.metadata.version || 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ============================================================
|
|
71
|
+
// QUERY HELPERS
|
|
72
|
+
// ============================================================
|
|
73
|
+
|
|
74
|
+
${modelName}Schema.query.active = function() {
|
|
75
|
+
return this.find({ isActive: true, isDeleted: false });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ============================================================
|
|
79
|
+
// STATIC METHODS
|
|
80
|
+
// ============================================================
|
|
81
|
+
|
|
82
|
+
${modelName}Schema.statics.findAllActive = async function(options = {}) {
|
|
83
|
+
return this.find({ isActive: true, isDeleted: false })
|
|
84
|
+
.sort(options.sort || { createdAt: -1 })
|
|
85
|
+
.limit(options.limit || 100)
|
|
86
|
+
.skip(options.skip || 0);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
${modelName}Schema.statics.softDelete = async function(id) {
|
|
90
|
+
return this.findByIdAndUpdate(
|
|
91
|
+
id,
|
|
92
|
+
{ isDeleted: true },
|
|
93
|
+
{ new: true }
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// INSTANCE METHODS
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
${modelName}Schema.methods.toJSON = function() {
|
|
102
|
+
const obj = this.toObject();
|
|
103
|
+
delete obj.__v;
|
|
104
|
+
return obj;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const ${modelName} = mongoose.model('${modelName}', ${modelName}Schema);
|
|
108
|
+
|
|
109
|
+
export default ${modelName};
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function generateAdvancedFieldDefinitions(fields, relations = []) {
|
|
114
|
+
const fieldDefs = [];
|
|
115
|
+
const relationMap = new Map(relations.map(r => [r.field, r]));
|
|
116
|
+
|
|
117
|
+
for (const field of fields) {
|
|
118
|
+
// Skip MongoDB's automatic _id field and id variations
|
|
119
|
+
if (field === '_id' || field === 'id' || field.toLowerCase() === 'id') {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const normalized = field.toLowerCase();
|
|
124
|
+
const relation = relationMap.get(field);
|
|
125
|
+
|
|
126
|
+
let fieldDef = '';
|
|
127
|
+
|
|
128
|
+
// Check if this field has a defined relation
|
|
129
|
+
if (relation) {
|
|
130
|
+
fieldDef = ` ${field}: { type: mongoose.Schema.Types.ObjectId, ref: '${relation.ref}', required: ${relation.required || false} }`;
|
|
131
|
+
}
|
|
132
|
+
// Email
|
|
133
|
+
else if (normalized.includes('email')) {
|
|
134
|
+
fieldDef = ` ${field}: { type: String, match: /.+@.+\\..+/, unique: true, sparse: true, lowercase: true, trim: true }`;
|
|
135
|
+
}
|
|
136
|
+
// Price/Amount/Cost
|
|
137
|
+
else if (normalized.match(/^(price|cost|amount|total|revenue|salary)$/)) {
|
|
138
|
+
fieldDef = ` ${field}: { type: Number, min: 0, default: 0 }`;
|
|
139
|
+
}
|
|
140
|
+
// Rating/Score
|
|
141
|
+
else if (normalized.match(/^(rating|score|points?)$/)) {
|
|
142
|
+
fieldDef = ` ${field}: { type: Number, min: 0, max: 100, default: 0 }`;
|
|
143
|
+
}
|
|
144
|
+
// Status
|
|
145
|
+
else if (normalized === 'status') {
|
|
146
|
+
fieldDef = ` ${field}: { type: String, enum: ['active', 'inactive', 'pending', 'archived', 'draft'], default: 'active' }`;
|
|
147
|
+
}
|
|
148
|
+
// Count/Views/Likes
|
|
149
|
+
else if (normalized.match(/^(count|views|likes|clicks|impressions)$/)) {
|
|
150
|
+
fieldDef = ` ${field}: { type: Number, default: 0, min: 0 }`;
|
|
151
|
+
}
|
|
152
|
+
// Boolean flags
|
|
153
|
+
else if (normalized.match(/^(is_|active|published|completed|verified|confirmed)/) ||
|
|
154
|
+
normalized.match(/(active|published|completed|verified|confirmed)$/i)) {
|
|
155
|
+
fieldDef = ` ${field}: { type: Boolean, default: false, index: true }`;
|
|
156
|
+
}
|
|
157
|
+
// Date fields
|
|
158
|
+
else if (normalized.match(/date|time|at$/)) {
|
|
159
|
+
fieldDef = ` ${field}: { type: Date, default: Date.now }`;
|
|
160
|
+
}
|
|
161
|
+
// Phone
|
|
162
|
+
else if (normalized.includes('phone')) {
|
|
163
|
+
fieldDef = ` ${field}: { type: String, match: /^\\+?[\\d\\s\\-()]+$/ }`;
|
|
164
|
+
}
|
|
165
|
+
// Arrays
|
|
166
|
+
else if (normalized.endsWith('s') && !normalized.endsWith('ss')) {
|
|
167
|
+
fieldDef = ` ${field}: [{ type: String }]`;
|
|
168
|
+
}
|
|
169
|
+
// Default to string
|
|
170
|
+
else {
|
|
171
|
+
fieldDef = ` ${field}: { type: String, trim: true }`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fieldDefs.push(fieldDef);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return fieldDefs.length > 0 ? fieldDefs.join(',\n') : ' // Fields defined';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function generateIndexes(fields, modelName) {
|
|
181
|
+
const indexes = [
|
|
182
|
+
`${modelName}Schema.index({ createdAt: -1 });`,
|
|
183
|
+
`${modelName}Schema.index({ updatedAt: -1 });`,
|
|
184
|
+
`${modelName}Schema.index({ isActive: 1 });`,
|
|
185
|
+
`${modelName}Schema.index({ isDeleted: 1, createdAt: -1 });`
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// Add indexes for common query fields
|
|
189
|
+
for (const field of fields) {
|
|
190
|
+
const normalized = field.toLowerCase();
|
|
191
|
+
if (normalized.match(/^(status|type|category|user|owner)$/)) {
|
|
192
|
+
indexes.push(`${modelName}Schema.index({ ${field}: 1, createdAt: -1 });`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return indexes.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate Advanced Routes with Pagination, Filtering, Sorting
|
|
201
|
+
*/
|
|
202
|
+
export function generateAdvancedCrudRoutes(resourceName, fields = [], hasAuth = false, relations = [], actions = []) {
|
|
203
|
+
const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).replace(/s$/, '');
|
|
204
|
+
const searchFields = fields.filter(f =>
|
|
205
|
+
!f.toLowerCase().match(/^(id|createdby|lastupdated)$/)
|
|
206
|
+
).slice(0, 3).join("', '");
|
|
207
|
+
|
|
208
|
+
const filterFields = fields.filter(f =>
|
|
209
|
+
f.toLowerCase().match(/^(status|type|category|isactive)$/)
|
|
210
|
+
).map(f => `'${f}'`).join(', ');
|
|
211
|
+
|
|
212
|
+
// Debug logging
|
|
213
|
+
console.log(`\n[DEBUG] Generating routes for: ${resourceName}`);
|
|
214
|
+
console.log(`[DEBUG] Model name: ${modelName}`);
|
|
215
|
+
console.log(`[DEBUG] Fields: ${JSON.stringify(fields)}`);
|
|
216
|
+
console.log(`[DEBUG] Search fields string: "${searchFields}"`);
|
|
217
|
+
console.log(`[DEBUG] Filter fields string: "${filterFields}"`);
|
|
218
|
+
|
|
219
|
+
const validators = generateValidators(fields);
|
|
220
|
+
const updateValidators = generateUpdateValidators(fields);
|
|
221
|
+
|
|
222
|
+
return `import express from 'express';
|
|
223
|
+
import { query, body, param } from 'express-validator';
|
|
224
|
+
import { validateErrors } from '../middleware/validation.js';
|
|
225
|
+
import { PaginationHelper } from '../utils/pagination.js';
|
|
226
|
+
import { ResponseHelper } from '../utils/helper.js';
|
|
227
|
+
import ${modelName} from '../models/${modelName}.js';
|
|
228
|
+
|
|
229
|
+
const router = express.Router();
|
|
230
|
+
|
|
231
|
+
// ============================================================
|
|
232
|
+
// GET ALL - WITH PAGINATION, FILTERING, SORTING, SEARCH
|
|
233
|
+
// ============================================================
|
|
234
|
+
router.get(
|
|
235
|
+
'/',
|
|
236
|
+
[
|
|
237
|
+
query('page').optional().isInt({ min: 1 }).toInt(),
|
|
238
|
+
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
|
|
239
|
+
query('sort').optional().isString(),
|
|
240
|
+
query('search').optional().isString().trim(),
|
|
241
|
+
validateErrors
|
|
242
|
+
],
|
|
243
|
+
async (req, res, next) => {
|
|
244
|
+
try {
|
|
245
|
+
const { page = 1, limit = 10, skip = 0 } = PaginationHelper.getPaginationParams(req);
|
|
246
|
+
const sort = PaginationHelper.getSortObject(req.query.sort);
|
|
247
|
+
|
|
248
|
+
let query = {};
|
|
249
|
+
|
|
250
|
+
// Apply search filter
|
|
251
|
+
if (req.query.search) {
|
|
252
|
+
query = PaginationHelper.buildSearchQuery(req.query.search, ['${searchFields}']);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Apply additional filters
|
|
256
|
+
const filters = PaginationHelper.buildFilterQuery(req, [${filterFields}]);
|
|
257
|
+
query = { ...query, ...filters };
|
|
258
|
+
|
|
259
|
+
// Exclude deleted items by default
|
|
260
|
+
query.isDeleted = false;
|
|
261
|
+
|
|
262
|
+
// Execute paginated query
|
|
263
|
+
const result = await PaginationHelper.paginate(${modelName}, query, { page, limit, skip, sort });
|
|
264
|
+
|
|
265
|
+
return ResponseHelper.paginated(res, result.data, result.pagination, '${resourceName} loaded successfully');
|
|
266
|
+
} catch (error) {
|
|
267
|
+
next(error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// ============================================================
|
|
273
|
+
// GET BY ID
|
|
274
|
+
// ============================================================
|
|
275
|
+
router.get(
|
|
276
|
+
'/:id',
|
|
277
|
+
[param('id').isMongoId(), validateErrors],
|
|
278
|
+
async (req, res, next) => {
|
|
279
|
+
try {
|
|
280
|
+
const item = await ${modelName}.findOne({ _id: req.params.id, isDeleted: false });
|
|
281
|
+
|
|
282
|
+
if (!item) {
|
|
283
|
+
return ResponseHelper.notFound(res, '${modelName}');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return ResponseHelper.success(res, item, 'Item retrieved successfully');
|
|
287
|
+
} catch (error) {
|
|
288
|
+
next(error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// ============================================================
|
|
294
|
+
// CREATE
|
|
295
|
+
// ============================================================
|
|
296
|
+
router.post(
|
|
297
|
+
'/',
|
|
298
|
+
[${validators ? validators + ', ' : ''}validateErrors],
|
|
299
|
+
async (req, res, next) => {
|
|
300
|
+
try {
|
|
301
|
+
const newItem = new ${modelName}(req.body);
|
|
302
|
+
const saved = await newItem.save();
|
|
303
|
+
|
|
304
|
+
return ResponseHelper.success(res, saved, '${modelName} created successfully', 201);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error.name === 'ValidationError') {
|
|
307
|
+
const errors = Object.values(error.errors).map(e => e.message);
|
|
308
|
+
return ResponseHelper.validationError(res, errors);
|
|
309
|
+
}
|
|
310
|
+
if (error.code === 11000) {
|
|
311
|
+
return ResponseHelper.error(res, 'Duplicate value for unique field', [], 409);
|
|
312
|
+
}
|
|
313
|
+
next(error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// ============================================================
|
|
319
|
+
// UPDATE (PUT - Full)
|
|
320
|
+
// ============================================================
|
|
321
|
+
router.put(
|
|
322
|
+
'/:id',
|
|
323
|
+
[
|
|
324
|
+
param('id').isMongoId(),
|
|
325
|
+
${updateValidators ? updateValidators + ',' : ''}
|
|
326
|
+
validateErrors
|
|
327
|
+
],
|
|
328
|
+
async (req, res, next) => {
|
|
329
|
+
try {
|
|
330
|
+
const updated = await ${modelName}.findByIdAndUpdate(
|
|
331
|
+
req.params.id,
|
|
332
|
+
req.body,
|
|
333
|
+
{ new: true, runValidators: true }
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (!updated) {
|
|
337
|
+
return ResponseHelper.notFound(res, '${modelName}');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return ResponseHelper.success(res, updated, '${modelName} updated successfully');
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error.name === 'ValidationError') {
|
|
343
|
+
const errors = Object.values(error.errors).map(e => e.message);
|
|
344
|
+
return ResponseHelper.validationError(res, errors);
|
|
345
|
+
}
|
|
346
|
+
next(error);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// ============================================================
|
|
352
|
+
// PARTIAL UPDATE (PATCH)
|
|
353
|
+
// ============================================================
|
|
354
|
+
router.patch(
|
|
355
|
+
'/:id',
|
|
356
|
+
[param('id').isMongoId(), validateErrors],
|
|
357
|
+
async (req, res, next) => {
|
|
358
|
+
try {
|
|
359
|
+
// Don't allow ID changes
|
|
360
|
+
delete req.body._id;
|
|
361
|
+
delete req.body.createdAt;
|
|
362
|
+
|
|
363
|
+
const updated = await ${modelName}.findByIdAndUpdate(
|
|
364
|
+
req.params.id,
|
|
365
|
+
{ \$set: req.body },
|
|
366
|
+
{ new: true, runValidators: true }
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (!updated) {
|
|
370
|
+
return ResponseHelper.notFound(res, '${modelName}');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return ResponseHelper.success(res, updated, 'Partial update successful');
|
|
374
|
+
} catch (error) {
|
|
375
|
+
next(error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// ============================================================
|
|
381
|
+
// DELETE (Soft Delete)
|
|
382
|
+
// ============================================================
|
|
383
|
+
router.delete(
|
|
384
|
+
'/:id',
|
|
385
|
+
[param('id').isMongoId(), validateErrors],
|
|
386
|
+
async (req, res, next) => {
|
|
387
|
+
try {
|
|
388
|
+
const deleted = await ${modelName}.softDelete(req.params.id);
|
|
389
|
+
|
|
390
|
+
if (!deleted) {
|
|
391
|
+
return ResponseHelper.notFound(res, '${modelName}');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return ResponseHelper.success(res, deleted, '${modelName} deleted successfully');
|
|
395
|
+
} catch (error) {
|
|
396
|
+
next(error);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// ============================================================
|
|
402
|
+
// BULK OPERATIONS
|
|
403
|
+
// ============================================================
|
|
404
|
+
|
|
405
|
+
// Bulk Create
|
|
406
|
+
router.post(
|
|
407
|
+
'/bulk/create',
|
|
408
|
+
[body('items').isArray({ min: 1 }), validateErrors],
|
|
409
|
+
async (req, res, next) => {
|
|
410
|
+
try {
|
|
411
|
+
const items = await ${modelName}.insertMany(req.body.items);
|
|
412
|
+
return ResponseHelper.success(res, items, \`Created \${items.length} items\`, 201);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
next(error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Bulk Delete
|
|
420
|
+
router.delete(
|
|
421
|
+
'/bulk/delete',
|
|
422
|
+
[body('ids').isArray({ min: 1 }), validateErrors],
|
|
423
|
+
async (req, res, next) => {
|
|
424
|
+
try {
|
|
425
|
+
const result = await ${modelName}.deleteMany({ _id: { $in: req.body.ids } });
|
|
426
|
+
return ResponseHelper.success(res, result, \`Deleted \${result.deletedCount} items\`);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
next(error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// ============================================================
|
|
434
|
+
// STATISTICS
|
|
435
|
+
// ============================================================
|
|
436
|
+
router.get(
|
|
437
|
+
'/stats/summary',
|
|
438
|
+
async (req, res, next) => {
|
|
439
|
+
try {
|
|
440
|
+
const total = await ${modelName}.countDocuments({ isDeleted: false });
|
|
441
|
+
const recent = await ${modelName}.find({ isDeleted: false }).sort({ createdAt: -1 }).limit(5);
|
|
442
|
+
|
|
443
|
+
return ResponseHelper.success(res, { total, recent }, 'Statistics retrieved');
|
|
444
|
+
} catch (error) {
|
|
445
|
+
next(error);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
export default router;
|
|
451
|
+
`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function generateValidators(fields) {
|
|
455
|
+
const validators = [];
|
|
456
|
+
for (const field of fields) {
|
|
457
|
+
const normalized = field.toLowerCase();
|
|
458
|
+
if (normalized === 'email') {
|
|
459
|
+
validators.push(`body('email').isEmail()`);
|
|
460
|
+
} else if (normalized === 'password') {
|
|
461
|
+
validators.push(`body('password').isLength({ min: 8 })`);
|
|
462
|
+
} else if (normalized.match(/^(price|cost|amount)$/)) {
|
|
463
|
+
validators.push(`body('${field}').optional().isFloat({ min: 0 })`);
|
|
464
|
+
} else if (normalized === 'name' || normalized === 'title') {
|
|
465
|
+
validators.push(`body('${field}').isString().isLength({ min: 2, max: 255 })`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return validators.join(',\n ');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function generateUpdateValidators(fields) {
|
|
472
|
+
return generateValidators(fields) || 'validateErrors';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export default { generateAdvancedCrudModel, generateAdvancedCrudRoutes };
|