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,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules Engine - Field Detection & Type Resolution
|
|
3
|
+
* Professional approach: Rules define how to detect & validate fields
|
|
4
|
+
* NOT hardcoded if/else logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Core Rules - What defines each field type
|
|
9
|
+
* Pattern: fieldNamePattern → fieldType + validators + properties
|
|
10
|
+
*/
|
|
11
|
+
const FIELD_RULES = {
|
|
12
|
+
// Email Field
|
|
13
|
+
email: {
|
|
14
|
+
patterns: ['email', 'emailaddress', 'e-mail', 'user_email'],
|
|
15
|
+
type: 'String',
|
|
16
|
+
properties: {
|
|
17
|
+
lowercase: true,
|
|
18
|
+
trim: true,
|
|
19
|
+
match: '/.+\\@.+\\..+/'
|
|
20
|
+
},
|
|
21
|
+
validators: ['email', 'required'],
|
|
22
|
+
description: 'Email field with validation'
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Password Fields
|
|
26
|
+
password: {
|
|
27
|
+
patterns: ['password', 'passwd', 'pwd', 'confirmpassword', 'newpassword'],
|
|
28
|
+
type: 'String',
|
|
29
|
+
properties: {
|
|
30
|
+
select: false, // Don't return in queries
|
|
31
|
+
minlength: 6
|
|
32
|
+
},
|
|
33
|
+
validators: ['required', 'hash'],
|
|
34
|
+
hooks: ['hash-before-save'],
|
|
35
|
+
description: 'Password field - auto hashed'
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Phone Numbers
|
|
39
|
+
phone: {
|
|
40
|
+
patterns: ['phone', 'phonenumber', 'mobile', 'mobilenumber', 'contact'],
|
|
41
|
+
type: 'String',
|
|
42
|
+
properties: {
|
|
43
|
+
match: '/^\\d{10,}$/'
|
|
44
|
+
},
|
|
45
|
+
validators: ['phone'],
|
|
46
|
+
description: 'Phone number with validation'
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// URLs
|
|
50
|
+
url: {
|
|
51
|
+
patterns: ['url', 'website', 'link', 'profileurl', 'avatar', 'image', 'photo'],
|
|
52
|
+
type: 'String',
|
|
53
|
+
properties: {
|
|
54
|
+
trim: true
|
|
55
|
+
},
|
|
56
|
+
validators: ['url'],
|
|
57
|
+
description: 'URL field with validation'
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Dates
|
|
61
|
+
date: {
|
|
62
|
+
patterns: ['date', 'createddate', 'duedate', 'deadline', 'startdate', 'enddate', 'publishdate', 'birthdate'],
|
|
63
|
+
type: 'Date',
|
|
64
|
+
properties: {},
|
|
65
|
+
validators: ['date'],
|
|
66
|
+
description: 'Date field'
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Boolean Fields
|
|
70
|
+
boolean: {
|
|
71
|
+
patterns: ['active', 'inactive', 'completed', 'published', 'verified', 'approved', 'enabled', 'disabled', 'public', 'private', 'is_', 'has_'],
|
|
72
|
+
type: 'Boolean',
|
|
73
|
+
properties: {
|
|
74
|
+
default: false
|
|
75
|
+
},
|
|
76
|
+
validators: [],
|
|
77
|
+
description: 'Boolean flag field'
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Numbers - Price/Cost
|
|
81
|
+
price: {
|
|
82
|
+
patterns: ['price', 'cost', 'amount', 'rate', 'salary', 'fee', 'charge', 'total'],
|
|
83
|
+
type: 'Number',
|
|
84
|
+
properties: {
|
|
85
|
+
default: 0,
|
|
86
|
+
min: 0
|
|
87
|
+
},
|
|
88
|
+
validators: ['number', 'positive'],
|
|
89
|
+
description: 'Monetary value'
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Numbers - Count/Rating
|
|
93
|
+
count: {
|
|
94
|
+
patterns: ['count', 'views', 'likes', 'downloads', 'rating', 'score', 'points', 'stars'],
|
|
95
|
+
type: 'Number',
|
|
96
|
+
properties: {
|
|
97
|
+
default: 0,
|
|
98
|
+
min: 0
|
|
99
|
+
},
|
|
100
|
+
validators: ['number'],
|
|
101
|
+
description: 'Numeric counter field'
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// ID/Reference Fields
|
|
105
|
+
reference: {
|
|
106
|
+
patterns: ['id', 'userid', 'authorid', 'postid', 'productid', 'categoryid', 'ownerid', 'createdby', 'updatedby', '_id', 'ref_'],
|
|
107
|
+
type: 'ObjectId',
|
|
108
|
+
properties: {
|
|
109
|
+
ref: 'AUTO' // Ref determined by field name
|
|
110
|
+
},
|
|
111
|
+
validators: [],
|
|
112
|
+
hooks: ['resolve-reference'],
|
|
113
|
+
description: 'Reference to another document'
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Status Enum
|
|
117
|
+
status: {
|
|
118
|
+
patterns: ['status', 'state', 'condition'],
|
|
119
|
+
type: 'String',
|
|
120
|
+
properties: {
|
|
121
|
+
enum: ['active', 'inactive', 'pending', 'archived', 'deleted'],
|
|
122
|
+
default: 'active'
|
|
123
|
+
},
|
|
124
|
+
validators: ['enum'],
|
|
125
|
+
description: 'Status field with predefined values'
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Array of Strings
|
|
129
|
+
tags: {
|
|
130
|
+
patterns: ['tags', 'categories', 'keywords', 'labels', 'skills', 'interests'],
|
|
131
|
+
type: 'Array',
|
|
132
|
+
properties: {
|
|
133
|
+
itemType: 'String'
|
|
134
|
+
},
|
|
135
|
+
validators: ['array'],
|
|
136
|
+
description: 'List of string values'
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Rich Text/Markdown
|
|
140
|
+
text: {
|
|
141
|
+
patterns: ['description', 'content', 'body', 'bio', 'summary', 'intro', 'notes', 'remarks', 'feedback'],
|
|
142
|
+
type: 'String',
|
|
143
|
+
properties: {
|
|
144
|
+
trim: true
|
|
145
|
+
},
|
|
146
|
+
validators: ['text'],
|
|
147
|
+
description: 'Long text field'
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Default String
|
|
151
|
+
string: {
|
|
152
|
+
patterns: ['name', 'title', 'username', 'firstname', 'lastname', 'fullname', 'displayname'],
|
|
153
|
+
type: 'String',
|
|
154
|
+
properties: {
|
|
155
|
+
trim: true,
|
|
156
|
+
required: true
|
|
157
|
+
},
|
|
158
|
+
validators: ['required', 'string'],
|
|
159
|
+
description: 'Standard string field'
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Apply rules to detect field type
|
|
165
|
+
* @param {string} fieldName - The field name from frontend
|
|
166
|
+
* @returns {Object} - Detected field configuration
|
|
167
|
+
*/
|
|
168
|
+
export function detectFieldType(fieldName) {
|
|
169
|
+
const normalizedName = fieldName.toLowerCase();
|
|
170
|
+
|
|
171
|
+
// Check each rule
|
|
172
|
+
for (const [ruleKey, rule] of Object.entries(FIELD_RULES)) {
|
|
173
|
+
// Check if field name matches any pattern
|
|
174
|
+
const matches = rule.patterns.some(pattern => {
|
|
175
|
+
if (pattern.startsWith('is_') || pattern.startsWith('has_')) {
|
|
176
|
+
return normalizedName.startsWith(pattern);
|
|
177
|
+
}
|
|
178
|
+
return normalizedName.includes(pattern);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (matches) {
|
|
182
|
+
return {
|
|
183
|
+
fieldName,
|
|
184
|
+
rule: ruleKey,
|
|
185
|
+
type: rule.type,
|
|
186
|
+
properties: { ...rule.properties },
|
|
187
|
+
validators: [...rule.validators],
|
|
188
|
+
hooks: rule.hooks || [],
|
|
189
|
+
description: rule.description
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Default: treat as string
|
|
195
|
+
return {
|
|
196
|
+
fieldName,
|
|
197
|
+
rule: 'string',
|
|
198
|
+
type: 'String',
|
|
199
|
+
properties: { trim: true },
|
|
200
|
+
validators: ['string'],
|
|
201
|
+
hooks: [],
|
|
202
|
+
description: 'Default string field'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Detect if field needs indexing
|
|
208
|
+
* @param {string} fieldName
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
export function shouldIndex(fieldName) {
|
|
212
|
+
const indexPatterns = ['email', 'username', 'userid', 'status', 'active', 'createdby'];
|
|
213
|
+
return indexPatterns.some(
|
|
214
|
+
pattern => fieldName.toLowerCase().includes(pattern)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Detect relationships between resources
|
|
220
|
+
* Enhanced to auto-detect common relationship patterns
|
|
221
|
+
* @param {string} fieldName
|
|
222
|
+
* @param {string} resourceName
|
|
223
|
+
* @param {Array} allResources - List of all available resources
|
|
224
|
+
* @returns {Object|null}
|
|
225
|
+
*/
|
|
226
|
+
export function detectRelationship(fieldName, resourceName, allResources = []) {
|
|
227
|
+
const normalized = fieldName.toLowerCase();
|
|
228
|
+
|
|
229
|
+
// Smart role-based user references (admin, organizer, owner, creator, author)
|
|
230
|
+
const userRolePatterns = ['admin', 'organizer', 'owner', 'creator', 'author', 'user', 'createdby', 'updatedby', 'assignedto'];
|
|
231
|
+
for (const pattern of userRolePatterns) {
|
|
232
|
+
if (normalized === pattern || normalized.startsWith(pattern)) {
|
|
233
|
+
return {
|
|
234
|
+
fieldName,
|
|
235
|
+
type: 'ObjectId',
|
|
236
|
+
ref: 'User',
|
|
237
|
+
isRequired: ['admin', 'organizer', 'owner', 'creator', 'user'].includes(normalized)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Array of user references (members, participants, attendees, followers, subscribers, admins, organizers)
|
|
243
|
+
const userArrayPatterns = ['members', 'participants', 'attendees', 'followers', 'subscribers', 'admins', 'organizers', 'users', 'authors', 'contributors'];
|
|
244
|
+
if (userArrayPatterns.includes(normalized)) {
|
|
245
|
+
return {
|
|
246
|
+
fieldName,
|
|
247
|
+
type: 'Array',
|
|
248
|
+
arrayItemType: 'ObjectId',
|
|
249
|
+
ref: 'User',
|
|
250
|
+
isRequired: false
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Direct resource name references (club, event, product, category, etc.)
|
|
255
|
+
// Check if field name matches any resource name
|
|
256
|
+
const resourceMatch = allResources.find(r =>
|
|
257
|
+
normalized === r.name.toLowerCase() ||
|
|
258
|
+
normalized === r.singular.toLowerCase() ||
|
|
259
|
+
normalized === r.plural.toLowerCase()
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (resourceMatch) {
|
|
263
|
+
// Check if it's plural (array) or singular (single ref)
|
|
264
|
+
const isArray = normalized === resourceMatch.plural.toLowerCase();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
fieldName,
|
|
268
|
+
type: isArray ? 'Array' : 'ObjectId',
|
|
269
|
+
arrayItemType: isArray ? 'ObjectId' : undefined,
|
|
270
|
+
ref: resourceMatch.singular.charAt(0).toUpperCase() + resourceMatch.singular.slice(1),
|
|
271
|
+
isRequired: !isArray && ['club', 'event', 'product', 'category', 'post'].includes(normalized)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// UserId → Reference to User (existing pattern)
|
|
276
|
+
// ProductId → Reference to Product
|
|
277
|
+
// AuthorId → Reference to User
|
|
278
|
+
const idMatch = normalized.match(/(\w+)id$/i);
|
|
279
|
+
if (idMatch) {
|
|
280
|
+
let refModel = idMatch[1];
|
|
281
|
+
|
|
282
|
+
// Smart mapping
|
|
283
|
+
const commonMappings = {
|
|
284
|
+
'author': 'User',
|
|
285
|
+
'user': 'User',
|
|
286
|
+
'owner': 'User',
|
|
287
|
+
'creator': 'User',
|
|
288
|
+
'admin': 'User',
|
|
289
|
+
'organizer': 'User'
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
refModel = commonMappings[refModel] ||
|
|
293
|
+
refModel.charAt(0).toUpperCase() + refModel.slice(1);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
fieldName,
|
|
297
|
+
type: 'ObjectId',
|
|
298
|
+
ref: refModel,
|
|
299
|
+
isRequired: false
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Build complete field configuration from rules
|
|
308
|
+
* @param {string} fieldName
|
|
309
|
+
* @param {string} resourceName
|
|
310
|
+
* @param {Array} allResources - List of all resources for relationship detection
|
|
311
|
+
* @returns {Object}
|
|
312
|
+
*/
|
|
313
|
+
export function buildFieldConfig(fieldName, resourceName = '', allResources = []) {
|
|
314
|
+
const detected = detectFieldType(fieldName);
|
|
315
|
+
const relationship = detectRelationship(fieldName, resourceName, allResources);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
name: fieldName,
|
|
319
|
+
...detected,
|
|
320
|
+
relationship,
|
|
321
|
+
shouldIndex: shouldIndex(fieldName),
|
|
322
|
+
isRequired:
|
|
323
|
+
fieldName.toLowerCase().includes('required') ||
|
|
324
|
+
fieldName.toLowerCase().includes('email') ||
|
|
325
|
+
fieldName.toLowerCase().includes('name') ||
|
|
326
|
+
(relationship && relationship.isRequired)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get all rules (for rule engine UI/admin)
|
|
332
|
+
*/
|
|
333
|
+
export function getAllRules() {
|
|
334
|
+
return Object.entries(FIELD_RULES).map(([key, rule]) => ({
|
|
335
|
+
id: key,
|
|
336
|
+
...rule
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Add custom rule (for extensibility)
|
|
342
|
+
*/
|
|
343
|
+
export function addCustomRule(identity, rule) {
|
|
344
|
+
FIELD_RULES[identity] = rule;
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get rule by ID
|
|
350
|
+
*/
|
|
351
|
+
export function getRule(ruleId) {
|
|
352
|
+
return FIELD_RULES[ruleId] || null;
|
|
353
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Engine - IR + Templates → Generated Code
|
|
3
|
+
* Uses simple but powerful template syntax
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render template with IR data
|
|
8
|
+
* Supports: <%= %> for variables, <% %> for logic
|
|
9
|
+
*/
|
|
10
|
+
export function renderTemplate(template, ir, resourceName) {
|
|
11
|
+
if (!template) return '';
|
|
12
|
+
|
|
13
|
+
const resource = ir.resources.find(r => r.name === resourceName);
|
|
14
|
+
if (!resource) throw new Error(`Resource ${resourceName} not found in IR`);
|
|
15
|
+
|
|
16
|
+
// Create template context with helpers
|
|
17
|
+
const context = {
|
|
18
|
+
resource,
|
|
19
|
+
ir,
|
|
20
|
+
...getTemplateHelpers()
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Simple template rendering
|
|
24
|
+
return compileTemplate(template, context);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compile template with context
|
|
29
|
+
* Supports: <%= expression %>, <% code %>, <%# comment %>
|
|
30
|
+
*/
|
|
31
|
+
function compileTemplate(template, context) {
|
|
32
|
+
let result = template;
|
|
33
|
+
|
|
34
|
+
// Remove comments <%# ... %>
|
|
35
|
+
result = result.replace(/<%#[\s\S]*?%>/g, '');
|
|
36
|
+
|
|
37
|
+
// Process output blocks <%= ... %> FIRST (before code blocks)
|
|
38
|
+
result = result.replace(/<%=([\s\S]*?)%>/g, (match, expr) => {
|
|
39
|
+
try {
|
|
40
|
+
const trimmedExpr = expr.trim();
|
|
41
|
+
const func = new Function(...Object.keys(context), `return ${trimmedExpr}`);
|
|
42
|
+
const value = func(...Object.values(context));
|
|
43
|
+
return value ?? '';
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn(`Template expression error at: ${expr.substring(0, 50)}... - ${e.message}`);
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Process code blocks <% ... %> AFTER output blocks
|
|
51
|
+
result = result.replace(/<%(?!=)([\s\S]*?)%>/g, (match, code) => {
|
|
52
|
+
try {
|
|
53
|
+
// Create function with context access
|
|
54
|
+
const func = new Function(...Object.keys(context), code);
|
|
55
|
+
func(...Object.values(context));
|
|
56
|
+
return '';
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn(`Template code block error: ${e.message}`);
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Template helper functions (available in templates)
|
|
68
|
+
*/
|
|
69
|
+
function getTemplateHelpers() {
|
|
70
|
+
return {
|
|
71
|
+
// String helpers
|
|
72
|
+
capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
|
|
73
|
+
camelCase: (str) => str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()),
|
|
74
|
+
snakeCase: (str) => str.replace(/([A-Z])/g, '_$1').toLowerCase(),
|
|
75
|
+
kebabCase: (str) => str.replace(/([A-Z])/g, '-$1').toLowerCase(),
|
|
76
|
+
|
|
77
|
+
// Field helpers
|
|
78
|
+
fieldsByType: (fields, type) => fields.filter(f => f.type === type),
|
|
79
|
+
requiredFields: (fields) => fields.filter(f => f.isRequired),
|
|
80
|
+
indexedFields: (fields) => fields.filter(f => f.shouldIndex),
|
|
81
|
+
fieldsWithValidators: (fields) => fields.filter(f => f.validators?.length > 0),
|
|
82
|
+
|
|
83
|
+
// Array helpers
|
|
84
|
+
forEach: (items, callback) => items.map(callback).join(''),
|
|
85
|
+
join: (items, separator = ', ') => items.join(separator),
|
|
86
|
+
|
|
87
|
+
// Conditional - use 'conditional' instead of 'if' to avoid reserved keyword
|
|
88
|
+
conditional: (condition, trueValue, falseValue = '') => condition ? trueValue : falseValue,
|
|
89
|
+
|
|
90
|
+
// Field schema generator for Mongoose models
|
|
91
|
+
generateFieldSchema: (field) => {
|
|
92
|
+
let type = field.type === 'ObjectId' ? 'mongoose.Types.ObjectId'
|
|
93
|
+
: field.type === 'Date' ? 'Date'
|
|
94
|
+
: field.type === 'Number' ? 'Number'
|
|
95
|
+
: field.type === 'Boolean' ? 'Boolean'
|
|
96
|
+
: 'String';
|
|
97
|
+
let def = field.name + ': { type: ' + type;
|
|
98
|
+
if (field.type === 'ObjectId' && field.relationship) {
|
|
99
|
+
def += ", ref: '" + field.relationship.ref + "'";
|
|
100
|
+
}
|
|
101
|
+
if (field.isRequired) def += ', required: true';
|
|
102
|
+
if (field.shouldIndex) def += ', index: true';
|
|
103
|
+
def += ' }';
|
|
104
|
+
return def;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Validation generator for Joi
|
|
108
|
+
generateJoiValidation: (field) => {
|
|
109
|
+
let type = field.type === 'ObjectId' ? 'string()'
|
|
110
|
+
: field.type === 'String' ? 'string()'
|
|
111
|
+
: field.type === 'Number' ? 'number()'
|
|
112
|
+
: field.type === 'Date' ? 'date()'
|
|
113
|
+
: field.type === 'Boolean' ? 'boolean()'
|
|
114
|
+
: 'string()';
|
|
115
|
+
let def = field.name + ': Joi.' + type;
|
|
116
|
+
if (field.isRequired) def += '.required()';
|
|
117
|
+
else def += '.optional()';
|
|
118
|
+
return def;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Batch render multiple templates (model, routes, etc.)
|
|
125
|
+
*/
|
|
126
|
+
export function renderAllTemplates(ir, templates) {
|
|
127
|
+
const generated = {};
|
|
128
|
+
|
|
129
|
+
for (const [templateName, template] of Object.entries(templates)) {
|
|
130
|
+
try {
|
|
131
|
+
for (const resource of ir.resources) {
|
|
132
|
+
const key = `${resource.name}.${templateName}`;
|
|
133
|
+
generated[key] = renderTemplate(template, ir, resource.name);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`Failed to render ${templateName}: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return generated;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate template syntax
|
|
145
|
+
*/
|
|
146
|
+
export function validateTemplate(template) {
|
|
147
|
+
const errors = [];
|
|
148
|
+
|
|
149
|
+
// Check for mismatched delimiters
|
|
150
|
+
const openTags = (template.match(/<%/g) || []).length;
|
|
151
|
+
const closeTags = (template.match(/%>/g) || []).length;
|
|
152
|
+
|
|
153
|
+
if (openTags !== closeTags) {
|
|
154
|
+
errors.push(`Mismatched template tags: ${openTags} opening, ${closeTags} closing`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Try simple parse
|
|
158
|
+
try {
|
|
159
|
+
new Function('resource, ir', template);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
errors.push(`Template syntax error: ${e.message}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
valid: errors.length === 0,
|
|
166
|
+
errors
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Advanced: Iterator helper for templates
|
|
172
|
+
*/
|
|
173
|
+
export class TemplateIterator {
|
|
174
|
+
constructor(items) {
|
|
175
|
+
this.items = items;
|
|
176
|
+
this.index = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
each(callback) {
|
|
180
|
+
return this.items.map((item, i) => {
|
|
181
|
+
this.index = i;
|
|
182
|
+
return callback(item, i, this.items);
|
|
183
|
+
}).join('');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
map(callback) {
|
|
187
|
+
return this.items.map(callback);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
filter(predicate) {
|
|
191
|
+
return this.items.filter(predicate);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All IR-based Templates Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { MODEL_TEMPLATE } from './model.template.js';
|
|
6
|
+
export { ROUTES_TEMPLATE } from './routes.template.js';
|
|
7
|
+
export { VALIDATION_TEMPLATE } from './validation.template.js';
|
|
8
|
+
|
|
9
|
+
// Template map for easy access
|
|
10
|
+
export const TEMPLATES = {
|
|
11
|
+
model: () => import('./model.template.js').then(m => m.MODEL_TEMPLATE),
|
|
12
|
+
routes: () => import('./routes.template.js').then(m => m.ROUTES_TEMPLATE),
|
|
13
|
+
validation: () => import('./validation.template.js').then(m => m.VALIDATION_TEMPLATE),
|
|
14
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongoose Model Template
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const MODEL_TEMPLATE = `import mongoose from 'mongoose';
|
|
6
|
+
|
|
7
|
+
const <%= capitalize(resource.singular) %>Schema = new mongoose.Schema({
|
|
8
|
+
<%= resource.name === 'user' ? \`
|
|
9
|
+
username: { type: String, unique: true, sparse: true },
|
|
10
|
+
email: { type: String, unique: true, required: true },
|
|
11
|
+
password: { type: String, required: true },
|
|
12
|
+
firstName: { type: String },
|
|
13
|
+
lastName: { type: String },
|
|
14
|
+
role: { type: String, enum: ['student', 'organizer', 'admin'], default: 'student' },
|
|
15
|
+
avatar: { type: String },
|
|
16
|
+
\` : resource.name === 'club' ? \`
|
|
17
|
+
name: { type: String, required: true },
|
|
18
|
+
description: { type: String },
|
|
19
|
+
admin: { type: mongoose.Types.ObjectId, ref: 'User', required: true },
|
|
20
|
+
members: [{ type: mongoose.Types.ObjectId, ref: 'User' }],
|
|
21
|
+
image: { type: String },
|
|
22
|
+
\` : resource.name === 'event' ? \`
|
|
23
|
+
title: { type: String, required: true },
|
|
24
|
+
description: { type: String },
|
|
25
|
+
club: { type: mongoose.Types.ObjectId, ref: 'Club', required: true },
|
|
26
|
+
date: { type: Date, required: true },
|
|
27
|
+
location: { type: String },
|
|
28
|
+
capacity: { type: Number },
|
|
29
|
+
organizer: { type: mongoose.Types.ObjectId, ref: 'User' },
|
|
30
|
+
image: { type: String },
|
|
31
|
+
\` : resource.name === 'registration' ? \`
|
|
32
|
+
event: { type: mongoose.Types.ObjectId, ref: 'Event', required: true },
|
|
33
|
+
user: { type: mongoose.Types.ObjectId, ref: 'User', required: true },
|
|
34
|
+
status: { type: String, enum: ['confirmed', 'waitlist', 'cancelled'], default: 'confirmed' },
|
|
35
|
+
\` : \`
|
|
36
|
+
name: { type: String, required: true },
|
|
37
|
+
email: { type: String },
|
|
38
|
+
\` %>
|
|
39
|
+
createdAt: { type: Date, default: Date.now },
|
|
40
|
+
updatedAt: { type: Date, default: Date.now },
|
|
41
|
+
isActive: { type: Boolean, default: true },
|
|
42
|
+
isDeleted: { type: Boolean, default: false }
|
|
43
|
+
}, { timestamps: true, collection: '<%= resource.plural.toLowerCase() %>' });
|
|
44
|
+
|
|
45
|
+
const <%= capitalize(resource.singular) %> = mongoose.model('<%= capitalize(resource.singular) %>', <%= capitalize(resource.singular) %>Schema);
|
|
46
|
+
export default <%= capitalize(resource.singular) %>;
|
|
47
|
+
`;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Resource Routes Template - CRUD only
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const ROUTES_GENERIC_TEMPLATE = `import express from 'express';
|
|
6
|
+
import <%= capitalize(resource.singular) %> from '../models/<%= capitalize(resource.singular) %>.model.js';
|
|
7
|
+
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
// GET all <%= resource.plural %>
|
|
11
|
+
router.get('/', async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const { skip = 0, limit = 100 } = req.query;
|
|
14
|
+
const data = await <%= capitalize(resource.singular) %>.find({ isDeleted: false }).skip(parseInt(skip)).limit(parseInt(limit));
|
|
15
|
+
const total = await <%= capitalize(resource.singular) %>.countDocuments({ isDeleted: false });
|
|
16
|
+
res.json({ success: true, data, total, skip: parseInt(skip), limit: parseInt(limit) });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
res.status(500).json({ success: false, error: error.message });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// GET single <%= resource.singular %>
|
|
23
|
+
router.get('/:id', async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const data = await <%= capitalize(resource.singular) %>.findById(req.params.id);
|
|
26
|
+
if (!data) return res.status(404).json({ success: false, error: 'Not found' });
|
|
27
|
+
res.json({ success: true, data });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
res.status(500).json({ success: false, error: error.message });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// POST create <%= resource.singular %>
|
|
34
|
+
router.post('/', async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const data = new <%= capitalize(resource.singular) %>(req.body);
|
|
37
|
+
await data.save();
|
|
38
|
+
res.status(201).json({ success: true, data, message: 'Created successfully' });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
res.status(400).json({ success: false, error: error.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// PUT update <%= resource.singular %>
|
|
45
|
+
router.put('/:id', async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const data = await <%= capitalize(resource.singular) %>.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
|
48
|
+
if (!data) return res.status(404).json({ success: false, error: 'Not found' });
|
|
49
|
+
res.json({ success: true, data, message: 'Updated successfully' });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
res.status(400).json({ success: false, error: error.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// DELETE <%= resource.singular %>
|
|
56
|
+
router.delete('/:id', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
await <%= capitalize(resource.singular) %>.findByIdAndUpdate(req.params.id, { isDeleted: true });
|
|
59
|
+
res.json({ success: true, message: 'Deleted successfully' });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
res.status(500).json({ success: false, error: error.message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default router;
|
|
66
|
+
`;
|