create-backlist 6.0.7 → 6.0.9

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 (46) hide show
  1. package/bin/index.js +141 -0
  2. package/package.json +4 -10
  3. package/src/analyzer.js +104 -315
  4. package/src/generators/dotnet.js +94 -120
  5. package/src/generators/java.js +109 -157
  6. package/src/generators/node.js +157 -261
  7. package/src/generators/template.js +2 -38
  8. package/src/templates/dotnet/partials/Controller.cs.ejs +14 -7
  9. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +2 -7
  10. package/src/templates/java-spring/partials/AuthController.java.ejs +10 -23
  11. package/src/templates/java-spring/partials/Controller.java.ejs +6 -17
  12. package/src/templates/java-spring/partials/Dockerfile.ejs +1 -6
  13. package/src/templates/java-spring/partials/Entity.java.ejs +5 -15
  14. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +7 -30
  15. package/src/templates/java-spring/partials/JwtService.java.ejs +10 -38
  16. package/src/templates/java-spring/partials/Repository.java.ejs +1 -10
  17. package/src/templates/java-spring/partials/Service.java.ejs +7 -45
  18. package/src/templates/java-spring/partials/User.java.ejs +4 -17
  19. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +4 -10
  20. package/src/templates/java-spring/partials/UserRepository.java.ejs +0 -8
  21. package/src/templates/java-spring/partials/docker-compose.yml.ejs +8 -16
  22. package/src/templates/node-ts-express/base/server.ts +6 -13
  23. package/src/templates/node-ts-express/base/tsconfig.json +3 -13
  24. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +7 -17
  25. package/src/templates/node-ts-express/partials/App.test.ts.ejs +26 -49
  26. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +62 -56
  27. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +10 -21
  28. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  29. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  30. package/src/templates/node-ts-express/partials/Dockerfile.ejs +11 -9
  31. package/src/templates/node-ts-express/partials/Model.cs.ejs +7 -25
  32. package/src/templates/node-ts-express/partials/Model.ts.ejs +12 -20
  33. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +55 -72
  34. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +12 -27
  35. package/src/templates/node-ts-express/partials/README.md.ejs +12 -9
  36. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +64 -44
  37. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +16 -31
  38. package/src/templates/node-ts-express/partials/package.json.ejs +1 -3
  39. package/src/templates/node-ts-express/partials/routes.ts.ejs +24 -35
  40. package/src/utils.js +5 -17
  41. package/bin/backlist.js +0 -228
  42. package/src/db/prisma.ts +0 -4
  43. package/src/scanner/analyzeFrontend.js +0 -146
  44. package/src/scanner/index.js +0 -99
  45. package/src/templates/dotnet/partials/Dto.cs.ejs +0 -8
  46. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +0 -4
@@ -7,70 +7,96 @@ const { analyzeFrontend } = require('../analyzer');
7
7
  const { renderAndWrite, getTemplatePath } = require('./template');
8
8
 
