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.
Files changed (103) hide show
  1. package/README.md +2 -0
  2. package/cli/index.js +2 -0
  3. package/cli.js +206 -0
  4. package/core/detector/detectAxios.js +107 -0
  5. package/core/detector/detectFetch.js +148 -0
  6. package/core/detector/detectForms.js +55 -0
  7. package/core/detector/detectSocket.js +341 -0
  8. package/core/generator/generateControllers.js +17 -0
  9. package/core/generator/generateModels.js +25 -0
  10. package/core/generator/generateRoutes.js +17 -0
  11. package/core/generator/generateServer.js +18 -0
  12. package/core/generator/generateSocket.js +160 -0
  13. package/core/index.js +14 -0
  14. package/core/ir/IRTypes.js +25 -0
  15. package/core/ir/buildIR.js +83 -0
  16. package/core/parser/parseJS.js +26 -0
  17. package/core/parser/parseTS.js +27 -0
  18. package/core/rules/relationRules.js +38 -0
  19. package/core/rules/resourceRules.js +32 -0
  20. package/core/rules/schemaInference.js +26 -0
  21. package/core/scanner/scanProject.js +58 -0
  22. package/deploy/cloudflare.js +41 -0
  23. package/deploy/cloudflareWorker.js +122 -0
  24. package/deploy/connect.js +198 -0
  25. package/deploy/flyio.js +51 -0
  26. package/deploy/index.js +322 -0
  27. package/deploy/netlify.js +29 -0
  28. package/deploy/railway.js +215 -0
  29. package/deploy/render.js +195 -0
  30. package/deploy/utils.js +383 -0
  31. package/deploy/vercel.js +29 -0
  32. package/index.js +18 -0
  33. package/lib/generator/advancedCrudGenerator.js +475 -0
  34. package/lib/generator/crudCodeGenerator.js +486 -0
  35. package/lib/generator/irBasedGenerator.js +360 -0
  36. package/lib/ir-builder/index.js +16 -0
  37. package/lib/ir-builder/irBuilder.js +330 -0
  38. package/lib/ir-builder/rulesEngine.js +353 -0
  39. package/lib/ir-builder/templateEngine.js +193 -0
  40. package/lib/ir-builder/templates/index.js +14 -0
  41. package/lib/ir-builder/templates/model.template.js +47 -0
  42. package/lib/ir-builder/templates/routes-generic.template.js +66 -0
  43. package/lib/ir-builder/templates/routes-user.template.js +105 -0
  44. package/lib/ir-builder/templates/routes.template.js +102 -0
  45. package/lib/ir-builder/templates/validation.template.js +15 -0
  46. package/lib/ir-integration.js +349 -0
  47. package/lib/modes/benchmark.js +162 -0
  48. package/lib/modes/configBasedGenerator.js +2258 -0
  49. package/lib/modes/connect.js +1125 -0
  50. package/lib/modes/doctorAi.js +172 -0
  51. package/lib/modes/generateApi.js +435 -0
  52. package/lib/modes/interactiveSetup.js +548 -0
  53. package/lib/modes/offline.clean.js +14 -0
  54. package/lib/modes/offline.enhanced.js +787 -0
  55. package/lib/modes/offline.js +295 -0
  56. package/lib/modes/offline.v2.js +13 -0
  57. package/lib/modes/sync.js +629 -0
  58. package/lib/scanner/apiEndpointExtractor.js +387 -0
  59. package/lib/scanner/authPatternDetector.js +54 -0
  60. package/lib/scanner/frontendScanner.js +642 -0
  61. package/lib/utils/apiClientGenerator.js +242 -0
  62. package/lib/utils/apiScanner.js +95 -0
  63. package/lib/utils/codeInjector.js +350 -0
  64. package/lib/utils/doctor.js +381 -0
  65. package/lib/utils/envGenerator.js +36 -0
  66. package/lib/utils/loadTester.js +61 -0
  67. package/lib/utils/performanceAnalyzer.js +298 -0
  68. package/lib/utils/resourceDetector.js +281 -0
  69. package/package.json +20 -0
  70. package/templates/.env.template +31 -0
  71. package/templates/advanced.model.template.js +201 -0
  72. package/templates/advanced.route.template.js +341 -0
  73. package/templates/auth.middleware.template.js +87 -0
  74. package/templates/auth.routes.template.js +238 -0
  75. package/templates/auth.user.model.template.js +78 -0
  76. package/templates/cache.middleware.js +34 -0
  77. package/templates/chat.models.template.js +260 -0
  78. package/templates/chat.routes.template.js +478 -0
  79. package/templates/compression.middleware.js +19 -0
  80. package/templates/database.config.js +74 -0
  81. package/templates/errorHandler.middleware.js +54 -0
  82. package/templates/express/controller.ejs +26 -0
  83. package/templates/express/model.ejs +9 -0
  84. package/templates/express/route.ejs +18 -0
  85. package/templates/express/server.ejs +16 -0
  86. package/templates/frontend.env.template +14 -0
  87. package/templates/model.template.js +86 -0
  88. package/templates/package.production.json +51 -0
  89. package/templates/package.template.json +41 -0
  90. package/templates/pagination.utility.js +110 -0
  91. package/templates/production.server.template.js +233 -0
  92. package/templates/rateLimiter.middleware.js +36 -0
  93. package/templates/requestLogger.middleware.js +19 -0
  94. package/templates/response.helper.js +179 -0
  95. package/templates/route.template.js +130 -0
  96. package/templates/security.middleware.js +78 -0
  97. package/templates/server.template.js +91 -0
  98. package/templates/socket.server.template.js +433 -0
  99. package/templates/utils.helper.js +157 -0
  100. package/templates/validation.middleware.js +63 -0
  101. package/templates/validation.schema.js +128 -0
  102. package/utils/fileWriter.js +15 -0
  103. package/utils/logger.js +18 -0
