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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR-Based Generator
|
|
3
|
+
* Main orchestrator: Scanner → IR → Templates → Generated Code
|
|
4
|
+
*
|
|
5
|
+
* This is the professional approach used by Yeoman, create-react-app, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { buildIR, validateIR, printIR } from '../ir-builder/irBuilder.js';
|
|
11
|
+
import { renderTemplate } from '../ir-builder/templateEngine.js';
|
|
12
|
+
import { MODEL_TEMPLATE } from '../ir-builder/templates/model.template.js';
|
|
13
|
+
import { ROUTES_USER_TEMPLATE } from '../ir-builder/templates/routes-user.template.js';
|
|
14
|
+
import { ROUTES_GENERIC_TEMPLATE } from '../ir-builder/templates/routes-generic.template.js';
|
|
15
|
+
import { VALIDATION_TEMPLATE } from '../ir-builder/templates/validation.template.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main entry point: Generate complete backend from scanner output
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* const generated = await generateBackendFromScanner(detectedApis, options);
|
|
22
|
+
*/
|
|
23
|
+
export async function generateBackendFromScanner(detectedApis, options = {}) {
|
|
24
|
+
try {
|
|
25
|
+
console.log('🔄 Starting IR-based generation pipeline...\n');
|
|
26
|
+
|
|
27
|
+
// Step 1: Build IR
|
|
28
|
+
console.log('📊 Step 1: Building Intermediate Representation (IR)...');
|
|
29
|
+
const ir = buildIR(detectedApis, options);
|
|
30
|
+
|
|
31
|
+
// Validate IR
|
|
32
|
+
const validation = validateIR(ir);
|
|
33
|
+
if (!validation.valid) {
|
|
34
|
+
console.error('⌠IR validation failed:');
|
|
35
|
+
validation.errors.forEach(err => console.error(` - ${err}`));
|
|
36
|
+
throw new Error('Invalid IR structure');
|
|
37
|
+
}
|
|
38
|
+
console.log('✅ IR built successfully');
|
|
39
|
+
console.log(` Resources: ${ir.resources.map(r => r.name).join(', ')}\n`);
|
|
40
|
+
|
|
41
|
+
// Step 2: Generate code for each resource
|
|
42
|
+
console.log('🔨 Step 2: Generating code from templates...');
|
|
43
|
+
const generated = {};
|
|
44
|
+
|
|
45
|
+
for (const resource of ir.resources) {
|
|
46
|
+
console.log(` Generating ${resource.name}...`);
|
|
47
|
+
|
|
48
|
+
// Use user routes template for users, generic for others
|
|
49
|
+
const routesTemplate = resource.name === 'user' ? ROUTES_USER_TEMPLATE : ROUTES_GENERIC_TEMPLATE;
|
|
50
|
+
|
|
51
|
+
generated[resource.name] = {
|
|
52
|
+
model: renderTemplate(MODEL_TEMPLATE, ir, resource.name),
|
|
53
|
+
routes: resource.name === 'user' ? routesTemplate : renderTemplate(routesTemplate, ir, resource.name),
|
|
54
|
+
validation: renderTemplate(VALIDATION_TEMPLATE, ir, resource.name),
|
|
55
|
+
ir: JSON.stringify(ir, null, 2)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`✅ Generated ${ir.resources.length} models, routes, and validators\n`);
|
|
60
|
+
|
|
61
|
+
// Step 3: Return complete structure
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
ir,
|
|
65
|
+
generated,
|
|
66
|
+
resources: ir.resources,
|
|
67
|
+
metadata: {
|
|
68
|
+
totalResources: ir.resources.length,
|
|
69
|
+
totalEndpoints: ir.resources.reduce((sum, r) => sum + r.endpoints.length, 0),
|
|
70
|
+
timestamp: new Date().toISOString()
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`⌠Generation failed: ${error.message}`);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Write generated files to disk
|
|
81
|
+
*/
|
|
82
|
+
export async function writeGeneratedFiles(generated, outputDir, projectType = 'express') {
|
|
83
|
+
try {
|
|
84
|
+
console.log(`\n📠Writing files to ${outputDir}...\n`);
|
|
85
|
+
|
|
86
|
+
// Create directory structure
|
|
87
|
+
const dirs = [
|
|
88
|
+
`${outputDir}/models`,
|
|
89
|
+
`${outputDir}/routes`,
|
|
90
|
+
`${outputDir}/validations`,
|
|
91
|
+
`${outputDir}/ir-schemas`
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const dir of dirs) {
|
|
95
|
+
if (!fs.existsSync(dir)) {
|
|
96
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
97
|
+
console.log(` 📂 Created ${path.relative(process.cwd(), dir)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Write files
|
|
102
|
+
let fileCount = 0;
|
|
103
|
+
|
|
104
|
+
for (const [resourceName, code] of Object.entries(generated)) {
|
|
105
|
+
if (typeof code === 'object') {
|
|
106
|
+
// Model
|
|
107
|
+
const modelPath = `${outputDir}/models/${capitalize(resourceName)}.model.js`;
|
|
108
|
+
fs.writeFileSync(modelPath, code.model);
|
|
109
|
+
console.log(` ✅ ${path.relative(process.cwd(), modelPath)}`);
|
|
110
|
+
fileCount++;
|
|
111
|
+
|
|
112
|
+
// Routes
|
|
113
|
+
const routesPath = `${outputDir}/routes/${resourceName}.routes.js`;
|
|
114
|
+
fs.writeFileSync(routesPath, code.routes);
|
|
115
|
+
console.log(` ✅ ${path.relative(process.cwd(), routesPath)}`);
|
|
116
|
+
fileCount++;
|
|
117
|
+
|
|
118
|
+
// Validation
|
|
119
|
+
const validationPath = `${outputDir}/validations/${resourceName}.validation.js`;
|
|
120
|
+
fs.writeFileSync(validationPath, code.validation);
|
|
121
|
+
console.log(` ✅ ${path.relative(process.cwd(), validationPath)}`);
|
|
122
|
+
fileCount++;
|
|
123
|
+
|
|
124
|
+
// IR Schema (for reference)
|
|
125
|
+
const schemaPath = `${outputDir}/ir-schemas/${resourceName}.ir.json`;
|
|
126
|
+
fs.writeFileSync(schemaPath, code.ir);
|
|
127
|
+
fileCount++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`\n✅ Generated ${fileCount} files successfully!\n`);
|
|
132
|
+
return { success: true, filesWritten: fileCount };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`⌠Failed to write files: ${error.message}`);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate complete backend structure (scaffold)
|
|
141
|
+
*/
|
|
142
|
+
export async function generateBackendScaffold(ir, outputDir) {
|
|
143
|
+
const scaffoldFiles = {
|
|
144
|
+
'server.js': generateServerFile(ir),
|
|
145
|
+
'package.json': generatePackageJson(ir),
|
|
146
|
+
'.env.example': generateEnvFile(ir),
|
|
147
|
+
'config/db.js': generateDbConfig(ir),
|
|
148
|
+
'middleware/errorHandler.js': generateErrorHandler(),
|
|
149
|
+
'middleware/requestLogger.js': generateRequestLogger()
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
console.log('\nðŸ—ï¸ Creating backend scaffold...\n');
|
|
153
|
+
|
|
154
|
+
for (const [filePath, content] of Object.entries(scaffoldFiles)) {
|
|
155
|
+
const fullPath = `${outputDir}/${filePath}`;
|
|
156
|
+
const dir = path.dirname(fullPath);
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(dir)) {
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(fullPath, content);
|
|
163
|
+
console.log(` ✅ ${path.relative(process.cwd(), fullPath)}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('\n✅ Backend scaffold created!\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Helper: Generate server.js
|
|
171
|
+
*/
|
|
172
|
+
function generateServerFile(ir) {
|
|
173
|
+
const imports = ir.resources
|
|
174
|
+
.map(r => `import ${capitalize(r.singular)}Routes from './routes/${r.name}.routes.js';`)
|
|
175
|
+
.join('\n');
|
|
176
|
+
|
|
177
|
+
const mountRoutes = ir.resources
|
|
178
|
+
.map(r => `app.use('/api/${r.plural}', ${capitalize(r.singular)}Routes);`)
|
|
179
|
+
.join('\n');
|
|
180
|
+
|
|
181
|
+
return `import express from 'express';
|
|
182
|
+
import cors from 'cors';
|
|
183
|
+
import mongoose from 'mongoose';
|
|
184
|
+
import dotenv from 'dotenv';
|
|
185
|
+
|
|
186
|
+
dotenv.config();
|
|
187
|
+
|
|
188
|
+
const app = express();
|
|
189
|
+
|
|
190
|
+
// Middleware
|
|
191
|
+
app.use(express.json());
|
|
192
|
+
app.use(cors());
|
|
193
|
+
|
|
194
|
+
// Connect to MongoDB
|
|
195
|
+
mongoose.connect(process.env.MONGODB_URI)
|
|
196
|
+
.then(() => console.log('✅ Connected to MongoDB'))
|
|
197
|
+
.catch(err => console.error('⌠MongoDB connection failed:', err));
|
|
198
|
+
|
|
199
|
+
// Routes
|
|
200
|
+
${imports}
|
|
201
|
+
|
|
202
|
+
${mountRoutes}
|
|
203
|
+
|
|
204
|
+
// Error handling
|
|
205
|
+
app.use((err, req, res, next) => {
|
|
206
|
+
console.error(err.stack);
|
|
207
|
+
res.status(err.status || 500).json({
|
|
208
|
+
success: false,
|
|
209
|
+
error: err.message
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const PORT = process.env.PORT || 3000;
|
|
214
|
+
app.listen(PORT, () => {
|
|
215
|
+
console.log(\`🚀 Server running on port \${PORT}\`);
|
|
216
|
+
});
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Helper: Generate package.json
|
|
222
|
+
*/
|
|
223
|
+
function generatePackageJson(ir) {
|
|
224
|
+
const pkg = {
|
|
225
|
+
name: 'offbyt-generated',
|
|
226
|
+
version: '1.0.0',
|
|
227
|
+
type: 'module',
|
|
228
|
+
scripts: {
|
|
229
|
+
start: 'node server.js',
|
|
230
|
+
dev: 'nodemon server.js',
|
|
231
|
+
test: 'echo "Error: no test specified" && exit 1'
|
|
232
|
+
},
|
|
233
|
+
dependencies: {
|
|
234
|
+
express: '^4.18.0',
|
|
235
|
+
mongoose: '^8.0.0',
|
|
236
|
+
cors: '^2.8.5',
|
|
237
|
+
dotenv: '^16.3.1',
|
|
238
|
+
joi: '^17.11.0'
|
|
239
|
+
},
|
|
240
|
+
devDependencies: {
|
|
241
|
+
nodemon: '^3.0.1'
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return JSON.stringify(pkg, null, 2);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Helper: Generate .env file
|
|
250
|
+
*/
|
|
251
|
+
function generateEnvFile(ir) {
|
|
252
|
+
return `# Database
|
|
253
|
+
MONGODB_URI=mongodb://localhost:27017/offbyt-${ir.settings.apiVersion}
|
|
254
|
+
|
|
255
|
+
# Server
|
|
256
|
+
PORT=3000
|
|
257
|
+
NODE_ENV=development
|
|
258
|
+
|
|
259
|
+
# API
|
|
260
|
+
API_VERSION=${ir.settings.apiVersion || 'v1'}
|
|
261
|
+
|
|
262
|
+
# Auth (if needed)
|
|
263
|
+
JWT_SECRET=your-secret-key-here
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Helper: Generate database config
|
|
269
|
+
*/
|
|
270
|
+
function generateDbConfig(ir) {
|
|
271
|
+
return `import mongoose from 'mongoose';
|
|
272
|
+
|
|
273
|
+
const connectDB = async () => {
|
|
274
|
+
try {
|
|
275
|
+
await mongoose.connect(process.env.MONGODB_URI, {
|
|
276
|
+
useNewUrlParser: true,
|
|
277
|
+
useUnifiedTopology: true,
|
|
278
|
+
});
|
|
279
|
+
console.log('✅ MongoDB connected');
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('⌠MongoDB connection failed:', error);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export default connectDB;
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Helper: Generate error handler
|
|
292
|
+
*/
|
|
293
|
+
function generateErrorHandler() {
|
|
294
|
+
return `export const errorHandler = (err, req, res, next) => {
|
|
295
|
+
const status = err.status || 500;
|
|
296
|
+
const message = err.message || 'Internal Server Error';
|
|
297
|
+
|
|
298
|
+
console.error(\`[\${new Date().toISOString()}] \${status} - \${message}\`);
|
|
299
|
+
|
|
300
|
+
res.status(status).json({
|
|
301
|
+
success: false,
|
|
302
|
+
status,
|
|
303
|
+
error: message,
|
|
304
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Helper: Generate request logger
|
|
312
|
+
*/
|
|
313
|
+
function generateRequestLogger() {
|
|
314
|
+
return `export const requestLogger = (req, res, next) => {
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
|
|
317
|
+
res.on('finish', () => {
|
|
318
|
+
const duration = Date.now() - start;
|
|
319
|
+
console.log(
|
|
320
|
+
\`[\${new Date().toISOString()}] \${req.method} \${req.path} - \${res.statusCode} (\${duration}ms)\`
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
next();
|
|
325
|
+
};
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Helper: Capitalize
|
|
331
|
+
*/
|
|
332
|
+
function capitalize(str) {
|
|
333
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Print generation summary
|
|
338
|
+
*/
|
|
339
|
+
export function printGenerationSummary(result) {
|
|
340
|
+
console.log('\n' + '='.repeat(60));
|
|
341
|
+
console.log('🎉 BACKEND GENERATION COMPLETE!');
|
|
342
|
+
console.log('='.repeat(60));
|
|
343
|
+
console.log(`\n📊 Generated Resources:`);
|
|
344
|
+
|
|
345
|
+
result.resources.forEach(resource => {
|
|
346
|
+
console.log(`\n ${capitalize(resource.singular)}:`);
|
|
347
|
+
console.log(` • Model: ${resource.name}.model.js`);
|
|
348
|
+
console.log(` • Routes: ${resource.name}.routes.js`);
|
|
349
|
+
console.log(` • Validation: ${resource.name}.validation.js`);
|
|
350
|
+
console.log(` • Fields: ${resource.fields.map(f => f.name).join(', ')}`);
|
|
351
|
+
console.log(` • Endpoints: ${resource.endpoints.map(e => `${e.method} ${e.path}`).join(', ')}`);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
console.log(`\n📈 Statistics:`);
|
|
355
|
+
console.log(` • Total Resources: ${result.metadata.totalResources}`);
|
|
356
|
+
console.log(` • Total Endpoints: ${result.metadata.totalEndpoints}`);
|
|
357
|
+
console.log(` • Generated At: ${result.metadata.timestamp}`);
|
|
358
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR Builder - Main Export
|
|
3
|
+
* Complete IR generation pipeline
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { buildIR, validateIR, printIR } from './irBuilder.js';
|
|
7
|
+
export {
|
|
8
|
+
detectFieldType,
|
|
9
|
+
buildFieldConfig,
|
|
10
|
+
shouldIndex,
|
|
11
|
+
detectRelationship,
|
|
12
|
+
getAllRules,
|
|
13
|
+
addCustomRule,
|
|
14
|
+
getRule
|
|
15
|
+
} from './rulesEngine.js';
|
|
16
|
+
export { renderTemplate, renderAllTemplates, validateTemplate } from './templateEngine.js';
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR Builder - Converts Scanner Output to Intermediate Representation
|
|
3
|
+
* Creates structured, language-agnostic description of backend
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { buildFieldConfig, detectRelationship } from './rulesEngine.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build IR from scanner detected APIs
|
|
10
|
+
* @param {Array} detectedApis - Output from scanner
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @returns {Object} - Complete IR
|
|
13
|
+
*/
|
|
14
|
+
export function buildIR(detectedApis = [], options = {}) {
|
|
15
|
+
const resources = [];
|
|
16
|
+
const relationships = [];
|
|
17
|
+
const globalSettings = {
|
|
18
|
+
hasAuth: options.hasAuth || false,
|
|
19
|
+
dbType: options.dbType || 'mongodb',
|
|
20
|
+
apiVersion: options.apiVersion || 'v1',
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Group APIs by resource
|
|
25
|
+
const resourceMap = groupApisByResource(detectedApis);
|
|
26
|
+
|
|
27
|
+
// PASS 1: Build basic resource info (names only)
|
|
28
|
+
const resourceNames = [];
|
|
29
|
+
for (const resourceName of Object.keys(resourceMap)) {
|
|
30
|
+
const normalized = normalizeResourceName(resourceName);
|
|
31
|
+
resourceNames.push({
|
|
32
|
+
name: normalized,
|
|
33
|
+
singular: singularize(resourceName),
|
|
34
|
+
plural: pluralize(resourceName)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// PASS 2: Build complete resource definitions with smart relationship detection
|
|
39
|
+
for (const [resourceName, apis] of Object.entries(resourceMap)) {
|
|
40
|
+
const resource = buildResourceIR(resourceName, apis, options, resourceNames);
|
|
41
|
+
resources.push(resource);
|
|
42
|
+
|
|
43
|
+
// Track relationships
|
|
44
|
+
resource.fields.forEach(field => {
|
|
45
|
+
if (field.relationship) {
|
|
46
|
+
relationships.push({
|
|
47
|
+
from: resource.name,
|
|
48
|
+
to: field.relationship.ref,
|
|
49
|
+
field: field.name,
|
|
50
|
+
type: 'reference'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
version: '1.0',
|
|
58
|
+
metadata: {
|
|
59
|
+
createdAt: new Date().toISOString(),
|
|
60
|
+
generator: 'offbyt-ir-builder'
|
|
61
|
+
},
|
|
62
|
+
settings: globalSettings,
|
|
63
|
+
resources,
|
|
64
|
+
relationships,
|
|
65
|
+
hooks: generateHooks(resources)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build single resource IR
|
|
71
|
+
*/
|
|
72
|
+
function buildResourceIR(resourceName, apis, options, resourceNames = []) {
|
|
73
|
+
const methods = new Set(apis.map(api => api.method));
|
|
74
|
+
|
|
75
|
+
// Infer fields from API documentation or use defaults
|
|
76
|
+
// Pass resourceNames so relationship detection can work
|
|
77
|
+
const fields = inferFieldsFromApis(apis, options, resourceNames);
|
|
78
|
+
|
|
79
|
+
// Add audit fields if auth enabled
|
|
80
|
+
if (options.hasAuth) {
|
|
81
|
+
if (!fields.find(f => f.name === 'userId')) {
|
|
82
|
+
fields.push({
|
|
83
|
+
name: 'userId',
|
|
84
|
+
type: 'ObjectId',
|
|
85
|
+
ref: 'User',
|
|
86
|
+
isRequired: true,
|
|
87
|
+
relationship: { ref: 'User' }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: normalizeResourceName(resourceName),
|
|
94
|
+
singular: singularize(resourceName),
|
|
95
|
+
plural: pluralize(resourceName),
|
|
96
|
+
description: `${capitalizeFirst(resourceName)} resource`,
|
|
97
|
+
fields,
|
|
98
|
+
routes: Array.from(methods),
|
|
99
|
+
endpoints: apis.map(api => ({
|
|
100
|
+
method: api.method,
|
|
101
|
+
path: api.path,
|
|
102
|
+
action: inferAction(api)
|
|
103
|
+
})),
|
|
104
|
+
validations: generateValidations(fields),
|
|
105
|
+
middleware: generateMiddleware(fields, options),
|
|
106
|
+
timestamps: true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Group APIs by resource name
|
|
112
|
+
*/
|
|
113
|
+
function groupApisByResource(apis) {
|
|
114
|
+
const grouped = {};
|
|
115
|
+
|
|
116
|
+
for (const api of apis) {
|
|
117
|
+
let resource = null;
|
|
118
|
+
|
|
119
|
+
// First check if api object has resource property explicitly
|
|
120
|
+
if (api.resource) {
|
|
121
|
+
resource = api.resource;
|
|
122
|
+
} else {
|
|
123
|
+
// Extract resource from path
|
|
124
|
+
// Handle both /api/users and /api/v1/users patterns
|
|
125
|
+
let match = api.path.match(/\/api\/(?:v\d+\/)?(\w+)/);
|
|
126
|
+
if (match) {
|
|
127
|
+
resource = match[1];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (resource) {
|
|
132
|
+
if (!grouped[resource]) grouped[resource] = [];
|
|
133
|
+
grouped[resource].push(api);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return grouped;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Infer fields from APIs (or use smart defaults)
|
|
142
|
+
*/
|
|
143
|
+
function inferFieldsFromApis(apis, options, resourceNames = []) {
|
|
144
|
+
const fields = [];
|
|
145
|
+
const fieldSet = new Set();
|
|
146
|
+
|
|
147
|
+
// Get common field names for this resource
|
|
148
|
+
const commonFields = getCommonFieldsForResource(apis[0]?.path);
|
|
149
|
+
|
|
150
|
+
for (const fieldName of commonFields) {
|
|
151
|
+
if (!fieldSet.has(fieldName)) {
|
|
152
|
+
const config = buildFieldConfig(fieldName, '', resourceNames);
|
|
153
|
+
fields.push(config);
|
|
154
|
+
fieldSet.add(fieldName);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return fields;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get smart default fields for resource based on type
|
|
163
|
+
* Enhanced with automatic relationship fields
|
|
164
|
+
*/
|
|
165
|
+
function getCommonFieldsForResource(apiPath) {
|
|
166
|
+
// Smart defaults based on resource type with relationship fields
|
|
167
|
+
const defaults = {
|
|
168
|
+
users: ['username', 'firstName', 'lastName', 'email', 'password', 'phone', 'avatar', 'bio', 'role', 'active'],
|
|
169
|
+
products: ['name', 'description', 'price', 'category', 'stock', 'image', 'rating', 'seller'],
|
|
170
|
+
posts: ['title', 'content', 'author', 'tags', 'published', 'views', 'likes'],
|
|
171
|
+
comments: ['text', 'author', 'post', 'likes', 'approved'],
|
|
172
|
+
categories: ['name', 'description', 'parent', 'active'],
|
|
173
|
+
events: ['title', 'description', 'club', 'date', 'location', 'capacity', 'organizer', 'image'],
|
|
174
|
+
clubs: ['name', 'description', 'admin', 'members', 'image'],
|
|
175
|
+
registrations: ['event', 'user', 'status'],
|
|
176
|
+
orders: ['items', 'total', 'status', 'user', 'deliveryAddress', 'paymentStatus'],
|
|
177
|
+
reviews: ['rating', 'text', 'author', 'product', 'verified']
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Try to match resource type from API path
|
|
181
|
+
for (const [type, fields] of Object.entries(defaults)) {
|
|
182
|
+
if (apiPath?.includes(`/${type}`)) {
|
|
183
|
+
return fields;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Default fields for any resource
|
|
188
|
+
return ['name', 'description', 'active'];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Infer action from API endpoint
|
|
193
|
+
*/
|
|
194
|
+
function inferAction(api) {
|
|
195
|
+
const methodToAction = {
|
|
196
|
+
'GET': 'read',
|
|
197
|
+
'POST': 'create',
|
|
198
|
+
'PUT': 'update',
|
|
199
|
+
'PATCH': 'update',
|
|
200
|
+
'DELETE': 'delete'
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return methodToAction[api.method] || 'custom';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate validation rules from fields
|
|
208
|
+
*/
|
|
209
|
+
function generateValidations(fields) {
|
|
210
|
+
const validations = {};
|
|
211
|
+
|
|
212
|
+
for (const field of fields) {
|
|
213
|
+
const fieldValidations = [];
|
|
214
|
+
|
|
215
|
+
if (field.isRequired) fieldValidations.push('required');
|
|
216
|
+
if (field.validators) fieldValidations.push(...field.validators);
|
|
217
|
+
|
|
218
|
+
if (fieldValidations.length > 0) {
|
|
219
|
+
validations[field.name] = fieldValidations;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return validations;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate required middleware based on fields
|
|
228
|
+
*/
|
|
229
|
+
function generateMiddleware(fields, options) {
|
|
230
|
+
const middleware = [];
|
|
231
|
+
|
|
232
|
+
// Add auth middleware if needed
|
|
233
|
+
if (options.hasAuth) {
|
|
234
|
+
middleware.push('authenticate');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Add validation middleware
|
|
238
|
+
middleware.push('validateInput');
|
|
239
|
+
|
|
240
|
+
// Add error handling
|
|
241
|
+
middleware.push('errorHandler');
|
|
242
|
+
|
|
243
|
+
return middleware;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate hooks needed for fields
|
|
248
|
+
*/
|
|
249
|
+
function generateHooks(resources) {
|
|
250
|
+
const hooks = {};
|
|
251
|
+
|
|
252
|
+
for (const resource of resources) {
|
|
253
|
+
hooks[resource.name] = [];
|
|
254
|
+
|
|
255
|
+
for (const field of resource.fields) {
|
|
256
|
+
if (field.hooks) {
|
|
257
|
+
hooks[resource.name].push(...field.hooks);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Remove duplicates
|
|
262
|
+
hooks[resource.name] = [...new Set(hooks[resource.name])];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return hooks;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Helper: Normalize resource name
|
|
270
|
+
*/
|
|
271
|
+
function normalizeResourceName(name) {
|
|
272
|
+
return name.toLowerCase().replace(/s$/, '').replace(/ies$/, 'y');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Helper: Singularize
|
|
277
|
+
*/
|
|
278
|
+
function singularize(word) {
|
|
279
|
+
if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
|
|
280
|
+
if (word.endsWith('s')) return word.slice(0, -1);
|
|
281
|
+
return word;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Helper: Pluralize
|
|
286
|
+
*/
|
|
287
|
+
function pluralize(word) {
|
|
288
|
+
if (word.endsWith('y')) return word.slice(0, -1) + 'ies';
|
|
289
|
+
return word + 's';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Helper: Capitalize first letter
|
|
294
|
+
*/
|
|
295
|
+
function capitalizeFirst(word) {
|
|
296
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Validate IR structure
|
|
301
|
+
*/
|
|
302
|
+
export function validateIR(ir) {
|
|
303
|
+
const errors = [];
|
|
304
|
+
|
|
305
|
+
if (!ir.version) errors.push('Missing IR version');
|
|
306
|
+
if (!ir.resources || !Array.isArray(ir.resources)) {
|
|
307
|
+
errors.push('Missing or invalid resources array');
|
|
308
|
+
}
|
|
309
|
+
if (!ir.settings) errors.push('Missing settings');
|
|
310
|
+
|
|
311
|
+
ir.resources?.forEach((resource, idx) => {
|
|
312
|
+
if (!resource.name) errors.push(`Resource ${idx} missing name`);
|
|
313
|
+
if (!resource.fields || !Array.isArray(resource.fields)) {
|
|
314
|
+
errors.push(`Resource ${resource.name} missing fields`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
valid: errors.length === 0,
|
|
320
|
+
errors
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Pretty print IR (for debugging)
|
|
326
|
+
*/
|
|
327
|
+
export function printIR(ir) {
|
|
328
|
+
return JSON.stringify(ir, null, 2);
|
|
329
|
+
}
|
|
330
|
+
|