9
9
  async function generateNodeProject(options) {
10
- const {
11
- projectDir,
12
- projectName,
13
- frontendSrcDir,
14
- dbType,
15
- addAuth,
16
- addSeeder,
17
- extraFeatures = [],
18
- } = options;
19
-
10
+ // v5.0: Destructure all new options
11
+ const { projectDir, projectName, frontendSrcDir, dbType, addAuth, addSeeder, extraFeatures = [] } = options;
20
12
  const port = 8000;
21
13
 
22
14
  try {
23
15
  // --- Step 1: Analyze Frontend ---
24
- console.log(chalk.blue(' -> Analyzing frontend for API endpoints (AST)...'));
25
- const endpoints = await analyzeFrontend(frontendSrcDir);
26
- if (endpoints.length > 0) console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
27
- else console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
16
+ console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
17
+ let endpoints = await analyzeFrontend(frontendSrcDir);
18
+
19
+ if (endpoints.length > 0) {
20
+ console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
21
+
22
+ // ============================================================
23
+ // 🔥 FIX START: Sanitizing Endpoints Logic
24
+ // ============================================================
25
+ endpoints = endpoints.map(ep => {
26
+ // 1. Path එක සුද්ද කිරීම (/api/v1/users -> ['users'])
27
+ // 'api', 'v1', හිස්තැන් අයින් කරනවා
28
+ const parts = ep.path.split('/').filter(part => part !== '' && part !== 'api' && part !== 'v1');
29
+
30
+ // Resource එක හොයාගැනීම (e.g., 'users')
31
+ let resource = parts[0] || 'Default';
32
+
33
+ // 2. Controller Name එක හැදීම (CamelCase: 'users' -> 'Users')
34
+ // Special Case: resource එක 'auth' නම් Controller එක 'Auth'
35
+ let controllerName = resource.charAt(0).toUpperCase() + resource.slice(1);
36
+
37
+ // 3. Function Names හරියටම මැප් කිරීම
38
+ let functionName = '';
39
+
40
+ // --- AUTH LOGIC ---
41
+ if (controllerName.toLowerCase() === 'auth') {
42
+ if (ep.path.includes('login')) functionName = 'loginUser';
43
+ else if (ep.path.includes('register')) functionName = 'registerUser';
44
+ else functionName = 'authAction'; // fallback
45
+ }
46
+ // --- GENERAL RESOURCES LOGIC ---
47
+ else {
48
+ // 'users' -> 'User' (Singular form for function names like createUser)
49
+ // 'users' -> 'Users' (Plural form for lists)
50
+ const singularName = resource.endsWith('s') ? resource.slice(0, -1) : resource;
51
+ const pluralName = resource.endsWith('s') ? resource : resource + 's';
52
+
53
+ const pascalSingular = singularName.charAt(0).toUpperCase() + singularName.slice(1);
54
+ const pascalPlural = pluralName.charAt(0).toUpperCase() + pluralName.slice(1);
55
+
56
+ if (ep.method === 'GET') {
57
+ if (ep.path.includes(':id')) functionName = `get${pascalSingular}ById`;
58
+ else functionName = `getAll${pascalPlural}`; // Fixes 'getAllUserss'
59
+ } else if (ep.method === 'POST') {
60
+ functionName = `create${pascalSingular}`;
61
+ } else if (ep.method === 'PUT') {
62
+ functionName = `update${pascalSingular}ById`;
63
+ } else if (ep.method === 'DELETE') {
64
+ functionName = `delete${pascalSingular}ById`;
65
+ } else {
66
+ functionName = `${ep.method.toLowerCase()}${pascalPlural}`;
67
+ }
68
+ }
69
+
70
+ // Update the endpoint object
71
+ return {
72
+ ...ep,
73
+ controllerName,
74
+ functionName // ejs file එකේදි <%= ep.functionName %> කියලා පාවිච්චි කරන්න පුළුවන් දැන්
75
+ };
76
+ });
77
+ // ============================================================
78
+ // 🔥 FIX END
79
+ // ============================================================
28
80
 
29
- // Group endpoints by controller
30
- const endpointsByController = new Map();
31
- for (const ep of endpoints) {
32
- const c = ep && ep.controllerName ? ep.controllerName : 'Default';
33
- if (c === 'Default') continue;
34
- if (!endpointsByController.has(c)) endpointsByController.set(c, []);
35
- endpointsByController.get(c).push(ep);
81
+ } else {
82
+ console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
36
83
  }
37
84
 
38
- // --- Step 2: Identify Models to Generate (merge fields per controller) ---
85
+ // --- Step 2: Identify Models to Generate ---
39
86
  const modelsToGenerate = new Map();
40
-
41
- for (const [controllerName, eps] of endpointsByController.entries()) {
42
- const fieldMap = new Map();
43
-
44
- for (const ep of eps) {
45
- const fields = ep?.requestBody?.fields || ep?.schemaFields;
46
- if (!fields) continue;
47
-
48
- for (const [key, type] of Object.entries(fields)) {
49
- fieldMap.set(key, { name: key, type, isUnique: key === 'email' });
50
- }
51
- }
52
-
53
- if (fieldMap.size > 0) {
54
- modelsToGenerate.set(controllerName, {
55
- name: controllerName,
56
- fields: Array.from(fieldMap.values()),
87
+ endpoints.forEach(ep => {
88
+ // Default සහ Auth වලට Model හදන්න ඕන නෑ (Auth එකට User Model එක යටින් හදනවා)
89
+ if (ep.schemaFields && ep.controllerName !== 'Default' && ep.controllerName !== 'Auth' && !modelsToGenerate.has(ep.controllerName)) {
90
+ modelsToGenerate.set(ep.controllerName, {
91
+ name: ep.controllerName,
92
+ fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type, isUnique: key === 'email' }))
57
93
  });
58
94
  }
59
- }
95
+ });
60
96
 
61
- // Ensure User model if auth enabled
62
97
  if (addAuth && !modelsToGenerate.has('User')) {
63
98
  console.log(chalk.yellow(' -> Authentication requires a "User" model. Creating a default one.'));
64
- modelsToGenerate.set('User', {
65
- name: 'User',
66
- fields: [
67
- { name: 'name', type: 'string' },
68
- { name: 'email', type: 'string', isUnique: true },
69
- { name: 'password', type: 'string' },
70
- ],
71
- });
72
- // Also ensure controller exists so routes/controller can generate if frontend didn't call /api/users
73
- if (!endpointsByController.has('User')) endpointsByController.set('User', []);
99
+ modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String', isUnique: true }, { name: 'password', type: 'String' }] });
74
100
  }
75
101
 
76
102
  // --- Step 3: Base Scaffolding ---
@@ -79,38 +105,28 @@ async function generateNodeProject(options) {
79
105
  await fs.ensureDir(destSrcDir);
80
106
  await fs.copy(getTemplatePath('node-ts-express/base/server.ts'), path.join(destSrcDir, 'server.ts'));
81
107
  await fs.copy(getTemplatePath('node-ts-express/base/tsconfig.json'), path.join(projectDir, 'tsconfig.json'));
82
-
108
+
83
109
  // --- Step 4: Prepare and Write package.json ---
84
- const packageJsonContent = JSON.parse(
85
- await ejs.renderFile(getTemplatePath('node-ts-express/partials/package.json.ejs'), { projectName })
86
- );
87
-
88
- if (dbType === 'mongoose') {
89
- packageJsonContent.dependencies['mongoose'] = '^7.6.3';
90
- }
91
-
110
+ const packageJsonContent = JSON.parse(await ejs.renderFile(getTemplatePath('node-ts-express/partials/package.json.ejs'), { projectName }));
111
+
112
+ if (dbType === 'mongoose') packageJsonContent.dependencies['mongoose'] = '^7.6.3';
92
113
  if (dbType === 'prisma') {
93
114
  packageJsonContent.dependencies['@prisma/client'] = '^5.6.0';
94
115
  packageJsonContent.devDependencies['prisma'] = '^5.6.0';
95
- // prisma seed entry is fine, but if you do mongoose seeder only, don't point prisma seed to scripts/seeder.ts
96
- packageJsonContent.prisma = { seed: `ts-node prisma/seed.ts` };
116
+ packageJsonContent.prisma = { seed: `ts-node ${addSeeder ? 'scripts/seeder.ts' : 'prisma/seed.ts'}` };
97
117
  }
98
-
99
118
  if (addAuth) {
100
119
  packageJsonContent.dependencies['jsonwebtoken'] = '^9.0.2';
101
120
  packageJsonContent.dependencies['bcryptjs'] = '^2.4.3';
102
121
  packageJsonContent.devDependencies['@types/jsonwebtoken'] = '^9.0.5';
103
122
  packageJsonContent.devDependencies['@types/bcryptjs'] = '^2.4.6';
104
123
  }
105
-
106
- // Seeder deps only if mongoose seeder enabled
107
- if (addSeeder && dbType === 'mongoose') {
124
+ if (addSeeder) {
108
125
  packageJsonContent.devDependencies['@faker-js/faker'] = '^8.3.1';
109
126
  if (!packageJsonContent.dependencies['chalk']) packageJsonContent.dependencies['chalk'] = '^4.1.2';
110
127
  packageJsonContent.scripts['seed'] = `ts-node scripts/seeder.ts`;
111
128
  packageJsonContent.scripts['destroy'] = `ts-node scripts/seeder.ts -d`;
112
129
  }
113
-
114
130
  if (extraFeatures.includes('testing')) {
115
131
  packageJsonContent.devDependencies['jest'] = '^29.7.0';
116
132
  packageJsonContent.devDependencies['supertest'] = '^6.3.3';
@@ -119,255 +135,135 @@ async function generateNodeProject(options) {
119
135
  packageJsonContent.devDependencies['ts-jest'] = '^29.1.1';
120
136
  packageJsonContent.scripts['test'] = 'jest --detectOpenHandles --forceExit';
121
137
  }
122
-
123
138
  if (extraFeatures.includes('swagger')) {
124
139
  packageJsonContent.dependencies['swagger-ui-express'] = '^5.0.0';
125
140
  packageJsonContent.dependencies['swagger-jsdoc'] = '^6.2.8';
126
141
  packageJsonContent.devDependencies['@types/swagger-ui-express'] = '^4.1.6';
127
142
  }
128
-
129
143
  await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
130
-
131
- // --- Step 5: Generate DB-specific files & Models ---
132
- await fs.ensureDir(path.join(destSrcDir, 'controllers'));
133
-
144
+
145
+ // --- Step 5: Generate DB-specific files & Controllers ---
134
146
  if (modelsToGenerate.size > 0) {
135
- if (dbType === 'mongoose') {
136
- console.log(chalk.blue(' -> Generating Mongoose models...'));
137
- await fs.ensureDir(path.join(destSrcDir, 'models'));
138
-
139
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
140
- // normalize schema fields to simple types (string/number/boolean)
141
- const schema = {};
142
- for (const field of modelData.fields || []) {
143
- const t = String(field.type || 'string').toLowerCase();
144
- schema[field.name] = (t === 'number' || t === 'boolean') ? t : 'string';
145
- }
146
-
147
- await renderAndWrite(
148
- getTemplatePath('node-ts-express/partials/Model.ts.ejs'),
149
- path.join(destSrcDir, 'models', `${modelName}.model.ts`),
150
- { modelName, schema, projectName }
151
- );
147
+ await fs.ensureDir(path.join(destSrcDir, 'controllers'));
148
+ if (dbType === 'mongoose') {
149
+ console.log(chalk.blue(' -> Generating Mongoose models and controllers...'));
150
+ await fs.ensureDir(path.join(destSrcDir, 'models'));
151
+ for (const [modelName, modelData] of modelsToGenerate.entries()) {
152
+ const schema = modelData.fields.reduce((acc, field) => { acc[field.name] = field.type; return acc; }, {});
153
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/Model.ts.ejs'), path.join(destSrcDir, 'models', `${modelName}.model.ts`), { modelName, schema, projectName });
154
+ }
155
+ } else if (dbType === 'prisma') {
156
+ console.log(chalk.blue(' -> Generating Prisma schema...'));
157
+ await fs.ensureDir(path.join(projectDir, 'prisma'));
158
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/PrismaSchema.prisma.ejs'), path.join(projectDir, 'prisma', 'schema.prisma'), { modelsToGenerate: Array.from(modelsToGenerate.values()) });
159
+ }
160
+ console.log(chalk.blue(' -> Generating controllers...'));
161
+ for (const [modelName] of modelsToGenerate.entries()) {
162
+ const templateFile = dbType === 'mongoose' ? 'Controller.ts.ejs' : 'PrismaController.ts.ejs';
163
+ // මෙතන Controller එක හදද්දි Auth එක Skip කරනවා (මොකද ඒක පහළ වෙනම හදනවා)
164
+ if (modelName !== 'Auth') {
165
+ await renderAndWrite(getTemplatePath(`node-ts-express/partials/${templateFile}`), path.join(destSrcDir, 'controllers', `${modelName}.controller.ts`), { modelName, projectName });
166
+ }
152
167
  }
153
- }
154
-
155
- if (dbType === 'prisma') {
156
- console.log(chalk.blue(' -> Generating Prisma schema + client...'));
157
- await fs.ensureDir(path.join(projectDir, 'prisma'));
158
-
159
- await renderAndWrite(
160
- getTemplatePath('node-ts-express/partials/PrismaSchema.prisma.ejs'),
161
- path.join(projectDir, 'prisma', 'schema.prisma'),
162
- { modelsToGenerate: Array.from(modelsToGenerate.values()), projectName }
163
- );
164
-
165
- // Prisma client singleton
166
- await fs.ensureDir(path.join(destSrcDir, 'db'));
167
- await renderAndWrite(
168
- getTemplatePath('node-ts-express/partials/prismaClient.ts.ejs'),
169
- path.join(destSrcDir, 'db', 'prisma.ts'),
170
- { projectName }
171
- );
172
- }
173
- }
174
-
175
- // --- Step 5b: Generate Controllers from Endpoints (AST-driven) ---
176
- console.log(chalk.blue(' -> Generating controllers (from endpoints)...'));
177
- for (const [controllerName, controllerEndpoints] of endpointsByController.entries()) {
178
- const tpl =
179
- dbType === 'prisma'
180
- ? 'node-ts-express/partials/PrismaController.FromEndpoints.ts.ejs'
181
- : 'node-ts-express/partials/Controller.FromEndpoints.ts.ejs';
182
-
183
- await renderAndWrite(
184
- getTemplatePath(tpl),
185
- path.join(destSrcDir, 'controllers', `${controllerName}.controller.ts`),
186
- { controllerName, endpoints: controllerEndpoints, projectName, dbType }
187
- );
188
168
  }
189
-
190
- // --- Step 6: Authentication Boilerplate ---
169
+
170
+ // --- Step 6: Generate Authentication Boilerplate ---
191
171
  if (addAuth) {
192
- console.log(chalk.blue(' -> Generating authentication boilerplate...'));
193
- await fs.ensureDir(path.join(destSrcDir, 'routes'));
194
- await fs.ensureDir(path.join(destSrcDir, 'middleware'));
195
-
196
- await renderAndWrite(
197
- getTemplatePath('node-ts-express/partials/Auth.controller.ts.ejs'),
198
- path.join(destSrcDir, 'controllers', 'Auth.controller.ts'),
199
- { dbType, projectName }
200
- );
201
-
202
- await renderAndWrite(
203
- getTemplatePath('node-ts-express/partials/Auth.routes.ts.ejs'),
204
- path.join(destSrcDir, 'routes', 'Auth.routes.ts'),
205
- { projectName }
206
- );
207
-
208
- await renderAndWrite(
209
- getTemplatePath('node-ts-express/partials/Auth.middleware.ts.ejs'),
210
- path.join(destSrcDir, 'middleware', 'Auth.middleware.ts'),
211
- { projectName }
212
- );
213
-
214
- // For mongoose: inject password hashing hook into User.model.ts if exists
215
- if (dbType === 'mongoose') {
216
- const userModelPath = path.join(destSrcDir, 'models', 'User.model.ts');
217
- if (await fs.pathExists(userModelPath)) {
218
- let userModelContent = await fs.readFile(userModelPath, 'utf-8');
219
-
220
- if (!userModelContent.includes(`import bcrypt`)) {
221
- userModelContent = userModelContent.replace(
222
- `import mongoose, { Schema, Document } from 'mongoose';`,
223
- `import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';`
224
- );
225
- }
226
-
227
- if (!userModelContent.includes(`pre('save'`)) {
228
- const preSaveHook =
229
- `\n// Hash password before saving\n` +
230
- `UserSchema.pre('save', async function(next) {\n` +
231
- ` if (!this.isModified('password')) { return next(); }\n` +
232
- ` const salt = await bcrypt.genSalt(10);\n` +
233
- ` this.password = await bcrypt.hash(this.password, salt);\n` +
234
- ` next();\n` +
235
- `});\n`;
236
-
237
- userModelContent = userModelContent.replace(
238
- `// Create and export the Model`,
239
- `${preSaveHook}\n// Create and export the Model`
240
- );
241
- }
242
-
243
- await fs.writeFile(userModelPath, userModelContent);
172
+ console.log(chalk.blue(' -> Generating authentication boilerplate...'));
173
+ await fs.ensureDir(path.join(destSrcDir, 'routes'));
174
+ await fs.ensureDir(path.join(destSrcDir, 'middleware'));
175
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.controller.ts.ejs'), path.join(destSrcDir, 'controllers', 'Auth.controller.ts'), { dbType, projectName });
176
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.routes.ts.ejs'), path.join(destSrcDir, 'routes', 'Auth.routes.ts'), { projectName });
177
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.middleware.ts.ejs'), path.join(destSrcDir, 'middleware', 'Auth.middleware.ts'), { projectName });
178
+
179
+ if (dbType === 'mongoose') {
180
+ const userModelPath = path.join(destSrcDir, 'models', 'User.model.ts');
181
+ if (await fs.pathExists(userModelPath)) {
182
+ let userModelContent = await fs.readFile(userModelPath, 'utf-8');
183
+ if (!userModelContent.includes('bcryptjs')) {
184
+ userModelContent = userModelContent.replace(`import mongoose, { Schema, Document } from 'mongoose';`, `import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';`);
185
+ const preSaveHook = `\n// Hash password before saving\nUserSchema.pre('save', async function(next) {\n if (!this.isModified('password')) { return next(); }\n const salt = await bcrypt.genSalt(10);\n this.password = await bcrypt.hash(this.password, salt);\n next();\n});\n`;
186
+ userModelContent = userModelContent.replace(`// Create and export the Model`, `${preSaveHook}\n// Create and export the Model`);
187
+ await fs.writeFile(userModelPath, userModelContent);
188
+ }
189
+ }
244
190
  }
245
- }
246
191
  }
247
192
 
248
- // --- Step 7: Seeder Script (mongoose only) ---
249
- if (addSeeder && dbType === 'mongoose') {
250
- console.log(chalk.blue(' -> Generating database seeder script (mongoose)...'));
193
+ // --- Step 7: Generate Seeder Script ---
194
+ if (addSeeder) {
195
+ console.log(chalk.blue(' -> Generating database seeder script...'));
251
196
  await fs.ensureDir(path.join(projectDir, 'scripts'));
252
- await renderAndWrite(
253
- getTemplatePath('node-ts-express/partials/Seeder.ts.ejs'),
254
- path.join(projectDir, 'scripts', 'seeder.ts'),
255
- { projectName }
256
- );
197
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/Seeder.ts.ejs'), path.join(projectDir, 'scripts', 'seeder.ts'), { projectName });
257
198
  }
258
199
 
259
- // --- Step 8: Extra Features ---
200
+ // --- Step 8: Generate Extra Features ---
260
201
  if (extraFeatures.includes('docker')) {
261
202
  console.log(chalk.blue(' -> Generating Docker files...'));
262
- await renderAndWrite(
263
- getTemplatePath('node-ts-express/partials/Dockerfile.ejs'),
264
- path.join(projectDir, 'Dockerfile'),
265
- { dbType, port }
266
- );
267
- await renderAndWrite(
268
- getTemplatePath('node-ts-express/partials/docker-compose.yml.ejs'),
269
- path.join(projectDir, 'docker-compose.yml'),
270
- { projectName, dbType, port }
271
- );
203
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), { dbType, port });
204
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName, dbType, port });
272
205
  }
273
-
274
206
  if (extraFeatures.includes('swagger')) {
275
207
  console.log(chalk.blue(' -> Generating API documentation setup...'));
276
208
  await fs.ensureDir(path.join(destSrcDir, 'utils'));
277
- await renderAndWrite(
278
- getTemplatePath('node-ts-express/partials/ApiDocs.ts.ejs'),
279
- path.join(destSrcDir, 'utils', 'swagger.ts'),
280
- { projectName, port, addAuth }
281
- );
209
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/ApiDocs.ts.ejs'), path.join(destSrcDir, 'utils', 'swagger.ts'), { projectName, port });
282
210
  }
283
-
284
211
  if (extraFeatures.includes('testing')) {
285
212
  console.log(chalk.blue(' -> Generating testing boilerplate...'));
286
- const jestConfig =
287
- `/** @type {import('ts-jest').JestConfigWithTsJest} */\n` +
288
- `module.exports = {\n` +
289
- ` preset: 'ts-jest',\n` +
290
- ` testEnvironment: 'node',\n` +
291
- ` verbose: true,\n` +
292
- `};\n`;
213
+ const jestConfig = `/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n verbose: true,\n};`;
293
214
  await fs.writeFile(path.join(projectDir, 'jest.config.js'), jestConfig);
294
-
295
215
  await fs.ensureDir(path.join(projectDir, 'src', '__tests__'));
296
- await renderAndWrite(
297
- getTemplatePath('node-ts-express/partials/App.test.ts.ejs'),
298
- path.join(projectDir, 'src', '__tests__', 'api.test.ts'),
299
- { addAuth, endpoints }
300
- );
216
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/App.test.ts.ejs'), path.join(projectDir, 'src', '__tests__', 'api.test.ts'), { addAuth });
301
217
  }
302
218
 
303
- // --- Step 9: Generate Main Route File & Inject into server.ts ---
304
- await renderAndWrite(
305
- getTemplatePath('node-ts-express/partials/routes.ts.ejs'),
306
- path.join(destSrcDir, 'routes.ts'),
307
- { endpoints, addAuth, dbType }
308
- );
309
-
219
+ // --- Step 9: Generate Main Route File & Inject Logic into Server ---
220
+ // Updated endpoints passed here
221
+ await renderAndWrite(getTemplatePath('node-ts-express/partials/routes.ts.ejs'), path.join(destSrcDir, 'routes.ts'), { endpoints, addAuth, dbType });
222
+
310
223
  let serverFileContent = await fs.readFile(path.join(destSrcDir, 'server.ts'), 'utf-8');
224
+ let dbConnectionCode = '', swaggerInjector = '', authRoutesInjector = '';
311
225
 
312
- // Mongoose db connect injection only; prisma uses src/db/prisma.ts
313
- let dbConnectionCode = '';
314
226
  if (dbType === 'mongoose') {
315
- dbConnectionCode =
316
- `\n// --- Database Connection ---\n` +
317
- `import mongoose from 'mongoose';\n` +
318
- `const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';\n` +
319
- `mongoose.connect(MONGO_URI)\n` +
320
- ` .then(() => console.log('MongoDB Connected...'))\n` +
321
- ` .catch(err => console.error(err));\n` +
322
- `// -------------------------\n`;
227
+ dbConnectionCode = `\n// --- Database Connection ---\nimport mongoose from 'mongoose';\nconst MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/${projectName}';\nmongoose.connect(MONGO_URI).then(() => console.log('MongoDB Connected...')).catch(err => console.error(err));\n// -------------------------\n`;
228
+ } else if (dbType === 'prisma') {
229
+ dbConnectionCode = `\nimport { PrismaClient } from '@prisma/client';\nexport const prisma = new PrismaClient();\n`;
323
230
  }
324
-
325
- let swaggerInjector = '';
326
231
  if (extraFeatures.includes('swagger')) {
327
- swaggerInjector = `\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n`;
232
+ swaggerInjector = `\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n`;
328
233
  }
329
-
330
- let authRoutesInjector = '';
331
234
  if (addAuth) {
332
- authRoutesInjector = `import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n`;
235
+ authRoutesInjector = `import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n`;
333
236
  }
334
237
 
335
238
  serverFileContent = serverFileContent
336
- .replace('dotenv.config();', `dotenv.config();${dbConnectionCode}`)
337
- .replace('// INJECT:ROUTES', `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes);\n`);
338
-
339
- // place swagger setup before listen
239
+ .replace("dotenv.config();", `dotenv.config();${dbConnectionCode}`)
240
+ .replace('// INJECT:ROUTES', `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes);`);
241
+
340
242
  const listenRegex = /(app\.listen\()/;
341
243
  serverFileContent = serverFileContent.replace(listenRegex, `${swaggerInjector}\n$1`);
342
-
343
244
  await fs.writeFile(path.join(destSrcDir, 'server.ts'), serverFileContent);
344
245
 
345
- // --- Step 10: Install Dependencies & Post-install ---
246
+ // --- Step 10: Install Dependencies & Run Post-install Scripts ---
346
247
  console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
347
248
  await execa('npm', ['install'], { cwd: projectDir });
348
-
349
249
  if (dbType === 'prisma') {
350
250
  console.log(chalk.blue(' -> Running `prisma generate`...'));
351
251
  await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
352
252
  }
353
-
354
- // --- Step 11: Final Files (.env.example) ---
253
+
254
+ // --- Step 11: Generate Final Files (.env.example) ---
355
255
  let envContent = `PORT=${port}\n`;
356
-
357
256
  if (dbType === 'mongoose') {
358
- envContent += `MONGO_URI=mongodb://127.0.0.1:27017/${projectName}\n`;
257
+ envContent += `MONGO_URI=mongodb://root:example@db:27017/${projectName}?authSource=admin\n`;
359
258
  } else if (dbType === 'prisma') {
360
- envContent += `DATABASE_URL="postgresql://user:password@localhost:5432/${projectName}?schema=public"\n`;
259
+ envContent += `DATABASE_URL="postgresql://user:password@db:5432/${projectName}?schema=public"\n`;
361
260
  }
362
-
363
- if (addAuth) envContent += `JWT_SECRET=change_me_long_secret_change_me_long_secret\n`;
364
-
365
- if (extraFeatures.includes('docker') && dbType === 'prisma') {
366
- envContent += `\n# Docker-compose credentials\nDB_USER=postgres\nDB_PASSWORD=password\nDB_NAME=${projectName}\n`;
261
+ if (addAuth) envContent += `JWT_SECRET=your_super_secret_jwt_key_12345\n`;
262
+ if (extraFeatures.includes('docker')) {
263
+ envContent += `\n# Docker-compose credentials (used in docker-compose.yml)\nDB_USER=user\nDB_PASSWORD=password\nDB_NAME=${projectName}`;
367
264
  }
368
-
369
265
  await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
370
-
266
+
371
267
  } catch (error) {
372
268
  throw error;
373
269
  }
@@ -2,47 +2,11 @@ const fs = require('fs-extra');
2
2
  const ejs = require('ejs');
3
3
  const path = require('path');
4
4
 
5
- function pascalCase(str) {
6
- return String(str || '')
7
- .replace(/[-_]+(.)/g, (_, c) => c.toUpperCase())
8
- .replace(/^\w/, c => c.toUpperCase())
9
- .replace(/[^a-zA-Z0-9]/g, '');
10
- }
11
-
12
- function camelCase(str) {
13
- const p = pascalCase(str);
14
- return p ? p.charAt(0).toLowerCase() + p.slice(1) : '';
15
- }
16
-
17
- function mapTsType(t) {
18
- const x = String(t || '').toLowerCase();
19
- if (x === 'number' || x === 'int' || x === 'integer' || x === 'float' || x === 'double') return 'number';
20
- if (x === 'boolean' || x === 'bool') return 'boolean';
21
- return 'string';
22
- }
23
-
24
- function mapMongooseType(t) {
25
- const x = String(t || '').toLowerCase();
26
- if (x === 'number' || x === 'int' || x === 'integer' || x === 'float' || x === 'double') return 'Number';
27
- if (x === 'boolean' || x === 'bool') return 'Boolean';
28
- return 'String';
29
- }
30
-
31
5
  async function renderAndWrite(templatePath, outPath, data) {
32
- const helpers = { pascalCase, camelCase, mapTsType, mapMongooseType };
33
-
34
6
  try {
35
7
  const tpl = await fs.readFile(templatePath, 'utf-8');
36
- const code = ejs.render(tpl, { ...(data || {}), ...helpers }, { filename: templatePath });
37
-
38
- // avoid rewriting identical content (useful in watch mode)
39
- const exists = await fs.pathExists(outPath);
40
- if (exists) {
41
- const current = await fs.readFile(outPath, 'utf-8');
42
- if (current === code) return;
43
- }
44
-
45
- await fs.outputFile(outPath, code.endsWith('\n') ? code : code + '\n');
8
+ const code = ejs.render(tpl, data || {}, { filename: templatePath }); // filename helps with EJS errors
9
+ await fs.outputFile(outPath, code);
46
10
  } catch (err) {
47
11
  console.error('EJS render failed for:', templatePath);
48
12
  console.error('Data keys:', Object.keys(data || {}));
@@ -3,15 +3,22 @@ using Microsoft.AspNetCore.Mvc;
3
3
  namespace <%= projectName %>.Controllers;
4
4
 
5
5
  [ApiController]
6
- [Route("[controller]")]
6
+ [Route("api/[controller]")]
7
7
  public class <%= controllerName %>Controller : ControllerBase
8
8
  {
9
- <% endpoints.forEach(ep => { -%>
10
- [Http<%= ep.method.charAt(0) + ep.method.slice(1).toLowerCase() %>("<%= ep.route.replace(`/${controllerName.toLowerCase()}`, "") %>")]
11
- public IActionResult <%= ep.actionName || (ep.method.toLowerCase() + controllerName) %>()
9
+ // Endpoints for <%= controllerName %> auto-generated by Backlist
10
+
11
+ <% endpoints.forEach(endpoint => { %>
12
+ <%# Convert /api/users/{id} to just {id} for the route attribute %>
13
+ <% const routePath = endpoint.path.replace(`/api/${controllerName.toLowerCase()}`, '').substring(1); %>
14
+ /**
15
+ * <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>
16
+ */
17
+ [Http<%= endpoint.method.charAt(0) + endpoint.method.slice(1).toLowerCase() %>("<%- routePath %>")]
18
+ public IActionResult AutoGenerated_<%= endpoint.method %>_<%= routePath.replace(/{|}/g, 'By_').replace(/[^a-zA-Z0-9_]/g, '') || 'Index' %>()
12
19
  {
13
- return Ok(new { message = "TODO: Implement <%= ep.method %> <%= ep.route %>" });
20
+ // TODO: Implement logic here. You can access route parameters like: public IActionResult Get(int id)
21
+ return Ok(new { message = "Auto-generated response for <%= endpoint.method.toUpperCase() %> <%= endpoint.path %>" });
14
22
  }
15
-
16
- <% }) -%>
23
+ <% }); %>
17
24
  }
@@ -4,27 +4,22 @@ package <%= group %>.<%= projectName %>;
4
4
  import org.springframework.boot.CommandLineRunner;
5
5
  import org.springframework.context.annotation.Bean;
6
6
  import org.springframework.context.annotation.Configuration;
7
-
8
7
  import <%= group %>.<%= projectName %>.model.User;
9
8
  import <%= group %>.<%= projectName %>.repository.UserRepository;
10
-
11
9
  import org.springframework.security.crypto.password.PasswordEncoder;
12
10
 
13
11
  @Configuration
14
12
  public class ApplicationSeeder {
15
-
16
13
  @Bean
17
14
  CommandLineRunner seed(UserRepository userRepository, PasswordEncoder encoder) {
18
15
  return args -> {
19
- userRepository.findByEmail("admin@example.com").ifPresentOrElse(u -> {
20
- // already exists
21
- }, () -> {
16
+ if (userRepository.findByEmail("admin@example.com").isEmpty()) {
22
17
  User admin = new User();
23
18
  admin.setName("Admin");
24
19
  admin.setEmail("admin@example.com");
25
20
  admin.setPassword(encoder.encode("admin123"));
26
21
  userRepository.save(admin);
27
- });
22
+ }
28
23
  };
29
24
  }
30
25
  }