@@ -0,0 +1,2258 @@
1
+ /**
2
+ * Configuration-Based Backend Generator
3
+ * Generates production-level backend for selected tech stack
4
+ */
5
+
6
+ import path from 'path';
7
+ import fs from 'fs';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { generateDependencies, getDatabaseConnectionTemplate, getEnvTemplate } from './interactiveSetup.js';
11
+
12
+ const TEMPLATES_DIR = path.resolve('./templates');
13
+
14
+ export async function generateWithConfig(projectPath, config) {
15
+ try {
16
+ const backendPath = path.join(projectPath, 'backend');
17
+
18
+ // Prevent stale entrypoints when switching frameworks in the same project.
19
+ cleanupFrameworkArtifacts(backendPath, config);
20
+
21
+ // Step 1: Create structure
22
+ const step1 = ora('Creating backend structure...').start();
23
+ createBackendStructure(backendPath, config);
24
+ step1.succeed('✅ Backend structure created');
25
+
26
+ // Step 2: Setup database
27
+ const step2 = ora('Setting up database configuration...').start();
28
+ setupDatabaseConfig(backendPath, config);
29
+ step2.succeed('✅ Database configured');
30
+
31
+ // Step 3: Create middleware
32
+ const step3 = ora('Setting up middleware...').start();
33
+ setupMiddleware(backendPath, config);
34
+ step3.succeed('✅ Middleware configured');
35
+
36
+ // Step 4: Create authentication (if enabled)
37
+ if (config.enableAuth) {
38
+ const step4 = ora('Setting up authentication system...').start();
39
+ setupAuthentication(backendPath, config);
40
+ step4.succeed('✅ Authentication configured');
41
+ }
42
+
43
+ // Step 5: Create main server
44
+ const step5 = ora('Creating main server file...').start();
45
+ createServerFile(backendPath, config);
46
+ step5.succeed('✅ Server created');
47
+
48
+ // Step 6: Setup Socket.io (if enabled)
49
+ if (config.enableSocket) {
50
+ const step6 = ora('Setting up realtime sockets...').start();
51
+ setupSockets(backendPath, config);
52
+ step6.succeed('✅ Sockets configured');
53
+ }
54
+
55
+ // Step 7: Create package.json
56
+ const step7 = ora('Creating package.json...').start();
57
+ createPackageJson(backendPath, config);
58
+ step7.succeed('✅ Dependencies configured');
59
+
60
+ // Step 8: Create .env
61
+ const step8 = ora('Creating environment configuration...').start();
62
+ createEnvFile(backendPath, config);
63
+ step8.succeed('✅ Environment files created (.env, .env.example, .gitignore)');
64
+
65
+ // Step 9: Scan frontend and generate detected resources (skip - will be done by smart API)
66
+ const step9 = ora('Skipping sample generation (using Smart API detection instead)...').start();
67
+ step9.succeed('✅ Backend structure ready for Smart API generation');
68
+
69
+ // Step 10: Generate SQL files for SQL databases
70
+ if (['mysql', 'postgresql', 'sqlite'].includes(config.database)) {
71
+ const step10 = ora('Generating SQL scripts...').start();
72
+ step10.succeed(`✅ SQL scripts created in backend/sql/ (ready for ${config.database.toUpperCase()})`);
73
+ }
74
+
75
+ console.log(chalk.cyan('\n🎉 Backend structure created!\n'));
76
+ console.log(chalk.cyan('✅ Smart API detection will run automatically next...\n'));
77
+
78
+ console.log(chalk.yellow('📝 Next steps:'));
79
+ console.log(chalk.yellow(` 1. cd ${projectPath}/backend`));
80
+ console.log(chalk.yellow(' 2. npm install'));
81
+ console.log(chalk.yellow(' 3. Review & update .env file:'));
82
+ console.log(chalk.gray(' - Database credentials'));
83
+ console.log(chalk.gray(' - JWT secrets (generate secure keys!)'));
84
+ console.log(chalk.gray(' - CORS origins'));
85
+ console.log(chalk.gray(' - See PRODUCTION CHECKLIST in .env'));
86
+
87
+ // Add SQL-specific instructions
88
+ if (['mysql', 'postgresql', 'sqlite'].includes(config.database)) {
89
+ console.log(chalk.yellow(' 4. Setup database using SQL scripts:'));
90
+ console.log(chalk.gray(' - Check backend/sql/01_schema.sql'));
91
+ console.log(chalk.gray(' - Run in MySQL Workbench or command line'));
92
+ console.log(chalk.gray(' - See backend/sql/README.md for instructions'));
93
+ console.log(chalk.yellow(' 5. npm run dev'));
94
+ } else {
95
+ console.log(chalk.yellow(' 4. npm run dev'));
96
+ }
97
+
98
+ console.log(chalk.cyan('\n💡 Tip: Never commit .env to git! Use .env.example instead.\n'));
99
+
100
+ } catch (error) {
101
+ console.error(chalk.red('❌ Error generating backend:'), error.message);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ function cleanupFrameworkArtifacts(backendPath, config) {
107
+ if (!fs.existsSync(backendPath)) {
108
+ return;
109
+ }
110
+
111
+ const removeIfExists = (relativePath) => {
112
+ const fullPath = path.join(backendPath, relativePath);
113
+ if (fs.existsSync(fullPath)) {
114
+ fs.rmSync(fullPath, { recursive: true, force: true });
115
+ }
116
+ };
117
+
118
+ if (config.framework === 'nestjs') {
119
+ // NestJS starts from main.ts and app.module.ts; old JS entrypoint causes confusion.
120
+ removeIfExists('server.js');
121
+ } else {
122
+ // Express/Fastify start from server.js; old Nest entrypoints are stale.
123
+ removeIfExists('main.ts');
124
+ removeIfExists('app.module.ts');
125
+ removeIfExists('tsconfig.json');
126
+ }
127
+ }
128
+
129
+ function createBackendStructure(backendPath, config) {
130
+ const dirs = [
131
+ 'config',
132
+ 'middleware',
133
+ 'models',
134
+ 'controllers',
135
+ 'routes',
136
+ 'services',
137
+ 'utils',
138
+ 'validators',
139
+ ];
140
+
141
+ if (config.enableSocket) {
142
+ dirs.push('socket', 'events');
143
+ }
144
+
145
+ if (config.enableAuth) {
146
+ dirs.push('auth');
147
+ }
148
+
149
+ for (const dir of dirs) {
150
+ const dirPath = path.join(backendPath, dir);
151
+ if (!fs.existsSync(dirPath)) {
152
+ fs.mkdirSync(dirPath, { recursive: true });
153
+ }
154
+ }
155
+
156
+ // Create directories if they don't exist
157
+ if (!fs.existsSync(backendPath)) {
158
+ fs.mkdirSync(backendPath, { recursive: true });
159
+ }
160
+ }
161
+
162
+ function setupDatabaseConfig(backendPath, config) {
163
+ const dbConfig = getDatabaseConnectionTemplate(config);
164
+
165
+ const configPath = path.join(backendPath, 'config');
166
+ if (!fs.existsSync(configPath)) {
167
+ fs.mkdirSync(configPath, { recursive: true });
168
+ }
169
+
170
+ fs.writeFileSync(
171
+ path.join(configPath, 'database.js'),
172
+ dbConfig
173
+ );
174
+ }
175
+
176
+ function setupMiddleware(backendPath, config) {
177
+ const middlewarePath = path.join(backendPath, 'middleware');
178
+
179
+ // Error handler
180
+ const errorHandler = `export const errorHandler = (err, req, res, next) => {
181
+ console.error(err.stack);
182
+
183
+ res.status(err.status || 500).json({
184
+ success: false,
185
+ message: err.message || 'Internal Server Error',
186
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
187
+ });
188
+ };`;
189
+
190
+ fs.writeFileSync(path.join(middlewarePath, 'errorHandler.js'), errorHandler);
191
+
192
+ // Request logger
193
+ const logger = `import morgan from 'morgan';
194
+
195
+ export const requestLogger = morgan(':method :url :status :response-time ms');`;
196
+
197
+ fs.writeFileSync(path.join(middlewarePath, 'logger.js'), logger);
198
+
199
+ // Validation middleware required by generated CRUD routes
200
+ const validation = `import { validationResult } from 'express-validator';
201
+
202
+ export const validateErrors = (req, res, next) => {
203
+ const errors = validationResult(req);
204
+ if (!errors.isEmpty()) {
205
+ return res.status(400).json({
206
+ success: false,
207
+ errors: errors.array()
208
+ });
209
+ }
210
+ next();
211
+ };
212
+
213
+ export const validateRequest = (schema) => {
214
+ return (req, res, next) => {
215
+ // If no schema is provided, just continue.
216
+ if (!schema || typeof schema.validate !== 'function') {
217
+ return next();
218
+ }
219
+
220
+ const { error, value } = schema.validate(req.body);
221
+
222
+ if (error) {
223
+ return res.status(400).json({
224
+ success: false,
225
+ message: 'Validation Error',
226
+ details: error.details.map(d => d.message)
227
+ });
228
+ }
229
+
230
+ req.validatedBody = value;
231
+ next();
232
+ };
233
+ };`;
234
+
235
+ fs.writeFileSync(path.join(middlewarePath, 'validation.js'), validation);
236
+
237
+ // Rate limiter
238
+ const rateLimiter = `export const rateLimiter = (limit = 100) => {
239
+ const requests = new Map();
240
+
241
+ return (req, res, next) => {
242
+ const ip = req.ip;
243
+ const now = Date.now();
244
+ const windowMs = 15 * 60 * 1000; // 15 minutes
245
+
246
+ if (!requests.has(ip)) {
247
+ requests.set(ip, []);
248
+ }
249
+
250
+ const userRequests = requests.get(ip).filter(time => now - time < windowMs);
251
+
252
+ if (userRequests.length >= limit) {
253
+ return res.status(429).json({ message: 'Too many requests' });
254
+ }
255
+
256
+ userRequests.push(now);
257
+ requests.set(ip, userRequests);
258
+ next();
259
+ };
260
+ };`;
261
+
262
+ fs.writeFileSync(path.join(middlewarePath, 'rateLimiter.js'), rateLimiter);
263
+ }
264
+
265
+ function setupAuthentication(backendPath, config) {
266
+ const authPath = path.join(backendPath, 'auth');
267
+
268
+ if (config.authType === 'jwt') {
269
+ // JWT Auth
270
+ const jwtAuth = `import jwt from 'jsonwebtoken';
271
+ import bcrypt from 'bcryptjs';
272
+
273
+ const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
274
+ const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d';
275
+
276
+ export const generateToken = (userId) => {
277
+ return jwt.sign({ userId }, JWT_SECRET, { expiresIn: JWT_EXPIRE });
278
+ };
279
+
280
+ export const verifyToken = (token) => {
281
+ try {
282
+ return jwt.verify(token, JWT_SECRET);
283
+ } catch (error) {
284
+ throw new Error('Invalid token');
285
+ }
286
+ };
287
+
288
+ export const hashPassword = async (password) => {
289
+ return bcrypt.hash(password, 10);
290
+ };
291
+
292
+ export const comparePassword = async (password, hash) => {
293
+ return bcrypt.compare(password, hash);
294
+ };
295
+
296
+ export const authMiddleware = (req, res, next) => {
297
+ const token = req.headers.authorization?.split(' ')[1];
298
+
299
+ if (!token) {
300
+ return res.status(401).json({ message: 'No token provided' });
301
+ }
302
+
303
+ try {
304
+ const decoded = verifyToken(token);
305
+ req.userId = decoded.userId;
306
+ next();
307
+ } catch (error) {
308
+ return res.status(401).json({ message: 'Invalid token' });
309
+ }
310
+ };`;
311
+
312
+ fs.writeFileSync(path.join(authPath, 'jwtAuth.js'), jwtAuth);
313
+ }
314
+ }
315
+
316
+ function createServerFile(backendPath, config) {
317
+ // Framework-specific server generation
318
+ if (config.framework === 'express') {
319
+ createExpressServer(backendPath, config);
320
+ } else if (config.framework === 'fastify') {
321
+ createFastifyServer(backendPath, config);
322
+ } else if (config.framework === 'nestjs') {
323
+ createNestJSServer(backendPath, config);
324
+ } else {
325
+ // Default to Express
326
+ createExpressServer(backendPath, config);
327
+ }
328
+ }
329
+
330
+ // Express Server Implementation
331
+ function createExpressServer(backendPath, config) {
332
+ let serverContent = `import express from 'express';
333
+ import dotenv from 'dotenv';
334
+ import cors from 'cors';
335
+ import helmet from 'helmet';
336
+ import { connectDatabase } from './config/database.js';
337
+ import { errorHandler } from './middleware/errorHandler.js';
338
+ import { requestLogger } from './middleware/logger.js';
339
+ `;
340
+
341
+ if (config.enableValidation) {
342
+ serverContent += `import { validateRequest } from './middleware/validation.js';\n`;
343
+ }
344
+
345
+ if (config.enableSocket) {
346
+ serverContent += `import { createServer } from 'http';
347
+ import { Server } from 'socket.io';
348
+ import { handleSocketEvents } from './socket/handlers.js';
349
+ `;
350
+ }
351
+
352
+ serverContent += `
353
+ dotenv.config();
354
+
355
+ const app = express();
356
+ const PORT = process.env.PORT || 5000;
357
+
358
+ // Middleware
359
+ app.use(helmet());
360
+ app.use(cors());
361
+ app.use(express.json());
362
+ app.use(express.urlencoded({ extended: true }));
363
+ app.use(requestLogger);
364
+
365
+ // Database Connection
366
+ connectDatabase().then(() => {
367
+ console.log('✅ Database connected');
368
+ }).catch(error => {
369
+ console.error('❌ Database connection failed:', error);
370
+ process.exit(1);
371
+ });
372
+
373
+ // Health Check
374
+ app.get('/api/health', (req, res) => {
375
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
376
+ });
377
+
378
+ // Error Handler
379
+ app.use(errorHandler);
380
+
381
+ // Server Start
382
+ `;
383
+
384
+ if (config.enableSocket) {
385
+ serverContent += `const server = createServer(app);
386
+ const io = new Server(server, {
387
+ cors: { origin: '*' }
388
+ });
389
+
390
+ io.on('connection', (socket) => {
391
+ console.log('User connected:', socket.id);
392
+ handleSocketEvents(io, socket);
393
+
394
+ socket.on('disconnect', () => {
395
+ console.log('User disconnected:', socket.id);
396
+ });
397
+ });
398
+
399
+ server.listen(PORT, () => {
400
+ console.log(\`🚀 Server running on http://localhost:\${PORT}\`);
401
+ });`;
402
+ } else {
403
+ serverContent += `app.listen(PORT, () => {
404
+ console.log(\`🚀 Server running on http://localhost:\${PORT}\`);
405
+ });`;
406
+ }
407
+
408
+ fs.writeFileSync(path.join(backendPath, 'server.js'), serverContent);
409
+ }
410
+
411
+ // Fastify Server Implementation
412
+ function createFastifyServer(backendPath, config) {
413
+ let serverContent = `import Fastify from 'fastify';
414
+ import fastifyCors from '@fastify/cors';
415
+ import fastifyHelmet from '@fastify/helmet';
416
+ import dotenv from 'dotenv';
417
+ import { connectDatabase } from './config/database.js';
418
+
419
+ dotenv.config();
420
+
421
+ const fastify = Fastify({
422
+ logger: ${config.enableLogging ? 'true' : 'false'}
423
+ });
424
+
425
+ const PORT = process.env.PORT || 5000;
426
+
427
+ // Register plugins
428
+ await fastify.register(fastifyHelmet);
429
+ await fastify.register(fastifyCors, { origin: '*' });
430
+
431
+ // Database Connection
432
+ connectDatabase().then(() => {
433
+ fastify.log.info('✅ Database connected');
434
+ }).catch(error => {
435
+ fastify.log.error('❌ Database connection failed:', error);
436
+ process.exit(1);
437
+ });
438
+
439
+ // Health Check
440
+ fastify.get('/api/health', async (request, reply) => {
441
+ return { status: 'ok', timestamp: new Date().toISOString() };
442
+ });
443
+
444
+ `;
445
+
446
+ if (config.enableSocket) {
447
+ serverContent += `// Socket.io with Fastify
448
+ import { Server } from 'socket.io';
449
+ import { handleSocketEvents } from './socket/handlers.js';
450
+
451
+ const server = fastify.server;
452
+ const io = new Server(server, {
453
+ cors: { origin: '*' }
454
+ });
455
+
456
+ io.on('connection', (socket) => {
457
+ fastify.log.info('User connected: ' + socket.id);
458
+ handleSocketEvents(io, socket);
459
+
460
+ socket.on('disconnect', () => {
461
+ fastify.log.info('User disconnected: ' + socket.id);
462
+ });
463
+ });
464
+
465
+ `;
466
+ }
467
+
468
+ serverContent += `// Start server
469
+ try {
470
+ await fastify.listen({ port: PORT, host: '0.0.0.0' });
471
+ console.log(\`🚀 Fastify server running on http://localhost:\${PORT}\`);
472
+ } catch (err) {
473
+ fastify.log.error(err);
474
+ process.exit(1);
475
+ }`;
476
+
477
+ fs.writeFileSync(path.join(backendPath, 'server.js'), serverContent);
478
+ }
479
+
480
+ // NestJS Server Implementation
481
+ function createNestJSServer(backendPath, config) {
482
+ // Main.ts - Entry point
483
+ const mainContent = `import 'reflect-metadata';
484
+ import { NestFactory } from '@nestjs/core';
485
+ import { AppModule } from './app.module';
486
+ import { ValidationPipe } from '@nestjs/common';
487
+ import * as dotenv from 'dotenv';
488
+ ${config.enableSocket ? `import { Server } from 'socket.io';` : ''}
489
+
490
+ dotenv.config();
491
+
492
+ async function bootstrap() {
493
+ const app = await NestFactory.create(AppModule);
494
+
495
+ // Global prefix
496
+ app.setGlobalPrefix('api');
497
+
498
+ // CORS
499
+ app.enableCors({
500
+ origin: process.env.CORS_ORIGIN || '*',
501
+ credentials: true
502
+ });
503
+
504
+ ${config.enableValidation ? `// Global validation pipe
505
+ app.useGlobalPipes(new ValidationPipe({
506
+ whitelist: true,
507
+ forbidNonWhitelisted: true,
508
+ transform: true
509
+ }));` : ''}
510
+
511
+ const PORT = process.env.PORT || 5000;
512
+ await app.listen(PORT);
513
+ ${config.enableSocket ? `
514
+ const io = new Server(app.getHttpServer(), {
515
+ cors: { origin: process.env.CORS_ORIGIN || '*' }
516
+ });
517
+
518
+ io.on('connection', (socket) => {
519
+ console.log('User connected:', socket.id);
520
+
521
+ socket.on('message:send', (data) => {
522
+ io.emit('message:new', data);
523
+ });
524
+
525
+ socket.on('user:typing', (data) => {
526
+ socket.broadcast.emit('user:typing', data);
527
+ });
528
+
529
+ socket.on('user:stopped-typing', (data) => {
530
+ socket.broadcast.emit('user:stopped-typing', data);
531
+ });
532
+
533
+ socket.on('disconnect', () => {
534
+ console.log('User disconnected:', socket.id);
535
+ });
536
+ });
537
+ ` : ''}
538
+ console.log(\`🚀 NestJS server running on http://localhost:\${PORT}\`);
539
+ }
540
+
541
+ bootstrap();`;
542
+
543
+ fs.writeFileSync(path.join(backendPath, 'main.ts'), mainContent);
544
+
545
+ // App Module
546
+ const appModuleContent = `import { Module } from '@nestjs/common';
547
+ import { ConfigModule } from '@nestjs/config';
548
+ ${config.database === 'mongodb' ? `import { MongooseModule } from '@nestjs/mongoose';` : ''}
549
+ ${['postgresql', 'mysql', 'sqlite'].includes(config.database) ? `import { TypeOrmModule } from '@nestjs/typeorm';` : ''}
550
+
551
+ @Module({
552
+ imports: [
553
+ ConfigModule.forRoot({
554
+ isGlobal: true
555
+ }),
556
+ ${config.database === 'mongodb' ? ` MongooseModule.forRoot(process.env.MONGODB_URI || 'mongodb://localhost:27017/offbyt'),` : ''}
557
+ ${config.database === 'postgresql' ? ` TypeOrmModule.forRoot({
558
+ type: 'postgres',
559
+ host: process.env.DB_HOST || 'localhost',
560
+ port: parseInt(process.env.DB_PORT) || 5432,
561
+ username: process.env.DB_USER || 'postgres',
562
+ password: process.env.DB_PASSWORD || 'password',
563
+ database: process.env.DB_NAME || 'offbyt',
564
+ autoLoadEntities: true,
565
+ synchronize: process.env.NODE_ENV === 'development'
566
+ }),` : ''}
567
+ ${config.database === 'mysql' ? ` TypeOrmModule.forRoot({
568
+ type: 'mysql',
569
+ host: process.env.DB_HOST || 'localhost',
570
+ port: parseInt(process.env.DB_PORT) || 3306,
571
+ username: process.env.DB_USER || 'root',
572
+ password: process.env.DB_PASSWORD || 'password',
573
+ database: process.env.DB_NAME || 'offbyt',
574
+ autoLoadEntities: true,
575
+ synchronize: process.env.NODE_ENV === 'development'
576
+ }),` : ''}
577
+ ${config.database === 'sqlite' ? ` TypeOrmModule.forRoot({
578
+ type: 'sqlite',
579
+ database: process.env.SQLITE_PATH || './database.sqlite',
580
+ autoLoadEntities: true,
581
+ synchronize: true
582
+ }),` : ''}
583
+ ],
584
+ controllers: [],
585
+ providers: []
586
+ })
587
+ export class AppModule {}`;
588
+
589
+ fs.writeFileSync(path.join(backendPath, 'app.module.ts'), appModuleContent);
590
+
591
+ // Create tsconfig.json
592
+ const tsconfigContent = `{
593
+ "compilerOptions": {
594
+ "module": "commonjs",
595
+ "declaration": true,
596
+ "removeComments": true,
597
+ "emitDecoratorMetadata": true,
598
+ "experimentalDecorators": true,
599
+ "allowSyntheticDefaultImports": true,
600
+ "target": "ES2021",
601
+ "sourceMap": true,
602
+ "outDir": "./dist",
603
+ "baseUrl": "./",
604
+ "incremental": true,
605
+ "skipLibCheck": true,
606
+ "strictNullChecks": false,
607
+ "noImplicitAny": false,
608
+ "strictBindCallApply": false,
609
+ "forceConsistentCasingInFileNames": false,
610
+ "noFallthroughCasesInSwitch": false,
611
+ "esModuleInterop": true
612
+ },
613
+ "include": ["*.ts", "**/*.ts"],
614
+ "exclude": ["node_modules", "dist"]
615
+ }`;
616
+
617
+ fs.writeFileSync(path.join(backendPath, 'tsconfig.json'), tsconfigContent);
618
+ }
619
+
620
+ function setupSockets(backendPath, config) {
621
+ const socketPath = path.join(backendPath, 'socket');
622
+ const eventsPath = path.join(backendPath, 'events');
623
+
624
+ // Create socket event handlers
625
+ const socketHandler = `export const handleSocketEvents = (io, socket) => {
626
+ socket.on('message:send', (data) => {
627
+ io.emit('message:new', data);
628
+ });
629
+
630
+ socket.on('user:typing', (data) => {
631
+ socket.broadcast.emit('user:typing', data);
632
+ });
633
+
634
+ socket.on('user:stopped-typing', (data) => {
635
+ socket.broadcast.emit('user:stopped-typing', data);
636
+ });
637
+ };`;
638
+
639
+ fs.writeFileSync(path.join(socketPath, 'handlers.js'), socketHandler);
640
+
641
+ // Create event emitters
642
+ const eventEmitter = `import { EventEmitter } from 'events';
643
+
644
+ export const appEvents = new EventEmitter();
645
+
646
+ appEvents.setMaxListeners(20);`;
647
+
648
+ fs.writeFileSync(path.join(eventsPath, 'emitter.js'), eventEmitter);
649
+ }
650
+
651
+ function createPackageJson(backendPath, config) {
652
+ const dependencies = generateDependencies(config);
653
+
654
+ const scripts = config.framework === 'nestjs'
655
+ ? {
656
+ build: 'tsc',
657
+ start: 'node dist/main.js',
658
+ dev: 'ts-node-dev --respawn --transpile-only main.ts',
659
+ 'start:prod': 'node dist/main.js',
660
+ test: 'echo "Error: no test specified" && exit 1'
661
+ }
662
+ : {
663
+ dev: 'node server.js',
664
+ start: 'node server.js',
665
+ test: 'echo "Error: no test specified" && exit 1'
666
+ };
667
+
668
+ const devDependencies = {
669
+ nodemon: '^3.0.1'
670
+ };
671
+
672
+ if (config.framework === 'nestjs') {
673
+ devDependencies['@nestjs/cli'] = '^10.2.1';
674
+ devDependencies['@nestjs/schematics'] = '^10.0.3';
675
+ devDependencies['@types/node'] = '^20.8.9';
676
+ devDependencies['typescript'] = '^5.2.2';
677
+ devDependencies['ts-node'] = '^10.9.1';
678
+ devDependencies['ts-node-dev'] = '^2.0.0';
679
+
680
+ if (config.database === 'mongodb') {
681
+ dependencies['@nestjs/mongoose'] = '^10.0.2';
682
+ } else {
683
+ dependencies['@nestjs/typeorm'] = '^10.0.1';
684
+ dependencies['typeorm'] = '^0.3.17';
685
+ }
686
+
687
+ if (config.enableValidation) {
688
+ dependencies['class-transformer'] = '^0.5.1';
689
+ }
690
+ } else {
691
+ // Generated route templates import from express-validator.
692
+ if (!dependencies['express-validator']) {
693
+ dependencies['express-validator'] = '^7.0.0';
694
+ }
695
+ }
696
+
697
+ const packageJson = {
698
+ name: 'offbyt-backend',
699
+ version: '1.0.0',
700
+ description: 'Production-ready backend generated by offbyt',
701
+ type: config.framework === 'nestjs' ? 'commonjs' : 'module',
702
+ main: config.framework === 'nestjs' ? 'dist/main.js' : 'server.js',
703
+ scripts,
704
+ dependencies,
705
+ devDependencies
706
+ };
707
+
708
+ fs.writeFileSync(
709
+ path.join(backendPath, 'package.json'),
710
+ JSON.stringify(packageJson, null, 2)
711
+ );
712
+ }
713
+
714
+ function createEnvFile(backendPath, config) {
715
+ const envTemplate = getEnvTemplate(config);
716
+
717
+ // Create .env file
718
+ fs.writeFileSync(path.join(backendPath, '.env'), envTemplate);
719
+
720
+ // Create .env.example for version control
721
+ fs.writeFileSync(path.join(backendPath, '.env.example'), envTemplate);
722
+
723
+ // Create/Update .gitignore to exclude .env
724
+ const gitignorePath = path.join(backendPath, '.gitignore');
725
+ let gitignoreContent = '';
726
+
727
+ if (fs.existsSync(gitignorePath)) {
728
+ gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
729
+ }
730
+
731
+ if (!gitignoreContent.includes('.env')) {
732
+ gitignoreContent += `\n# Environment variables\n.env\n.env.local\n.env.*.local\n\n# Logs\nlogs\n*.log\n\n# Dependency directories\nnode_modules/\n\n# Production build\nbuild/\ndist/\n\n# OS files\n.DS_Store\nThumbs.db\n`;
733
+ fs.writeFileSync(gitignorePath, gitignoreContent);
734
+ }
735
+ }
736
+
737
+ async function detectAndGenerateResources(projectPath, backendPath, config) {
738
+ try {
739
+ // Extract API info from frontend code
740
+ const resources = new Map();
741
+ const apiEndpoints = new Set();
742
+
743
+ // Try to scan pages directory
744
+ const pagesDir = path.join(projectPath, 'src', 'pages');
745
+ if (fs.existsSync(pagesDir)) {
746
+ const files = fs.readdirSync(pagesDir).filter(f => f.endsWith('.jsx'));
747
+
748
+ files.forEach(file => {
749
+ const content = fs.readFileSync(path.join(pagesDir, file), 'utf8');
750
+ const fileEndpoints = extractApiEndpointsFromCode(content);
751
+ fileEndpoints.forEach(endpoint => apiEndpoints.add(endpoint));
752
+
753
+ // Detect resources from all API endpoint shapes, including nested routes
754
+ fileEndpoints.forEach(endpoint => {
755
+ const segments = endpoint
756
+ .replace(/^\/api\//, '')
757
+ .split('/')
758
+ .filter(Boolean);
759
+
760
+ if (segments.length === 0) {
761
+ return;
762
+ }
763
+
764
+ const topResource = segments[0];
765
+ ensureResourceEntry(resources, topResource, content);
766
+
767
+ // Nested pattern: /api/{parent}/:id/{child}
768
+ for (let i = 0; i < segments.length - 2; i++) {
769
+ const parent = segments[i];
770
+ const param = segments[i + 1];
771
+ const child = segments[i + 2];
772
+
773
+ if (!parent.startsWith(':') && param.startsWith(':') && !child.startsWith(':')) {
774
+ ensureResourceEntry(resources, child, content);
775
+ }
776
+ }
777
+ });
778
+
779
+ // Keep operation hints updated for each detected resource in this file
780
+ resources.forEach((resourceInfo, resourceName) => {
781
+ resourceInfo.hasCreate = resourceInfo.hasCreate || hasOperationForResource(content, resourceName, 'post');
782
+ resourceInfo.hasUpdate = resourceInfo.hasUpdate || hasOperationForResource(content, resourceName, 'put') || hasOperationForResource(content, resourceName, 'patch');
783
+ resourceInfo.hasDelete = resourceInfo.hasDelete || hasOperationForResource(content, resourceName, 'delete');
784
+ resourceInfo.hasList = resourceInfo.hasList || hasOperationForResource(content, resourceName, 'get');
785
+ });
786
+ });
787
+ }
788
+
789
+ const apiAnalysis = analyzeApiStructure(apiEndpoints, resources);
790
+
791
+ // Hydrate child foreign-key columns from nested endpoint relationships
792
+ apiAnalysis.relationships.forEach(rel => {
793
+ const childResource = resources.get(rel.child);
794
+ if (!childResource) {
795
+ return;
796
+ }
797
+
798
+ const fkField = buildForeignKeyName(rel.parent);
799
+ if (!childResource.fields.includes(fkField)) {
800
+ childResource.fields.push(fkField);
801
+ }
802
+ });
803
+
804
+ // Attach relation metadata to resources for framework-specific generators
805
+ resources.forEach((resourceInfo, resourceName) => {
806
+ resourceInfo.parentRelations = apiAnalysis.relationships.filter(rel => rel.child === resourceName);
807
+ resourceInfo.childRelations = apiAnalysis.relationships.filter(rel => rel.parent === resourceName);
808
+ });
809
+
810
+ // Generate models and routes for detected resources
811
+ const generatedResources = [];
812
+ resources.forEach((resourceInfo, resourceName) => {
813
+ try {
814
+ generateModel(backendPath, resourceName, resourceInfo, config);
815
+ generateRoute(backendPath, resourceName, resourceInfo, config, apiAnalysis);
816
+ generatedResources.push(resourceName);
817
+ } catch (e) {
818
+ console.warn(`⚠️ Skipped ${resourceName}:`, e.message);
819
+ }
820
+ });
821
+
822
+ // If no resources detected, generate samples
823
+ if (generatedResources.length === 0) {
824
+ createSampleResources(backendPath, config);
825
+
826
+ // Generate SQL files even for sample resources
827
+ if (['mysql', 'postgresql', 'sqlite'].includes(config.database)) {
828
+ const sampleResources = new Map();
829
+ sampleResources.set('users', {
830
+ name: 'users',
831
+ fields: ['name', 'email', 'password'],
832
+ hasAuth: false
833
+ });
834
+ generateSQLFiles(backendPath, sampleResources, config, { relationships: [] });
835
+ }
836
+
837
+ return { resources: ['User (sample)'] };
838
+ }
839
+
840
+ // Generate SQL files for SQL databases
841
+ if (['mysql', 'postgresql', 'sqlite'].includes(config.database)) {
842
+ generateSQLFiles(backendPath, resources, config, apiAnalysis);
843
+ }
844
+
845
+ // Update server.js with routes
846
+ updateServerWithRoutes(backendPath, generatedResources, config);
847
+
848
+ return { resources: generatedResources };
849
+ } catch (error) {
850
+ console.warn('⚠️ Auto-detection error:', error.message);
851
+ createSampleResources(backendPath, config);
852
+ return { resources: ['User (sample)'] };
853
+ }
854
+ }
855
+
856
+ function ensureResourceEntry(resources, resourceName, content) {
857
+ if (!resourceName || resourceName.startsWith(':')) {
858
+ return;
859
+ }
860
+
861
+ if (!resources.has(resourceName)) {
862
+ resources.set(resourceName, {
863
+ name: resourceName,
864
+ fields: extractFieldsFromCode(content, resourceName),
865
+ hasCreate: false,
866
+ hasUpdate: false,
867
+ hasDelete: false,
868
+ hasList: false
869
+ });
870
+ }
871
+ }
872
+
873
+ function hasOperationForResource(content, resourceName, method) {
874
+ const escaped = resourceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
875
+ const axiosRegex = new RegExp(`axios\\.${method}\\s*\\(\\s*['\"\"]/api/${escaped}(?:/|[\"'\\?])`, 'i');
876
+ const fetchRegex = new RegExp(`fetch\\s*\\(\\s*['\"\"]/api/${escaped}(?:/|[\"'\\?]).*?(method\\s*:\\s*['\"]${method.toUpperCase()}['\"])?`, 'is');
877
+
878
+ if (method === 'get') {
879
+ return axiosRegex.test(content) || new RegExp(`fetch\\s*\\(\\s*['\"\"]/api/${escaped}(?:/|[\"'\\?])`, 'i').test(content);
880
+ }
881
+
882
+ return axiosRegex.test(content) || fetchRegex.test(content);
883
+ }
884
+
885
+ function extractApiEndpointsFromCode(content) {
886
+ const endpoints = new Set();
887
+ const callRegexes = [
888
+ /axios\.(?:get|post|put|delete|patch)\s*\(\s*['\"`]([^'\"`]+)['\"`]/gi,
889
+ /fetch\s*\(\s*['\"`]([^'\"`]+)['\"`]/gi
890
+ ];
891
+
892
+ callRegexes.forEach(regex => {
893
+ let match;
894
+ while ((match = regex.exec(content)) !== null) {
895
+ const value = match[1];
896
+ const apiIndex = value.indexOf(`/api/`);
897
+ if (apiIndex === -1) {
898
+ continue;
899
+ }
900
+
901
+ const endpoint = normalizeApiEndpoint(value.slice(apiIndex));
902
+ if (endpoint.startsWith(`/api/`)) {
903
+ endpoints.add(endpoint);
904
+ }
905
+ }
906
+ });
907
+
908
+ // Fallback: find plain /api/... paths in strings if call parser misses any
909
+ const fallbackRegex = /\/api\/[A-Za-z0-9_\-/:${}.]+/g;
910
+ const fallbackMatches = content.match(fallbackRegex) || [];
911
+ fallbackMatches.forEach(item => endpoints.add(normalizeApiEndpoint(item)));
912
+
913
+ return Array.from(endpoints);
914
+ }
915
+
916
+ function normalizeApiEndpoint(endpoint) {
917
+ let normalized = endpoint.trim();
918
+ normalized = normalized.replace(/\$\{[^}]+\}/g, ':id');
919
+ normalized = normalized.split('?')[0];
920
+ normalized = normalized.replace(/\/+$/g, '');
921
+ if (!normalized.startsWith(`/api/`)) {
922
+ return normalized;
923
+ }
924
+ return normalized;
925
+ }
926
+
927
+ function analyzeApiStructure(apiEndpoints, resources) {
928
+ const relationships = [];
929
+ const seenPairs = new Set();
930
+
931
+ apiEndpoints.forEach(endpoint => {
932
+ const segments = endpoint
933
+ .replace(/^\/api\//, '')
934
+ .split('/')
935
+ .filter(Boolean);
936
+
937
+ for (let i = 0; i < segments.length - 2; i++) {
938
+ const parent = segments[i];
939
+ const param = segments[i + 1];
940
+ const child = segments[i + 2];
941
+
942
+ if (parent.startsWith(':') || child.startsWith(':') || !param.startsWith(':')) {
943
+ continue;
944
+ }
945
+
946
+ if (!resources.has(parent) || !resources.has(child) || parent === child) {
947
+ continue;
948
+ }
949
+
950
+ const key = `${parent}->${child}`;
951
+ if (seenPairs.has(key)) {
952
+ continue;
953
+ }
954
+
955
+ seenPairs.add(key);
956
+ relationships.push({ parent, child, viaParam: param });
957
+ }
958
+ });
959
+
960
+ return {
961
+ endpoints: Array.from(apiEndpoints),
962
+ relationships
963
+ };
964
+ }
965
+
966
+ function buildForeignKeyName(parentResource) {
967
+ const singular = parentResource.endsWith('s') && parentResource.length > 1
968
+ ? parentResource.slice(0, -1)
969
+ : parentResource;
970
+ return `${singular}Id`;
971
+ }
972
+
973
+ function extractFieldsFromCode(content, resourceName) {
974
+ const fields = new Set();
975
+
976
+ // Look for useState patterns like setNewProduct({ name: '', price: '', ...})
977
+ const statePatterns = content.match(/setNew\w+\([{][^}]*[}]/g) || [];
978
+ statePatterns.forEach(pattern => {
979
+ const fieldMatches = pattern.match(/(\w+):/g);
980
+ if (fieldMatches) {
981
+ fieldMatches.forEach(field => {
982
+ const fieldName = field.replace(':', '');
983
+ if (fieldName && !['id', 'createdAt', 'updatedAt'].includes(fieldName)) {
984
+ fields.add(fieldName);
985
+ }
986
+ });
987
+ }
988
+ });
989
+
990
+ return Array.from(fields).slice(0, 8); // Limit to 8 fields
991
+ }
992
+
993
+ function generateModel(backendPath, resourceName, resourceInfo, config) {
994
+ if (config.framework === 'nestjs') {
995
+ generateNestJSModel(backendPath, resourceName, resourceInfo, config);
996
+ } else {
997
+ // Express and Fastify use the same model structure
998
+ generateExpressFastifyModel(backendPath, resourceName, resourceInfo, config);
999
+ }
1000
+ }
1001
+
1002
+ function generateExpressFastifyModel(backendPath, resourceName, resourceInfo, config) {
1003
+ const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).toLowerCase();
1004
+ const fields = resourceInfo.fields || ['name', 'description'];
1005
+
1006
+ let modelCode = '';
1007
+
1008
+ if (config.database === 'mongodb') {
1009
+ const schemaFields = fields
1010
+ .map(f => ` ${f}: { type: String, trim: true }`)
1011
+ .join(',\n');
1012
+ const schemaFieldBlock = schemaFields ? `${schemaFields},\n` : '';
1013
+
1014
+ modelCode = `import mongoose from 'mongoose';
1015
+
1016
+ const ${modelName.toLowerCase()}Schema = new mongoose.Schema({
1017
+ ${schemaFieldBlock} createdAt: { type: Date, default: Date.now },
1018
+ updatedAt: { type: Date, default: Date.now }
1019
+ });
1020
+
1021
+ export const ${modelName} = mongoose.model('${modelName}', ${modelName.toLowerCase()}Schema);`;
1022
+ } else {
1023
+ const sequelizeFields = fields
1024
+ .map(f => ` ${f}: { type: DataTypes.STRING, allowNull: true },`)
1025
+ .join('\n');
1026
+
1027
+ modelCode = `import { DataTypes } from 'sequelize';
1028
+ import { sequelize } from '../config/database.js';
1029
+
1030
+ export const ${modelName} = sequelize.define('${modelName}', {
1031
+ ${sequelizeFields}
1032
+ }, {
1033
+ timestamps: true
1034
+ });`;
1035
+ }
1036
+
1037
+ const modelPath = path.join(backendPath, 'models', `${modelName}.js`);
1038
+ fs.writeFileSync(modelPath, modelCode);
1039
+ }
1040
+
1041
+ function generateNestJSModel(backendPath, resourceName, resourceInfo, config) {
1042
+ const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).toLowerCase();
1043
+ const fields = resourceInfo.fields || ['name', 'description'];
1044
+
1045
+ let modelCode = '';
1046
+
1047
+ if (config.database === 'mongodb') {
1048
+ const schemaFields = fields
1049
+ .map(f => ` @Prop({ required: false })\n ${f}: string;`)
1050
+ .join('\n\n');
1051
+
1052
+ modelCode = `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
1053
+ import { Document } from 'mongoose';
1054
+
1055
+ @Schema({ timestamps: true })
1056
+ export class ${modelName} extends Document {
1057
+ ${schemaFields}
1058
+
1059
+ @Prop({ default: Date.now })
1060
+ createdAt: Date;
1061
+
1062
+ @Prop({ default: Date.now })
1063
+ updatedAt: Date;
1064
+ }
1065
+
1066
+ export const ${modelName}Schema = SchemaFactory.createForClass(${modelName});`;
1067
+ } else {
1068
+ // TypeORM for SQL databases
1069
+ const entityFields = fields
1070
+ .map(f => getNestEntityFieldDefinition(f))
1071
+ .join('\n\n');
1072
+
1073
+ modelCode = `import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
1074
+
1075
+ @Entity('${resourceName}')
1076
+ export class ${modelName} {
1077
+ @PrimaryGeneratedColumn()
1078
+ id: number;
1079
+
1080
+ ${entityFields}
1081
+
1082
+ @CreateDateColumn()
1083
+ createdAt: Date;
1084
+
1085
+ @UpdateDateColumn()
1086
+ updatedAt: Date;
1087
+ }`;
1088
+ }
1089
+
1090
+ // NestJS uses entities folder for SQL, schemas for MongoDB
1091
+ const folder = config.database === 'mongodb' ? 'schemas' : 'entities';
1092
+ const folderPath = path.join(backendPath, folder);
1093
+ if (!fs.existsSync(folderPath)) {
1094
+ fs.mkdirSync(folderPath, { recursive: true });
1095
+ }
1096
+
1097
+ const modelPath = path.join(folderPath, `${modelName.toLowerCase()}.entity.ts`);
1098
+ fs.writeFileSync(modelPath, modelCode);
1099
+
1100
+ // Compatibility model file for users expecting /models in NestJS mode
1101
+ const modelsPath = path.join(backendPath, 'models');
1102
+ if (!fs.existsSync(modelsPath)) {
1103
+ fs.mkdirSync(modelsPath, { recursive: true });
1104
+ }
1105
+ const modelCompatCode = config.database === 'mongodb'
1106
+ ? `export { ${modelName} } from '../schemas/${modelName.toLowerCase()}.entity';\n`
1107
+ : `export { ${modelName} } from '../entities/${modelName.toLowerCase()}.entity';\n`;
1108
+ fs.writeFileSync(path.join(modelsPath, `${resourceName}.model.ts`), modelCompatCode);
1109
+ }
1110
+
1111
+ function getNestEntityFieldDefinition(fieldName) {
1112
+ if (fieldName.endsWith('Id')) {
1113
+ return ` @Column({ nullable: true, type: 'int' })\n ${fieldName}: number;`;
1114
+ }
1115
+ if (fieldName.includes('price') || fieldName.includes('amount')) {
1116
+ return ` @Column({ nullable: true, type: 'decimal', precision: 10, scale: 2 })\n ${fieldName}: number;`;
1117
+ }
1118
+ if (fieldName.includes('stock') || fieldName.includes('count') || fieldName.includes('quantity')) {
1119
+ return ` @Column({ nullable: true, type: 'int' })\n ${fieldName}: number;`;
1120
+ }
1121
+ if (fieldName.includes('is') || fieldName.includes('has')) {
1122
+ return ` @Column({ nullable: true, default: false })\n ${fieldName}: boolean;`;
1123
+ }
1124
+ if (fieldName.includes('description') || fieldName.includes('content') || fieldName.includes('text')) {
1125
+ return ` @Column({ nullable: true, type: 'text' })\n ${fieldName}: string;`;
1126
+ }
1127
+ return ` @Column({ nullable: true })\n ${fieldName}: string;`;
1128
+ }
1129
+
1130
+ function generateRoute(backendPath, resourceName, resourceInfo, config, apiAnalysis) {
1131
+ if (config.framework === 'express') {
1132
+ generateExpressRoute(backendPath, resourceName, resourceInfo, config);
1133
+ } else if (config.framework === 'fastify') {
1134
+ generateFastifyRoute(backendPath, resourceName, resourceInfo, config);
1135
+ } else if (config.framework === 'nestjs') {
1136
+ generateNestJSController(backendPath, resourceName, resourceInfo, config, apiAnalysis);
1137
+ }
1138
+ }
1139
+
1140
+ function generateExpressRoute(backendPath, resourceName, resourceInfo, config) {
1141
+ const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).toLowerCase();
1142
+ const listQuery = config.database === 'mongodb'
1143
+ ? `${modelName}.find().sort({ createdAt: -1 })`
1144
+ : `${modelName}.findAll()`;
1145
+
1146
+ const routeCode = `import express from 'express';
1147
+ import { ${modelName} } from '../models/${modelName}.js';
1148
+
1149
+ const router = express.Router();
1150
+
1151
+ // GET all ${resourceName}
1152
+ router.get('/', async (req, res) => {
1153
+ try {
1154
+ const items = await ${listQuery};
1155
+ res.json({ data: items, success: true });
1156
+ } catch (error) {
1157
+ res.status(500).json({ error: error.message, success: false });
1158
+ }
1159
+ });
1160
+
1161
+ // GET ${resourceName} by ID
1162
+ router.get('/:id', async (req, res) => {
1163
+ try {
1164
+ const item = await ${modelName}.${config.database === 'mongodb' ? 'findById' : 'findByPk'}(req.params.id);
1165
+ if (!item) {
1166
+ return res.status(404).json({ error: '${modelName} not found', success: false });
1167
+ }
1168
+ res.json({ data: item, success: true });
1169
+ } catch (error) {
1170
+ res.status(500).json({ error: error.message, success: false });
1171
+ }
1172
+ });
1173
+
1174
+ // POST create ${resourceName}
1175
+ router.post('/', async (req, res) => {
1176
+ try {
1177
+ const item = ${config.database === 'mongodb' ? `new ${modelName}(req.body);
1178
+ await item.save();` : `await ${modelName}.create(req.body);`}
1179
+ res.status(201).json({ data: item, success: true });
1180
+ } catch (error) {
1181
+ res.status(400).json({ error: error.message, success: false });
1182
+ }
1183
+ });
1184
+
1185
+ // PUT update ${resourceName}
1186
+ router.put('/:id', async (req, res) => {
1187
+ try {
1188
+ const item = await ${modelName}.${config.database === 'mongodb' ? 'findByIdAndUpdate(req.params.id, req.body, { new: true })' : 'update(req.body, { where: { id: req.params.id }, returning: true })'};
1189
+ if (!item) {
1190
+ return res.status(404).json({ error: '${modelName} not found', success: false });
1191
+ }
1192
+ res.json({ data: item, success: true });
1193
+ } catch (error) {
1194
+ res.status(400).json({ error: error.message, success: false });
1195
+ }
1196
+ });
1197
+
1198
+ // DELETE ${resourceName}
1199
+ router.delete('/:id', async (req, res) => {
1200
+ try {
1201
+ const item = await ${modelName}.${config.database === 'mongodb' ? 'findByIdAndDelete' : 'destroy({ where: { id: req.params.id } })'}(req.params.id);
1202
+ if (!item) {
1203
+ return res.status(404).json({ error: '${modelName} not found', success: false });
1204
+ }
1205
+ res.json({ message: '${modelName} deleted', success: true });
1206
+ } catch (error) {
1207
+ res.status(500).json({ error: error.message, success: false });
1208
+ }
1209
+ });
1210
+
1211
+ export default router;`;
1212
+
1213
+ const routePath = path.join(backendPath, 'routes', `${resourceName}.routes.js`);
1214
+ fs.writeFileSync(routePath, routeCode);
1215
+ }
1216
+
1217
+ function generateFastifyRoute(backendPath, resourceName, resourceInfo, config) {
1218
+ const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).toLowerCase();
1219
+ const listQuery = config.database === 'mongodb'
1220
+ ? `${modelName}.find().sort({ createdAt: -1 })`
1221
+ : `${modelName}.findAll()`;
1222
+
1223
+ const routeCode = `import { ${modelName} } from '../models/${modelName}.js';
1224
+
1225
+ export default async function ${resourceName}Routes(fastify, opts) {
1226
+ // GET all ${resourceName}
1227
+ fastify.get('/api/${resourceName}', async (request, reply) => {
1228
+ try {
1229
+ const items = await ${listQuery};
1230
+ return { data: items, success: true };
1231
+ } catch (error) {
1232
+ reply.code(500);
1233
+ return { error: error.message, success: false };
1234
+ }
1235
+ });
1236
+
1237
+ // GET ${resourceName} by ID
1238
+ fastify.get('/api/${resourceName}/:id', async (request, reply) => {
1239
+ try {
1240
+ const item = await ${modelName}.${config.database === 'mongodb' ? 'findById' : 'findByPk'}(request.params.id);
1241
+ if (!item) {
1242
+ reply.code(404);
1243
+ return { error: '${modelName} not found', success: false };
1244
+ }
1245
+ return { data: item, success: true };
1246
+ } catch (error) {
1247
+ reply.code(500);
1248
+ return { error: error.message, success: false };
1249
+ }
1250
+ });
1251
+
1252
+ // POST create ${resourceName}
1253
+ fastify.post('/api/${resourceName}', async (request, reply) => {
1254
+ try {
1255
+ const item = ${config.database === 'mongodb' ? `new ${modelName}(request.body);
1256
+ await item.save();` : `await ${modelName}.create(request.body);`}
1257
+ reply.code(201);
1258
+ return { data: item, success: true };
1259
+ } catch (error) {
1260
+ reply.code(400);
1261
+ return { error: error.message, success: false };
1262
+ }
1263
+ });
1264
+
1265
+ // PUT update ${resourceName}
1266
+ fastify.put('/api/${resourceName}/:id', async (request, reply) => {
1267
+ try {
1268
+ const item = await ${modelName}.${config.database === 'mongodb' ? 'findByIdAndUpdate(request.params.id, request.body, { new: true })' : 'update(request.body, { where: { id: request.params.id }, returning: true })'};
1269
+ if (!item) {
1270
+ reply.code(404);
1271
+ return { error: '${modelName} not found', success: false };
1272
+ }
1273
+ return { data: item, success: true };
1274
+ } catch (error) {
1275
+ reply.code(400);
1276
+ return { error: error.message, success: false };
1277
+ }
1278
+ });
1279
+
1280
+ // DELETE ${resourceName}
1281
+ fastify.delete('/api/${resourceName}/:id', async (request, reply) => {
1282
+ try {
1283
+ const item = await ${modelName}.${config.database === 'mongodb' ? 'findByIdAndDelete' : 'destroy({ where: { id: request.params.id } })'}(request.params.id);
1284
+ if (!item) {
1285
+ reply.code(404);
1286
+ return { error: '${modelName} not found', success: false };
1287
+ }
1288
+ return { message: '${modelName} deleted', success: true };
1289
+ } catch (error) {
1290
+ reply.code(500);
1291
+ return { error: error.message, success: false };
1292
+ }
1293
+ });
1294
+ }`;
1295
+
1296
+ const routePath = path.join(backendPath, 'routes', `${resourceName}.routes.js`);
1297
+ fs.writeFileSync(routePath, routeCode);
1298
+ }
1299
+
1300
+ function generateNestJSController(backendPath, resourceName, resourceInfo, config, apiAnalysis) {
1301
+ const modelName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1).toLowerCase();
1302
+ const serviceName = `${modelName}Service`;
1303
+ const controllerName = `${modelName}Controller`;
1304
+ const isMongo = config.database === 'mongodb';
1305
+ const idType = isMongo ? 'string' : 'number';
1306
+ const incomingRelations = getIncomingRelationships(apiAnalysis, resourceName);
1307
+ const primaryRelation = incomingRelations.length > 0 ? incomingRelations[0] : null;
1308
+ const relationFieldName = primaryRelation ? buildForeignKeyName(primaryRelation.parent) : null;
1309
+ const relationParamName = relationFieldName || 'parentId';
1310
+ const controllerBasePath = primaryRelation
1311
+ ? `${primaryRelation.parent}/:${relationParamName}/${resourceName}`
1312
+ : resourceName;
1313
+
1314
+ const parseNumeric = (valueName) => (isMongo ? valueName : `Number(${valueName})`);
1315
+
1316
+ const relationServiceMethods = primaryRelation ? `
1317
+ async findByRelation(${relationParamName}: ${idType}) {
1318
+ ${isMongo
1319
+ ? `return this.${modelName.toLowerCase()}Model.find({ ${relationFieldName}: ${relationParamName} }).exec();`
1320
+ : `return this.${modelName.toLowerCase()}Repository.find({ where: { ${relationFieldName}: ${relationParamName} } as any });`}
1321
+ }
1322
+
1323
+ async createForRelation(${relationParamName}: ${idType}, data: Partial<${modelName}>) {
1324
+ ${isMongo
1325
+ ? `const item = new this.${modelName.toLowerCase()}Model({ ...data, ${relationFieldName}: ${relationParamName} });
1326
+ return item.save();`
1327
+ : `return this.${modelName.toLowerCase()}Repository.save({ ...data, ${relationFieldName}: ${relationParamName} } as any);`}
1328
+ }
1329
+ ` : '';
1330
+
1331
+ // Generate Service
1332
+ const serviceCode = `import { Injectable } from '@nestjs/common';
1333
+ ${isMongo ? `import { InjectModel } from '@nestjs/mongoose';
1334
+ import { Model } from 'mongoose';
1335
+ import { ${modelName} } from '../schemas/${modelName.toLowerCase()}.entity';` : `import { InjectRepository } from '@nestjs/typeorm';
1336
+ import { Repository } from 'typeorm';
1337
+ import { ${modelName} } from '../entities/${modelName.toLowerCase()}.entity';`}
1338
+
1339
+ @Injectable()
1340
+ export class ${serviceName} {
1341
+ constructor(
1342
+ ${isMongo ? `@InjectModel(${modelName}.name) private ${modelName.toLowerCase()}Model: Model<${modelName}>` : `@InjectRepository(${modelName}) private ${modelName.toLowerCase()}Repository: Repository<${modelName}>`}
1343
+ ) {}
1344
+
1345
+ async findAll() {
1346
+ ${isMongo ? `return this.${modelName.toLowerCase()}Model.find().exec();` : `return this.${modelName.toLowerCase()}Repository.find();`}
1347
+ }
1348
+
1349
+ async findOne(id: ${idType}) {
1350
+ ${isMongo ? `return this.${modelName.toLowerCase()}Model.findById(id).exec();` : `return this.${modelName.toLowerCase()}Repository.findOne({ where: { id } });`}
1351
+ }
1352
+
1353
+ async create(data: Partial<${modelName}>) {
1354
+ ${isMongo ? `const item = new this.${modelName.toLowerCase()}Model(data);
1355
+ return item.save();` : `return this.${modelName.toLowerCase()}Repository.save(data as any);`}
1356
+ }
1357
+
1358
+ async update(id: ${idType}, data: Partial<${modelName}>) {
1359
+ ${isMongo ? `return this.${modelName.toLowerCase()}Model.findByIdAndUpdate(id, data, { new: true }).exec();` : `await this.${modelName.toLowerCase()}Repository.update(id, data as any);
1360
+ return this.findOne(id);`}
1361
+ }
1362
+
1363
+ async remove(id: ${idType}) {
1364
+ ${isMongo ? `return this.${modelName.toLowerCase()}Model.findByIdAndDelete(id).exec();` : `return this.${modelName.toLowerCase()}Repository.delete(id);`}
1365
+ }
1366
+ ${relationServiceMethods}}
1367
+ `;
1368
+
1369
+ const relationControllerMethods = primaryRelation ? `
1370
+ @Get()
1371
+ async findAllByRelation(@Param('${relationParamName}') ${relationParamName}: ${idType}) {
1372
+ const items = await this.${modelName.toLowerCase()}Service.findByRelation(${parseNumeric(relationParamName)} as ${idType});
1373
+ return { data: items, success: true };
1374
+ }
1375
+
1376
+ @Post()
1377
+ async createForRelation(@Param('${relationParamName}') ${relationParamName}: ${idType}, @Body() data: any) {
1378
+ const item = await this.${modelName.toLowerCase()}Service.createForRelation(${parseNumeric(relationParamName)} as ${idType}, data);
1379
+ return { data: item, success: true };
1380
+ }
1381
+ ` : `
1382
+ @Get()
1383
+ async findAll() {
1384
+ const items = await this.${modelName.toLowerCase()}Service.findAll();
1385
+ return { data: items, success: true };
1386
+ }
1387
+
1388
+ @Post()
1389
+ async create(@Body() data: any) {
1390
+ const item = await this.${modelName.toLowerCase()}Service.create(data);
1391
+ return { data: item, success: true };
1392
+ }
1393
+ `;
1394
+
1395
+ // Generate Controller
1396
+ const controllerCode = `import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
1397
+ import { ${serviceName} } from '../services/${modelName.toLowerCase()}.service';
1398
+
1399
+ @Controller('${controllerBasePath}')
1400
+ export class ${controllerName} {
1401
+ constructor(private readonly ${modelName.toLowerCase()}Service: ${serviceName}) {}
1402
+ ${relationControllerMethods}
1403
+ @Get(':id')
1404
+ async findOne(@Param('id') id: ${idType}) {
1405
+ const item = await this.${modelName.toLowerCase()}Service.findOne(${parseNumeric('id')} as ${idType});
1406
+ if (!item) {
1407
+ return { error: '${modelName} not found', success: false };
1408
+ }
1409
+ return { data: item, success: true };
1410
+ }
1411
+
1412
+ @Put(':id')
1413
+ async update(@Param('id') id: ${idType}, @Body() data: any) {
1414
+ const item = await this.${modelName.toLowerCase()}Service.update(${parseNumeric('id')} as ${idType}, data);
1415
+ return { data: item, success: true };
1416
+ }
1417
+
1418
+ @Delete(':id')
1419
+ async remove(@Param('id') id: ${idType}) {
1420
+ await this.${modelName.toLowerCase()}Service.remove(${parseNumeric('id')} as ${idType});
1421
+ return { message: '${modelName} deleted', success: true };
1422
+ }
1423
+ }
1424
+ `;
1425
+
1426
+ // Generate Module
1427
+ const moduleCode = `import { Module } from '@nestjs/common';
1428
+ ${isMongo ? `import { MongooseModule } from '@nestjs/mongoose';
1429
+ import { ${modelName}, ${modelName}Schema } from '../schemas/${modelName.toLowerCase()}.entity';` : `import { TypeOrmModule } from '@nestjs/typeorm';
1430
+ import { ${modelName} } from '../entities/${modelName.toLowerCase()}.entity';`}
1431
+ import { ${serviceName} } from '../services/${modelName.toLowerCase()}.service';
1432
+ import { ${controllerName} } from '../controllers/${modelName.toLowerCase()}.controller';
1433
+
1434
+ @Module({
1435
+ imports: [${isMongo ? `MongooseModule.forFeature([{ name: ${modelName}.name, schema: ${modelName}Schema }])` : `TypeOrmModule.forFeature([${modelName}])`}],
1436
+ controllers: [${controllerName}],
1437
+ providers: [${serviceName}],
1438
+ exports: [${serviceName}]
1439
+ })
1440
+ export class ${modelName}Module {}
1441
+ `;
1442
+
1443
+ // Create folders
1444
+ const controllersPath = path.join(backendPath, 'controllers');
1445
+ if (!fs.existsSync(controllersPath)) {
1446
+ fs.mkdirSync(controllersPath, { recursive: true });
1447
+ }
1448
+
1449
+ const servicesPath = path.join(backendPath, 'services');
1450
+ if (!fs.existsSync(servicesPath)) {
1451
+ fs.mkdirSync(servicesPath, { recursive: true });
1452
+ }
1453
+
1454
+ const modulesPath = path.join(backendPath, 'modules');
1455
+ if (!fs.existsSync(modulesPath)) {
1456
+ fs.mkdirSync(modulesPath, { recursive: true });
1457
+ }
1458
+
1459
+ // Write files
1460
+ fs.writeFileSync(path.join(controllersPath, `${modelName.toLowerCase()}.controller.ts`), controllerCode);
1461
+ fs.writeFileSync(path.join(servicesPath, `${modelName.toLowerCase()}.service.ts`), serviceCode);
1462
+ fs.writeFileSync(path.join(modulesPath, `${modelName.toLowerCase()}.module.ts`), moduleCode);
1463
+
1464
+ // Compatibility route metadata file for users expecting /routes in NestJS mode
1465
+ const routesPath = path.join(backendPath, 'routes');
1466
+ if (!fs.existsSync(routesPath)) {
1467
+ fs.mkdirSync(routesPath, { recursive: true });
1468
+ }
1469
+
1470
+ const baseApiPath = `/api/${controllerBasePath}`;
1471
+ const routeManifestCode = `export const ${resourceName}RouteManifest = {
1472
+ framework: 'nestjs',
1473
+ resource: '${resourceName}',
1474
+ basePath: '${baseApiPath}',
1475
+ endpoints: [
1476
+ { method: 'GET', path: '${baseApiPath}' },
1477
+ { method: 'POST', path: '${baseApiPath}' },
1478
+ { method: 'GET', path: '${baseApiPath}/:id' },
1479
+ { method: 'PUT', path: '${baseApiPath}/:id' },
1480
+ { method: 'DELETE', path: '${baseApiPath}/:id' }
1481
+ ]
1482
+ };
1483
+ `;
1484
+ fs.writeFileSync(path.join(routesPath, `${resourceName}.routes.ts`), routeManifestCode);
1485
+ }
1486
+
1487
+ function updateServerWithRoutes(backendPath, resources, config) {
1488
+ if (config.framework === 'express') {
1489
+ updateExpressServer(backendPath, resources);
1490
+ } else if (config.framework === 'fastify') {
1491
+ updateFastifyServer(backendPath, resources);
1492
+ } else if (config.framework === 'nestjs') {
1493
+ updateNestJSAppModule(backendPath, resources);
1494
+ }
1495
+ }
1496
+
1497
+ function updateExpressServer(backendPath, resources) {
1498
+ const serverPath = path.join(backendPath, 'server.js');
1499
+ let serverCode = fs.readFileSync(serverPath, 'utf8');
1500
+
1501
+ // Only update if not already updated
1502
+ if (serverCode.includes('auto-generated routes')) {
1503
+ return;
1504
+ }
1505
+
1506
+ // Add imports at the top
1507
+ const importLines = resources
1508
+ .map(r => `import ${r}Router from './routes/${r}.routes.js';`)
1509
+ .join('\n');
1510
+
1511
+ // Find last import line
1512
+ const lastImportMatch = serverCode.match(/import .* from .*;\n/g);
1513
+ if (lastImportMatch && lastImportMatch.length > 0) {
1514
+ const lastImport = lastImportMatch[lastImportMatch.length - 1];
1515
+ const lastImportIndex = serverCode.lastIndexOf(lastImport);
1516
+ serverCode = serverCode.slice(0, lastImportIndex + lastImport.length) + importLines + '\n' + serverCode.slice(lastImportIndex + lastImport.length);
1517
+ }
1518
+
1519
+ // Add route registrations after health check
1520
+ const healthCheckRegex = /app\.get\('\/api\/health'[\s\S]*?\}\);/;
1521
+ const match = healthCheckRegex.exec(serverCode);
1522
+ if (match) {
1523
+ const insertPoint = match.index + match[0].length;
1524
+ const routeLines = '\n\n// Auto-generated routes\n' + resources
1525
+ .map(r => `app.use('/api/${r}', ${r}Router);`)
1526
+ .join('\n');
1527
+ serverCode = serverCode.slice(0, insertPoint) + routeLines + serverCode.slice(insertPoint);
1528
+ }
1529
+
1530
+ fs.writeFileSync(serverPath, serverCode);
1531
+ }
1532
+
1533
+ function updateFastifyServer(backendPath, resources) {
1534
+ const serverPath = path.join(backendPath, 'server.js');
1535
+ let serverCode = fs.readFileSync(serverPath, 'utf8');
1536
+
1537
+ // Only update if not already updated
1538
+ if (serverCode.includes('auto-generated routes')) {
1539
+ return;
1540
+ }
1541
+
1542
+ // Add imports at the top
1543
+ const importLines = resources
1544
+ .map(r => `import ${r}Routes from './routes/${r}.routes.js';`)
1545
+ .join('\n');
1546
+
1547
+ // Find last import line
1548
+ const lastImportMatch = serverCode.match(/import .* from .*;\n/g);
1549
+ if (lastImportMatch && lastImportMatch.length > 0) {
1550
+ const lastImport = lastImportMatch[lastImportMatch.length - 1];
1551
+ const lastImportIndex = serverCode.lastIndexOf(lastImport);
1552
+ serverCode = serverCode.slice(0, lastImportIndex + lastImport.length) + importLines + '\n' + serverCode.slice(lastImportIndex + lastImport.length);
1553
+ }
1554
+
1555
+ // Add route registrations after health check
1556
+ const healthCheckRegex = /fastify\.get\('\/api\/health'[\s\S]*?\}\);/;
1557
+ const match = healthCheckRegex.exec(serverCode);
1558
+ if (match) {
1559
+ const insertPoint = match.index + match[0].length;
1560
+ const routeLines = '\n\n// Auto-generated routes\n' + resources
1561
+ .map(r => `await fastify.register(${r}Routes);`)
1562
+ .join('\n');
1563
+ serverCode = serverCode.slice(0, insertPoint) + routeLines + serverCode.slice(insertPoint);
1564
+ }
1565
+
1566
+ fs.writeFileSync(serverPath, serverCode);
1567
+ }
1568
+
1569
+ function updateNestJSAppModule(backendPath, resources) {
1570
+ const appModulePath = path.join(backendPath, 'app.module.ts');
1571
+ let appModuleCode = fs.readFileSync(appModulePath, 'utf8');
1572
+
1573
+ // Only update if not already updated
1574
+ if (appModuleCode.includes('auto-generated modules')) {
1575
+ return;
1576
+ }
1577
+
1578
+ // Add module imports at the top
1579
+ const importLines = resources
1580
+ .map(r => {
1581
+ const modelName = r.charAt(0).toUpperCase() + r.slice(1).toLowerCase();
1582
+ return `import { ${modelName}Module } from './modules/${r.toLowerCase()}.module';`;
1583
+ })
1584
+ .join('\n');
1585
+
1586
+ // Find last import line
1587
+ const lastImportMatch = appModuleCode.match(/import .* from .*;\n/g);
1588
+ if (lastImportMatch && lastImportMatch.length > 0) {
1589
+ const lastImport = lastImportMatch[lastImportMatch.length - 1];
1590
+ const lastImportIndex = appModuleCode.lastIndexOf(lastImport);
1591
+ appModuleCode = appModuleCode.slice(0, lastImportIndex + lastImport.length) + '\n// Auto-generated modules\n' + importLines + '\n' + appModuleCode.slice(lastImportIndex + lastImport.length);
1592
+ }
1593
+
1594
+ // Add modules to AppModule imports array
1595
+ const importsArrayRegex = /imports:\s*\[([\s\S]*?)\],\s*controllers:/;
1596
+ const match = importsArrayRegex.exec(appModuleCode);
1597
+ if (match) {
1598
+ const moduleNames = resources.map(r => {
1599
+ const modelName = r.charAt(0).toUpperCase() + r.slice(1).toLowerCase();
1600
+ return `${modelName}Module`;
1601
+ });
1602
+
1603
+ const currentImports = match[1].replace(/\s+$/, '');
1604
+ const currentWithoutTrailingComma = currentImports.replace(/,\s*$/, '');
1605
+ const moduleBlock = moduleNames.join(',\n ');
1606
+ const nextImports = currentWithoutTrailingComma.trim().length > 0
1607
+ ? `${currentWithoutTrailingComma},\n ${moduleBlock}`
1608
+ : `\n ${moduleBlock}\n `;
1609
+
1610
+ appModuleCode = appModuleCode.replace(
1611
+ importsArrayRegex,
1612
+ `imports: [${nextImports}\n ],\n controllers:`
1613
+ );
1614
+ }
1615
+
1616
+ fs.writeFileSync(appModulePath, appModuleCode);
1617
+ }
1618
+
1619
+ // Generate SQL files for SQL databases
1620
+ export function generateSQLFiles(backendPath, resources, config, apiAnalysis = { relationships: [] }) {
1621
+ const sqlPath = path.join(backendPath, 'sql');
1622
+ if (!fs.existsSync(sqlPath)) {
1623
+ fs.mkdirSync(sqlPath, { recursive: true });
1624
+ }
1625
+
1626
+ const dbType = config.database; // 'mysql', 'postgresql', or 'sqlite'
1627
+
1628
+ // 01_schema.sql - CREATE TABLE statements
1629
+ let schemaSQL = generateSchemaSQL(resources, dbType, apiAnalysis);
1630
+ fs.writeFileSync(path.join(sqlPath, '01_schema.sql'), schemaSQL);
1631
+
1632
+ // 02_crud_operations.sql - CRUD queries
1633
+ let crudSQL = generateCRUDSQL(resources, dbType, apiAnalysis);
1634
+ fs.writeFileSync(path.join(sqlPath, '02_crud_operations.sql'), crudSQL);
1635
+
1636
+ // 03_relationships_joins.sql - JOIN queries
1637
+ let joinSQL = generateJoinSQL(resources, dbType, apiAnalysis);
1638
+ fs.writeFileSync(path.join(sqlPath, '03_relationships_joins.sql'), joinSQL);
1639
+
1640
+ // 04_seed_data.sql - Sample data
1641
+ let seedSQL = generateSeedSQL(resources, dbType, apiAnalysis);
1642
+ fs.writeFileSync(path.join(sqlPath, '04_seed_data.sql'), seedSQL);
1643
+
1644
+ // README.md - Instructions
1645
+ let readmeSQL = generateSQLReadme(dbType);
1646
+ fs.writeFileSync(path.join(sqlPath, 'README.md'), readmeSQL);
1647
+ }
1648
+
1649
+ function generateSchemaSQL(resources, dbType, apiAnalysis = { relationships: [] }) {
1650
+ const isPostgres = dbType === 'postgresql';
1651
+ const isSQLite = dbType === 'sqlite';
1652
+ const isMySQL = dbType === 'mysql';
1653
+
1654
+ let sql = `-- ============================================
1655
+ -- offbyt ${dbType.toUpperCase()} Schema
1656
+ -- Generated for ${dbType === 'mysql' ? 'MySQL' : dbType === 'postgresql' ? 'PostgreSQL' : 'SQLite'} Database
1657
+ -- Auto-generated from detected frontend resources
1658
+ -- ============================================
1659
+
1660
+ `;
1661
+
1662
+ if (!isSQLite) {
1663
+ sql += `-- Drop existing tables (use with caution in production)\n`;
1664
+
1665
+ resources.forEach((_, resourceName) => {
1666
+ sql += `DROP TABLE IF EXISTS ${resourceName}${isPostgres ? ' CASCADE' : ''};\n`;
1667
+ });
1668
+ sql += `\n`;
1669
+ }
1670
+
1671
+ // Generate CREATE TABLE for each resource
1672
+ resources.forEach((resourceInfo, resourceName) => {
1673
+ const tableName = resourceName;
1674
+ const fields = resourceInfo.fields || ['name', 'description'];
1675
+ const incomingRelations = getIncomingRelationships(apiAnalysis, tableName);
1676
+ const fkFields = incomingRelations.map(rel => buildForeignKeyName(rel.parent));
1677
+
1678
+ sql += `-- ============================================\n`;
1679
+ sql += `-- Table: ${tableName}\n`;
1680
+ sql += `-- Description: ${resourceName.charAt(0).toUpperCase() + resourceName.slice(1)} data\n`;
1681
+ sql += `-- ============================================\n`;
1682
+
1683
+ const tableLines = [];
1684
+ if (isPostgres) {
1685
+ tableLines.push(' id SERIAL PRIMARY KEY');
1686
+ } else if (isSQLite) {
1687
+ tableLines.push(' id INTEGER PRIMARY KEY AUTOINCREMENT');
1688
+ } else {
1689
+ tableLines.push(' id INT AUTO_INCREMENT PRIMARY KEY');
1690
+ }
1691
+
1692
+ fields.forEach(field => {
1693
+ const isRelationField = fkFields.includes(field);
1694
+ const fieldDef = isRelationField
1695
+ ? (isPostgres || isSQLite ? 'INTEGER NOT NULL' : 'INT NOT NULL')
1696
+ : getSQLFieldDefinition(field, dbType);
1697
+ tableLines.push(` ${field} ${fieldDef}`);
1698
+ });
1699
+
1700
+ fkFields.forEach(fkField => {
1701
+ if (!fields.includes(fkField)) {
1702
+ const fkType = isPostgres || isSQLite ? 'INTEGER NOT NULL' : 'INT NOT NULL';
1703
+ tableLines.push(` ${fkField} ${fkType}`);
1704
+ }
1705
+ });
1706
+
1707
+ if (isPostgres) {
1708
+ tableLines.push(' "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
1709
+ tableLines.push(' "updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
1710
+ } else if (isSQLite) {
1711
+ tableLines.push(' createdAt DATETIME DEFAULT CURRENT_TIMESTAMP');
1712
+ tableLines.push(' updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP');
1713
+ } else {
1714
+ tableLines.push(' createdAt DATETIME DEFAULT CURRENT_TIMESTAMP');
1715
+ tableLines.push(' updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
1716
+ }
1717
+
1718
+ incomingRelations.forEach(rel => {
1719
+ const fkField = buildForeignKeyName(rel.parent);
1720
+ if (isPostgres) {
1721
+ tableLines.push(` FOREIGN KEY (${fkField}) REFERENCES ${rel.parent}(id) ON DELETE CASCADE`);
1722
+ } else if (isSQLite) {
1723
+ tableLines.push(` FOREIGN KEY (${fkField}) REFERENCES ${rel.parent}(id) ON DELETE CASCADE`);
1724
+ } else {
1725
+ tableLines.push(` FOREIGN KEY (${fkField}) REFERENCES ${rel.parent}(id) ON DELETE CASCADE`);
1726
+ }
1727
+ });
1728
+
1729
+ if (isSQLite) {
1730
+ sql += `CREATE TABLE IF NOT EXISTS ${tableName} (\n${tableLines.join(',\n')}\n`;
1731
+ } else {
1732
+ sql += `CREATE TABLE ${tableName} (\n${tableLines.join(',\n')}\n`;
1733
+ }
1734
+
1735
+ if (isMySQL) {
1736
+ sql += `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\n`;
1737
+ } else {
1738
+ sql += `);\n\n`;
1739
+ }
1740
+
1741
+ // Add indexes for common fields
1742
+ if (fields.includes('email')) {
1743
+ sql += `CREATE INDEX idx_${tableName}_email ON ${tableName}(email);\n`;
1744
+ }
1745
+ if (fields.includes('name')) {
1746
+ sql += `CREATE INDEX idx_${tableName}_name ON ${tableName}(name);\n`;
1747
+ }
1748
+ if (fields.includes('category')) {
1749
+ sql += `CREATE INDEX idx_${tableName}_category ON ${tableName}(category);\n`;
1750
+ }
1751
+ fkFields.forEach(fkField => {
1752
+ if (isSQLite) {
1753
+ sql += `CREATE INDEX IF NOT EXISTS idx_${tableName}_${fkField} ON ${tableName}(${fkField});\n`;
1754
+ } else {
1755
+ sql += `CREATE INDEX idx_${tableName}_${fkField} ON ${tableName}(${fkField});\n`;
1756
+ }
1757
+ });
1758
+ sql += `\n`;
1759
+ });
1760
+
1761
+ // Keep legacy relationship templates only when dedicated resources are absent
1762
+ if (resources.has('conversations') && !resources.has('messages') && !resources.has('conversation_participants')) {
1763
+ sql += generateConversationRelationships(dbType);
1764
+ }
1765
+
1766
+ sql += `-- ============================================\n`;
1767
+ sql += `-- Schema created successfully!\n`;
1768
+ sql += `-- ============================================\n`;
1769
+ sql += `${isPostgres ? "SELECT 'Schema created successfully!' AS status;" : "SELECT 'Schema created successfully!' AS Status;"}\n`;
1770
+
1771
+ return sql;
1772
+ }
1773
+
1774
+ function getSQLFieldDefinition(fieldName, dbType) {
1775
+ const isPostgres = dbType === 'postgresql';
1776
+ const isSQLite = dbType === 'sqlite';
1777
+
1778
+ // Detect field type based on name
1779
+ if (fieldName.endsWith('Id')) {
1780
+ return isPostgres || isSQLite ? 'INTEGER' : 'INT';
1781
+ } else if (fieldName === 'id') {
1782
+ return isPostgres || isSQLite ? 'INTEGER' : 'INT';
1783
+ } else if (fieldName === 'createdAt' || fieldName === 'updatedAt') {
1784
+ return isPostgres ? 'TIMESTAMP' : 'DATETIME';
1785
+ } else if (fieldName.includes('conversation')) {
1786
+ return isPostgres || isSQLite ? 'INTEGER' : 'INT';
1787
+ } else if (fieldName.includes('user') && fieldName.endsWith('Id')) {
1788
+ return isPostgres || isSQLite ? 'INTEGER' : 'INT';
1789
+ } else if (fieldName.includes('email')) {
1790
+ return isPostgres ? 'VARCHAR(255) UNIQUE' : 'VARCHAR(255)';
1791
+ } else if (fieldName.includes('price') || fieldName.includes('amount')) {
1792
+ return 'DECIMAL(10, 2)';
1793
+ } else if (fieldName.includes('stock') || fieldName.includes('count') || fieldName.includes('quantity')) {
1794
+ return isPostgres || isSQLite ? 'INTEGER DEFAULT 0' : 'INT DEFAULT 0';
1795
+ } else if (fieldName.includes('description') || fieldName.includes('content') || fieldName.includes('text')) {
1796
+ return 'TEXT';
1797
+ } else if (fieldName.includes('is') || fieldName.includes('has')) {
1798
+ return 'BOOLEAN DEFAULT FALSE';
1799
+ } else if (fieldName.includes('date') || fieldName.includes('time')) {
1800
+ return isPostgres ? 'TIMESTAMP' : 'DATETIME';
1801
+ } else {
1802
+ return 'VARCHAR(255)';
1803
+ }
1804
+ }
1805
+
1806
+ function getDefaultInsertStatement(tableName, dbType) {
1807
+ if (dbType === 'mysql') {
1808
+ return `INSERT INTO ${tableName} () VALUES ();`;
1809
+ }
1810
+ return `INSERT INTO ${tableName} DEFAULT VALUES;`;
1811
+ }
1812
+
1813
+ function getDefaultUpdateSet(dbType) {
1814
+ if (dbType === 'postgresql') {
1815
+ return '"updatedAt" = CURRENT_TIMESTAMP';
1816
+ }
1817
+ return 'updatedAt = CURRENT_TIMESTAMP';
1818
+ }
1819
+
1820
+ function getSeedInsertStatement(tableName, dbType) {
1821
+ if (dbType === 'mysql') {
1822
+ return `INSERT INTO ${tableName} () VALUES ();`;
1823
+ }
1824
+ return `INSERT INTO ${tableName} DEFAULT VALUES;`;
1825
+ }
1826
+
1827
+ function getIncomingRelationships(apiAnalysis, resourceName) {
1828
+ if (!apiAnalysis || !Array.isArray(apiAnalysis.relationships)) {
1829
+ return [];
1830
+ }
1831
+ return apiAnalysis.relationships.filter(rel => rel.child === resourceName);
1832
+ }
1833
+
1834
+ function generateConversationRelationships(dbType) {
1835
+ const isPostgres = dbType === 'postgresql';
1836
+ const isSQLite = dbType === 'sqlite';
1837
+
1838
+ let sql = `-- ============================================\n`;
1839
+ sql += `-- Relationship Tables for Conversations\n`;
1840
+ sql += `-- ============================================\n\n`;
1841
+
1842
+ // conversation_participants table
1843
+ if (isPostgres) {
1844
+ sql += `CREATE TABLE conversation_participants (\n`;
1845
+ sql += ` id SERIAL PRIMARY KEY,\n`;
1846
+ sql += ` "conversationId" INTEGER NOT NULL,\n`;
1847
+ sql += ` "userId" INTEGER NOT NULL,\n`;
1848
+ sql += ` "joinedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n`;
1849
+ sql += ` FOREIGN KEY ("conversationId") REFERENCES conversations(id) ON DELETE CASCADE,\n`;
1850
+ sql += ` FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE,\n`;
1851
+ sql += ` UNIQUE ("conversationId", "userId")\n`;
1852
+ sql += `);\n\n`;
1853
+ } else if (isSQLite) {
1854
+ sql += `CREATE TABLE IF NOT EXISTS conversation_participants (\n`;
1855
+ sql += ` id INTEGER PRIMARY KEY AUTOINCREMENT,\n`;
1856
+ sql += ` conversationId INTEGER NOT NULL,\n`;
1857
+ sql += ` userId INTEGER NOT NULL,\n`;
1858
+ sql += ` joinedAt DATETIME DEFAULT CURRENT_TIMESTAMP,\n`;
1859
+ sql += ` FOREIGN KEY (conversationId) REFERENCES conversations(id) ON DELETE CASCADE,\n`;
1860
+ sql += ` FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,\n`;
1861
+ sql += ` UNIQUE (conversationId, userId)\n`;
1862
+ sql += `);\n\n`;
1863
+ } else {
1864
+ sql += `CREATE TABLE conversation_participants (\n`;
1865
+ sql += ` id INT AUTO_INCREMENT PRIMARY KEY,\n`;
1866
+ sql += ` conversationId INT NOT NULL,\n`;
1867
+ sql += ` userId INT NOT NULL,\n`;
1868
+ sql += ` joinedAt DATETIME DEFAULT CURRENT_TIMESTAMP,\n`;
1869
+ sql += ` FOREIGN KEY (conversationId) REFERENCES conversations(id) ON DELETE CASCADE,\n`;
1870
+ sql += ` FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE,\n`;
1871
+ sql += ` UNIQUE KEY unique_conversation_user (conversationId, userId),\n`;
1872
+ sql += ` INDEX idx_conversationId (conversationId),\n`;
1873
+ sql += ` INDEX idx_userId (userId)\n`;
1874
+ sql += `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\n`;
1875
+ }
1876
+
1877
+ // messages table
1878
+ if (isPostgres) {
1879
+ sql += `CREATE TABLE messages (\n`;
1880
+ sql += ` id SERIAL PRIMARY KEY,\n`;
1881
+ sql += ` "conversationId" INTEGER NOT NULL,\n`;
1882
+ sql += ` "senderId" INTEGER NOT NULL,\n`;
1883
+ sql += ` text TEXT NOT NULL,\n`;
1884
+ sql += ` "isRead" BOOLEAN DEFAULT FALSE,\n`;
1885
+ sql += ` "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n`;
1886
+ sql += ` FOREIGN KEY ("conversationId") REFERENCES conversations(id) ON DELETE CASCADE,\n`;
1887
+ sql += ` FOREIGN KEY ("senderId") REFERENCES users(id) ON DELETE CASCADE\n`;
1888
+ sql += `);\n\n`;
1889
+ sql += `CREATE INDEX idx_messages_conversationId ON messages("conversationId");\n`;
1890
+ sql += `CREATE INDEX idx_messages_senderId ON messages("senderId");\n\n`;
1891
+ } else if (isSQLite) {
1892
+ sql += `CREATE TABLE IF NOT EXISTS messages (\n`;
1893
+ sql += ` id INTEGER PRIMARY KEY AUTOINCREMENT,\n`;
1894
+ sql += ` conversationId INTEGER NOT NULL,\n`;
1895
+ sql += ` senderId INTEGER NOT NULL,\n`;
1896
+ sql += ` text TEXT NOT NULL,\n`;
1897
+ sql += ` isRead BOOLEAN DEFAULT 0,\n`;
1898
+ sql += ` createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,\n`;
1899
+ sql += ` FOREIGN KEY (conversationId) REFERENCES conversations(id) ON DELETE CASCADE,\n`;
1900
+ sql += ` FOREIGN KEY (senderId) REFERENCES users(id) ON DELETE CASCADE\n`;
1901
+ sql += `);\n\n`;
1902
+ sql += `CREATE INDEX IF NOT EXISTS idx_messages_conversationId ON messages(conversationId);\n`;
1903
+ sql += `CREATE INDEX IF NOT EXISTS idx_messages_senderId ON messages(senderId);\n\n`;
1904
+ } else {
1905
+ sql += `CREATE TABLE messages (\n`;
1906
+ sql += ` id INT AUTO_INCREMENT PRIMARY KEY,\n`;
1907
+ sql += ` conversationId INT NOT NULL,\n`;
1908
+ sql += ` senderId INT NOT NULL,\n`;
1909
+ sql += ` text TEXT NOT NULL,\n`;
1910
+ sql += ` isRead BOOLEAN DEFAULT FALSE,\n`;
1911
+ sql += ` createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,\n`;
1912
+ sql += ` FOREIGN KEY (conversationId) REFERENCES conversations(id) ON DELETE CASCADE,\n`;
1913
+ sql += ` FOREIGN KEY (senderId) REFERENCES users(id) ON DELETE CASCADE,\n`;
1914
+ sql += ` INDEX idx_conversationId (conversationId),\n`;
1915
+ sql += ` INDEX idx_senderId (senderId)\n`;
1916
+ sql += `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\n`;
1917
+ }
1918
+
1919
+ return sql;
1920
+ }
1921
+
1922
+ function generateCRUDSQL(resources, dbType, apiAnalysis = { relationships: [] }) {
1923
+ let sql = `-- ============================================\n`;
1924
+ sql += `-- offbyt ${dbType.toUpperCase()} CRUD Operations\n`;
1925
+ sql += `-- All Create, Read, Update, Delete Queries\n`;
1926
+ sql += `-- Ready to use in your application\n`;
1927
+ sql += `-- ============================================\n\n`;
1928
+
1929
+ resources.forEach((resourceInfo, resourceName) => {
1930
+ const fields = resourceInfo.fields || ['name'];
1931
+ const incomingRelations = getIncomingRelationships(apiAnalysis, resourceName);
1932
+ const relationFields = incomingRelations
1933
+ .map(rel => buildForeignKeyName(rel.parent))
1934
+ .filter(field => !fields.includes(field));
1935
+ const allInsertFields = [...fields, ...relationFields];
1936
+ const sampleValues = allInsertFields.map(f => generateSampleValue(f)).join(', ');
1937
+ const updateSets = allInsertFields.slice(0, 2).map(f => `${f} = ${generateSampleValue(f)}`).join(', ');
1938
+
1939
+ sql += `-- ============================================\n`;
1940
+ sql += `-- ${resourceName.toUpperCase()} - CRUD Operations\n`;
1941
+ sql += `-- ============================================\n\n`;
1942
+
1943
+ // CREATE
1944
+ sql += `-- CREATE: Insert new ${resourceName}\n`;
1945
+ if (allInsertFields.length === 0) {
1946
+ sql += `${getDefaultInsertStatement(resourceName, dbType)}\n\n`;
1947
+ } else {
1948
+ sql += `INSERT INTO ${resourceName} (${allInsertFields.join(', ')})\n`;
1949
+ sql += `VALUES (${sampleValues});\n\n`;
1950
+ }
1951
+
1952
+ // READ
1953
+ sql += `-- READ: Get all ${resourceName}\n`;
1954
+ sql += `SELECT * FROM ${resourceName} ORDER BY createdAt DESC;\n\n`;
1955
+
1956
+ sql += `-- READ: Get ${resourceName} by ID\n`;
1957
+ sql += `SELECT * FROM ${resourceName} WHERE id = 1;\n\n`;
1958
+
1959
+ if (fields.includes('name')) {
1960
+ sql += `-- READ: Search ${resourceName} by name\n`;
1961
+ sql += `SELECT * FROM ${resourceName} WHERE name LIKE '%search%' ORDER BY name;\n\n`;
1962
+ }
1963
+
1964
+ if (fields.includes('category')) {
1965
+ sql += `-- READ: Get ${resourceName} by category\n`;
1966
+ sql += `SELECT * FROM ${resourceName} WHERE category = 'Category1' ORDER BY name;\n\n`;
1967
+ }
1968
+
1969
+ incomingRelations.forEach(rel => {
1970
+ const fkField = buildForeignKeyName(rel.parent);
1971
+ sql += `-- READ: Get ${resourceName} by ${rel.parent} relation\n`;
1972
+ sql += `SELECT * FROM ${resourceName} WHERE ${fkField} = 1 ORDER BY createdAt DESC;\n\n`;
1973
+ });
1974
+
1975
+ // UPDATE
1976
+ sql += `-- UPDATE: Update ${resourceName}\n`;
1977
+ sql += `UPDATE ${resourceName}\n`;
1978
+ sql += `SET ${updateSets || getDefaultUpdateSet(dbType)}\n`;
1979
+ sql += `WHERE id = 1;\n\n`;
1980
+
1981
+ // DELETE
1982
+ sql += `-- DELETE: Delete ${resourceName}\n`;
1983
+ sql += `DELETE FROM ${resourceName} WHERE id = 1;\n\n`;
1984
+ });
1985
+
1986
+ return sql;
1987
+ }
1988
+
1989
+ function generateSampleValue(fieldName) {
1990
+ if (fieldName.includes('email')) return "'user@example.com'";
1991
+ if (fieldName.endsWith('Id')) return '1';
1992
+ if (fieldName.includes('price')) return "99.99";
1993
+ if (fieldName.includes('stock') || fieldName.includes('quantity')) return "100";
1994
+ if (fieldName.includes('is') || fieldName.includes('has')) return "TRUE";
1995
+ if (fieldName.includes('role')) return "'user'";
1996
+ if (fieldName.includes('status')) return "'active'";
1997
+ if (fieldName.includes('category')) return "'Category1'";
1998
+ return `'Sample ${fieldName}'`;
1999
+ }
2000
+
2001
+ function generateJoinSQL(resources, dbType, apiAnalysis = { relationships: [] }) {
2002
+ let sql = `-- ============================================\n`;
2003
+ sql += `-- offbyt ${dbType.toUpperCase()} Relationships & JOINs\n`;
2004
+ sql += `-- Complex queries with table relationships\n`;
2005
+ sql += `-- ============================================\n\n`;
2006
+
2007
+ const relationships = (apiAnalysis && Array.isArray(apiAnalysis.relationships)) ? apiAnalysis.relationships : [];
2008
+
2009
+ relationships.forEach(rel => {
2010
+ const fkField = buildForeignKeyName(rel.parent);
2011
+ const normalizedParam = rel.viaParam.startsWith(':') ? rel.viaParam : `:${rel.viaParam}`;
2012
+ sql += `-- Nested endpoint relation: /api/${rel.parent}/${normalizedParam}/${rel.child}\n`;
2013
+ sql += `SELECT child.*, parent.id AS parentId\n`;
2014
+ sql += `FROM ${rel.child} child\n`;
2015
+ sql += `INNER JOIN ${rel.parent} parent ON child.${fkField} = parent.id\n`;
2016
+ sql += `WHERE parent.id = 1\n`;
2017
+ sql += `ORDER BY child.createdAt DESC;\n\n`;
2018
+
2019
+ sql += `-- Count ${rel.child} rows per ${rel.parent}\n`;
2020
+ sql += `SELECT parent.id, COUNT(child.id) AS ${rel.child}Count\n`;
2021
+ sql += `FROM ${rel.parent} parent\n`;
2022
+ sql += `LEFT JOIN ${rel.child} child ON child.${fkField} = parent.id\n`;
2023
+ sql += `GROUP BY parent.id\n`;
2024
+ sql += `ORDER BY parent.id DESC;\n\n`;
2025
+ });
2026
+
2027
+ // Legacy conversation joins (only when relationship table exists)
2028
+ if (resources.has('conversations') && resources.has('users') && resources.has('conversation_participants')) {
2029
+ sql += `-- Get conversations with participant details\n`;
2030
+ sql += `SELECT c.*, u.name as userName, u.email\n`;
2031
+ sql += `FROM conversations c\n`;
2032
+ sql += `INNER JOIN conversation_participants cp ON c.id = cp.conversationId\n`;
2033
+ sql += `INNER JOIN users u ON cp.userId = u.id\n`;
2034
+ sql += `WHERE c.id = 1;\n\n`;
2035
+
2036
+ sql += `-- Get user's all conversations\n`;
2037
+ sql += `SELECT c.*, COUNT(m.id) as messageCount\n`;
2038
+ sql += `FROM conversations c\n`;
2039
+ sql += `INNER JOIN conversation_participants cp ON c.id = cp.conversationId\n`;
2040
+ sql += `LEFT JOIN messages m ON c.id = m.conversationId\n`;
2041
+ sql += `WHERE cp.userId = 1\n`;
2042
+ sql += `GROUP BY c.id\n`;
2043
+ sql += `ORDER BY c.updatedAt DESC;\n\n`;
2044
+ }
2045
+
2046
+ // General JOIN example
2047
+ if (resources.size > 1) {
2048
+ const [first, second] = Array.from(resources.keys()).slice(0, 2);
2049
+ const fkField = buildForeignKeyName(first);
2050
+ sql += `-- Example JOIN between ${first} and ${second}\n`;
2051
+ sql += `SELECT a.*, b.*\n`;
2052
+ sql += `FROM ${first} a\n`;
2053
+ sql += `LEFT JOIN ${second} b ON a.id = b.${fkField}\n`;
2054
+ sql += `LIMIT 10;\n\n`;
2055
+ }
2056
+
2057
+ return sql;
2058
+ }
2059
+
2060
+ function generateSeedSQL(resources, dbType, apiAnalysis = { relationships: [] }) {
2061
+ let sql = `-- ============================================\n`;
2062
+ sql += `-- offbyt ${dbType.toUpperCase()} Sample Data\n`;
2063
+ sql += `-- Seed data for testing and development\n`;
2064
+ sql += `-- ============================================\n\n`;
2065
+
2066
+ sql += `-- Clear existing data (use with caution!)\n`;
2067
+ if (dbType === 'mysql') {
2068
+ sql += `SET FOREIGN_KEY_CHECKS = 0;\n`;
2069
+ }
2070
+
2071
+ resources.forEach((_, resourceName) => {
2072
+ sql += `DELETE FROM ${resourceName};\n`;
2073
+ });
2074
+
2075
+ if (dbType === 'mysql') {
2076
+ sql += `SET FOREIGN_KEY_CHECKS = 1;\n`;
2077
+ }
2078
+ sql += `\n`;
2079
+
2080
+ // Generate sample data for each resource
2081
+ resources.forEach((resourceInfo, resourceName) => {
2082
+ const fields = resourceInfo.fields || ['name'];
2083
+ const incomingRelations = getIncomingRelationships(apiAnalysis, resourceName);
2084
+ const relationFields = incomingRelations
2085
+ .map(rel => buildForeignKeyName(rel.parent))
2086
+ .filter(field => !fields.includes(field));
2087
+ const allInsertFields = [...fields, ...relationFields];
2088
+
2089
+ sql += `-- Insert sample ${resourceName}\n`;
2090
+ if (allInsertFields.length === 0) {
2091
+ for (let i = 0; i < 5; i++) {
2092
+ sql += `${getSeedInsertStatement(resourceName, dbType)}\n`;
2093
+ }
2094
+ sql += `\n`;
2095
+ return;
2096
+ }
2097
+
2098
+ sql += `INSERT INTO ${resourceName} (${allInsertFields.join(', ')}) VALUES\n`;
2099
+ for (let i = 1; i <= 5; i++) {
2100
+ const values = allInsertFields.map(f => generateSampleValue(f).replace('Sample', `Sample ${i}`)).join(', ');
2101
+ sql += `(${values})${i < 5 ? ',' : ';'}\n`;
2102
+ }
2103
+ sql += `\n`;
2104
+ });
2105
+
2106
+ return sql;
2107
+ }
2108
+
2109
+ function generateSQLReadme(dbType) {
2110
+ return `# SQL Files for ${dbType.toUpperCase()}
2111
+
2112
+ ## Files Generated
2113
+
2114
+ - **01_schema.sql** - Database schema with CREATE TABLE statements
2115
+ - **02_crud_operations.sql** - All CRUD operation queries
2116
+ - **03_relationships_joins.sql** - JOIN queries for relationships
2117
+ - **04_seed_data.sql** - Sample data for testing
2118
+
2119
+ ## How to Use
2120
+
2121
+ ### MySQL
2122
+ \`\`\`bash
2123
+ mysql -u root -p database_name < sql/01_schema.sql
2124
+ mysql -u root -p database_name < sql/04_seed_data.sql
2125
+ \`\`\`
2126
+
2127
+ ### PostgreSQL
2128
+ \`\`\`bash
2129
+ psql -U postgres -d database_name -f sql/01_schema.sql
2130
+ psql -U postgres -d database_name -f sql/04_seed_data.sql
2131
+ \`\`\`
2132
+
2133
+ ### SQLite
2134
+ \`\`\`bash
2135
+ sqlite3 database.db < sql/01_schema.sql
2136
+ sqlite3 database.db < sql/04_seed_data.sql
2137
+ \`\`\`
2138
+
2139
+ ## Execute in Order
2140
+
2141
+ 1. First run \`01_schema.sql\` to create tables
2142
+ 2. Then run \`04_seed_data.sql\` to insert sample data
2143
+ 3. Use \`02_crud_operations.sql\` as query reference
2144
+ 4. Use \`03_relationships_joins.sql\` for complex queries
2145
+ `;
2146
+ }
2147
+
2148
+ function createSampleResources(backendPath, config) {
2149
+ // Sample User Model
2150
+ let userModel = '';
2151
+
2152
+ if (config.database === 'mongodb') {
2153
+ userModel = `import mongoose from 'mongoose';
2154
+
2155
+ const userSchema = new mongoose.Schema({
2156
+ name: { type: String, required: true },
2157
+ email: { type: String, required: true, unique: true },
2158
+ password: { type: String, required: true },
2159
+ createdAt: { type: Date, default: Date.now },
2160
+ updatedAt: { type: Date, default: Date.now }
2161
+ });
2162
+
2163
+ export const User = mongoose.model('User', userSchema);`;
2164
+ } else {
2165
+ userModel = `import { DataTypes } from 'sequelize';
2166
+ import { sequelize } from '../config/database.js';
2167
+
2168
+ export const User = sequelize.define('User', {
2169
+ name: { type: DataTypes.STRING, allowNull: false },
2170
+ email: { type: DataTypes.STRING, allowNull: false, unique: true },
2171
+ password: { type: DataTypes.STRING, allowNull: false },
2172
+ }, {
2173
+ timestamps: true
2174
+ });`;
2175
+ }
2176
+
2177
+ fs.writeFileSync(path.join(backendPath, 'models', 'User.js'), userModel);
2178
+
2179
+ // Sample Route
2180
+ const sampleRoute = `import express from 'express';
2181
+
2182
+ const router = express.Router();
2183
+
2184
+ router.get('/', (req, res) => {
2185
+ res.json({ message: 'Welcome to offbyt API' });
2186
+ });
2187
+
2188
+ export default router;`;
2189
+
2190
+ fs.writeFileSync(path.join(backendPath, 'routes', 'index.js'), sampleRoute);
2191
+
2192
+ // Sample Controller
2193
+ const sampleController = `export const getUsers = async (req, res) => {
2194
+ try {
2195
+ // TODO: Implement user retrieval logic
2196
+ res.json({ users: [] });
2197
+ } catch (error) {
2198
+ res.status(500).json({ error: error.message });
2199
+ }
2200
+ };
2201
+
2202
+ export const createUser = async (req, res) => {
2203
+ try {
2204
+ // TODO: Implement user creation logic
2205
+ res.status(201).json({ message: 'User created' });
2206
+ } catch (error) {
2207
+ res.status(500).json({ error: error.message });
2208
+ }
2209
+ };`;
2210
+
2211
+ fs.writeFileSync(path.join(backendPath, 'controllers', 'userController.js'), sampleController);
2212
+
2213
+ // README
2214
+ const readme = `# offbyt Generated Backend
2215
+
2216
+ This is a production-ready backend generated by offbyt.
2217
+
2218
+ ## Configuration
2219
+ - Database: ${config.database}
2220
+ - Framework: ${config.framework}
2221
+ - Realtime Sockets: ${config.enableSocket ? 'Yes' : 'No'}
2222
+ - Authentication: ${config.enableAuth ? 'Yes' : 'No'}
2223
+
2224
+ ## Getting Started
2225
+
2226
+ \`\`\`bash
2227
+ # Install dependencies
2228
+ npm install
2229
+
2230
+ # Update .env file
2231
+ cp ../.env .env
2232
+
2233
+ # Start development server
2234
+ npm run dev
2235
+ \`\`\`
2236
+
2237
+ ## Project Structure
2238
+ - \`config/\` - Database and app configuration
2239
+ - \`middleware/\` - Express middleware
2240
+ - \`models/\` - Database models
2241
+ - \`controllers/\` - Business logic controllers
2242
+ - \`routes/\` - API route definitions
2243
+ - \`services/\` - Service layer
2244
+ - \`utils/\` - Utility functions
2245
+ - \`validators/\` - Request validation schemas
2246
+
2247
+ ## Available Scripts
2248
+ - \`npm run dev\` - Start development server
2249
+ - \`npm start\` - Start production server
2250
+
2251
+ ## Environment Variables
2252
+ See \`../.env\` for required environment variables.
2253
+
2254
+ Generated by offbyt âš¡`;
2255
+
2256
+ fs.writeFileSync(path.join(backendPath, 'README.md'), readme);
2257
+ }
2258
+