create-backlist 6.0.7 → 6.0.8

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 +85 -262
  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
@@ -5,153 +5,127 @@ const path = require('path');
5
5
  const { analyzeFrontend } = require('../analyzer');
6
6
  const { renderAndWrite, getTemplatePath } = require('./template');
7
7
 
8
- function groupByController(endpoints) {
9
- const map = new Map();
10
- for (const ep of endpoints) {
11
- const c = ep.controllerName || 'Default';
12
- if (!map.has(c)) map.set(c, []);
13
- map.get(c).push(ep);
14
- }
15
- return map;
16
- }
17
-
18
- function collectDtoModels(endpoints) {
19
- const models = new Map();
20
-
21
- for (const ep of endpoints) {
22
- if (ep.requestBody?.fields && ep.requestBody.modelName) {
23
- models.set(ep.requestBody.modelName, { name: ep.requestBody.modelName, fields: ep.requestBody.fields });
24
- }
25
- if (ep.responseBody?.fields && ep.responseBody.modelName) {
26
- models.set(ep.responseBody.modelName, { name: ep.responseBody.modelName, fields: ep.responseBody.fields });
27
- }
28
- }
29
-
30
- return models;
31
- }
32
-
33
- async function ensureProgramMarkers(programCsPath) {
34
- let content = await fs.readFile(programCsPath, 'utf-8');
35
-
36
- if (!content.includes('<backlist:usings>')) {
37
- // add marker near top
38
- content = content.replace(
39
- /^/m,
40
- `// <backlist:usings>\n// </backlist:usings>\n\n`
41
- );
42
- }
43
- if (!content.includes('<backlist:services>')) {
44
- content = content.replace(
45
- 'builder.Services.AddControllers();',
46
- `builder.Services.AddControllers();\n\n// <backlist:services>\n// </backlist:services>`
47
- );
48
- }
49
- if (!content.includes('<backlist:middleware>')) {
50
- content = content.replace(
51
- 'var app = builder.Build();',
52
- `var app = builder.Build();\n\n// <backlist:middleware>\n// </backlist:middleware>\n`
53
- );
54
- }
55
-
56
- await fs.writeFile(programCsPath, content);
57
- }
58
-
59
- function insertBetweenMarkers(content, markerName, insertText) {
60
- const start = `// <backlist:${markerName}>`;
61
- const end = `// </backlist:${markerName}>`;
62
-
63
- const s = content.indexOf(start);
64
- const e = content.indexOf(end);
65
- if (s === -1 || e === -1 || e < s) return content;
66
-
67
- const before = content.slice(0, s + start.length);
68
- const after = content.slice(e);
69
-
70
- return `${before}\n${insertText}\n${after}`;
71
- }
72
-
73
8
  async function generateDotnetProject(options) {
74
9
  const { projectDir, projectName, frontendSrcDir } = options;
75
10
 
76
11
  try {
77
- console.log(chalk.blue(' -> Analyzing frontend for C# backend (AST)...'));
12
+ // --- Step 1: Analysis & Model Identification ---
13
+ console.log(chalk.blue(' -> Analyzing frontend for C# backend...'));
78
14
  const endpoints = await analyzeFrontend(frontendSrcDir);
15
+ const modelsToGenerate = new Map();
16
+ endpoints.forEach(ep => {
17
+ // For C#, we create a model if schemaFields exist for any endpoint related to a controller
18
+ if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
19
+ modelsToGenerate.set(ep.controllerName, {
20
+ name: ep.controllerName,
21
+ fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type }))
22
+ });
23
+ }
24
+ });
79
25
 
80
- const byController = groupByController(endpoints);
81
- const dtoModels = collectDtoModels(endpoints);
82
-
83
- console.log(chalk.green(` -> Found ${endpoints.length} endpoints, ${dtoModels.size} DTO models, ${byController.size} controllers.`));
26
+ if (modelsToGenerate.size > 0) {
27
+ console.log(chalk.green(` -> Identified ${modelsToGenerate.size} models/controllers to generate.`));
28
+ } else {
29
+ console.log(chalk.yellow(' -> No API calls with body data found. A basic API project will be created without models.'));
30
+ }
84
31
 
85
- // Scaffold base project
32
+ // --- Step 2: Create Base .NET Project using `dotnet new` ---
86
33
  console.log(chalk.blue(' -> Scaffolding .NET Core Web API project...'));
87
34
  await execa('dotnet', ['new', 'webapi', '-n', projectName, '-o', projectDir, '--no-https']);
35
+
36
+ // --- Step 3: Add Required NuGet Packages ---
37
+ if (modelsToGenerate.size > 0) {
38
+ console.log(chalk.blue(' -> Adding NuGet packages (Entity Framework Core)...'));
39
+ const packages = [
40
+ 'Microsoft.EntityFrameworkCore.Design',
41
+ 'Microsoft.EntityFrameworkCore.InMemory' // Using InMemory for a simple, runnable setup
42
+ // For a real DB, a user would add: 'Npgsql.EntityFrameworkCore.PostgreSQL' or 'Microsoft.EntityFrameworkCore.SqlServer'
43
+ ];
44
+ for (const pkg of packages) {
45
+ await execa('dotnet', ['add', 'package', pkg], { cwd: projectDir });
46
+ }
47
+ }
88
48
 
89
- // Remove WeatherForecast
90
- await fs.remove(path.join(projectDir, 'Controllers', 'WeatherForecastController.cs'));
91
- await fs.remove(path.join(projectDir, 'WeatherForecast.cs'));
92
-
93
- // Generate DTOs
94
- if (dtoModels.size > 0) {
95
- console.log(chalk.blue(' -> Generating DTO models...'));
96
- const dtoDir = path.join(projectDir, 'Models', 'DTOs');
97
- await fs.ensureDir(dtoDir);
98
-
99
- for (const model of dtoModels.values()) {
49
+ // --- Step 4: Generate Models and DbContext from Templates ---
50
+ if (modelsToGenerate.size > 0) {
51
+ console.log(chalk.blue(' -> Generating EF Core models and DbContext...'));
52
+ const modelsDir = path.join(projectDir, 'Models');
53
+ const dataDir = path.join(projectDir, 'Data');
54
+ await fs.ensureDir(modelsDir);
55
+ await fs.ensureDir(dataDir);
56
+
57
+ for (const [modelName, modelData] of modelsToGenerate.entries()) {
58
+ await renderAndWrite(
59
+ getTemplatePath('dotnet/partials/Model.cs.ejs'),
60
+ path.join(modelsDir, `${modelName}.cs`),
61
+ { projectName, modelName, model: modelData }
62
+ );
63
+ }
64
+
100
65
  await renderAndWrite(
101
- getTemplatePath('dotnet/partials/Dto.cs.ejs'),
102
- path.join(dtoDir, `${model.name}.cs`),
103
- { projectName, model }
66
+ getTemplatePath('dotnet/partials/DbContext.cs.ejs'),
67
+ path.join(dataDir, 'ApplicationDbContext.cs'),
68
+ { projectName, modelsToGenerate: Array.from(modelsToGenerate.values()) }
104
69
  );
105
- }
106
- }
107
-
108
- // Generate Controllers from endpoints (not CRUD stub)
109
- console.log(chalk.blue(' -> Generating controllers from detected endpoints...'));
110
- const controllersDir = path.join(projectDir, 'Controllers');
111
- await fs.ensureDir(controllersDir);
112
-
113
- for (const [controllerName, controllerEndpoints] of byController.entries()) {
114
- if (controllerName === 'Default') continue;
115
-
116
- await renderAndWrite(
117
- getTemplatePath('dotnet/partials/Controller.FromEndpoints.cs.ejs'),
118
- path.join(controllersDir, `${controllerName}Controller.cs`),
119
- { projectName, controllerName, endpoints: controllerEndpoints }
120
- );
121
70
  }
122
71
 
123
- // Program.cs markers + CORS insert (idempotent)
124
- console.log(chalk.blue(' -> Configuring Program.cs (idempotent markers)...'));
72
+ // --- Step 5: Configure Services in Program.cs ---
73
+ console.log(chalk.blue(' -> Configuring services in Program.cs...'));
125
74
  const programCsPath = path.join(projectDir, 'Program.cs');
126
- await ensureProgramMarkers(programCsPath);
127
-
128
75
  let programCsContent = await fs.readFile(programCsPath, 'utf-8');
129
-
130
- const corsBlock = `
76
+
77
+ let usingStatements = 'using Microsoft.EntityFrameworkCore;\nusing '+projectName+'.Data;\n';
78
+ programCsContent = usingStatements + programCsContent;
79
+
80
+ let dbContextService = `// Configure the database context\nbuilder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseInMemoryDatabase("MyDb"));`;
81
+ programCsContent = programCsContent.replace('builder.Services.AddControllers();', `builder.Services.AddControllers();\n\n${dbContextService}`);
82
+
83
+ // Enable CORS to allow frontend communication
84
+ const corsPolicy = `
131
85
  builder.Services.AddCors(options =>
132
86
  {
133
- options.AddDefaultPolicy(policy =>
134
- policy.WithOrigins("http://localhost:3000", "http://localhost:5173")
135
- .AllowAnyHeader()
136
- .AllowAnyMethod()
137
- );
138
- });`.trim();
139
-
140
- programCsContent = insertBetweenMarkers(programCsContent, 'services', corsBlock);
141
- programCsContent = insertBetweenMarkers(programCsContent, 'middleware', 'app.UseCors();');
87
+ options.AddDefaultPolicy(
88
+ policy =>
89
+ {
90
+ policy.WithOrigins("http://localhost:3000", "http://localhost:5173") // Common frontend dev ports
91
+ .AllowAnyHeader()
92
+ .AllowAnyMethod();
93
+ });
94
+ });`;
95
+ programCsContent = programCsContent.replace('var app = builder.Build();', `${corsPolicy}\n\nvar app = builder.Build();\n\napp.UseCors();`);
142
96
 
143
97
  await fs.writeFile(programCsPath, programCsContent);
144
98
 
145
- // README
99
+ // --- Step 6: Generate Controllers with full CRUD ---
100
+ console.log(chalk.blue(' -> Generating controllers with CRUD logic...'));
101
+ await fs.remove(path.join(projectDir, 'Controllers', 'WeatherForecastController.cs'));
102
+ await fs.remove(path.join(projectDir, 'WeatherForecast.cs'));
103
+
104
+ const controllersToGenerate = new Set(Array.from(modelsToGenerate.keys()));
105
+ // Also add controllers for endpoints that didn't have a body but were detected
106
+ endpoints.forEach(ep => {
107
+ if (ep.controllerName !== 'Default') controllersToGenerate.add(ep.controllerName);
108
+ });
109
+
110
+ for (const controllerName of controllersToGenerate) {
111
+ await renderAndWrite(
112
+ getTemplatePath('dotnet/partials/Controller.cs.ejs'),
113
+ path.join(projectDir, 'Controllers', `${controllerName}Controller.cs`),
114
+ { projectName, controllerName }
115
+ );
116
+ }
117
+
118
+ // --- Step 7: Generate README ---
146
119
  await renderAndWrite(
147
- getTemplatePath('dotnet/partials/README.md.ejs'),
148
- path.join(projectDir, 'README.md'),
149
- { projectName }
120
+ getTemplatePath('dotnet/partials/README.md.ejs'),
121
+ path.join(projectDir, 'README.md'),
122
+ { projectName }
150
123
  );
151
124
 
152
125
  console.log(chalk.green(' -> C# backend generation is complete!'));
153
126
 
154
127
  } catch (error) {
128
+ // Re-throw the error to be caught by the main CLI handler
155
129
  throw error;
156
130
  }
157
131
  }
@@ -1,4 +1,5 @@
1
1
  const chalk = require('chalk');
2
+ const { execa } = require('execa');
2
3
  const fs = require('fs-extra');
3
4
  const path = require('path');
4
5
  const axios = require('axios');
@@ -7,10 +8,8 @@ const { analyzeFrontend } = require('../analyzer');
7
8
  const { renderAndWrite, getTemplatePath } = require('./template');
8
9
 
9
10
  function sanitizeArtifactId(name) {
10
- return String(name || 'backend')
11
- .toLowerCase()
12
- .replace(/[^a-z0-9\-]/g, '-')
13
- .replace(/-+/g, '-');
11
+ // Lowercase, keep letters, numbers and dashes; replace others with dashes
12
+ return String(name || 'backend').toLowerCase().replace(/[^a-z0-9\-]/g, '-').replace(/-+/g, '-');
14
13
  }
15
14
 
16
15
  async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
@@ -20,16 +19,20 @@ async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, d
20
19
  groupId,
21
20
  artifactId,
22
21
  name,
23
- packageName: `${groupId}.${artifactId.replace(/-/g, '')}`,
22
+ packageName: `${groupId}.${artifactId.replace(/-/g, '')}`, // com.example.myapp
24
23
  dependencies: dependencies.join(','),
25
24
  });
25
+
26
26
  if (bootVersion) params.set('bootVersion', bootVersion);
27
27
 
28
28
  const url = `https://start.spring.io/starter.zip?${params.toString()}`;
29
- return axios.get(url, {
29
+
30
+ const res = await axios.get(url, {
30
31
  responseType: 'stream',
31
32
  headers: { Accept: 'application/zip' }
32
33
  });
34
+
35
+ return res;
33
36
  }
34
37
 
35
38
  async function extractZipStream(stream, dest) {
@@ -41,186 +44,135 @@ async function extractZipStream(stream, dest) {
41
44
  });
42
45
  }
43
46
 
44
- function groupByController(endpoints) {
45
- const map = new Map();
46
- for (const ep of endpoints || []) {
47
- const c = ep.controllerName || 'Default';
48
- if (!map.has(c)) map.set(c, []);
49
- map.get(c).push(ep);
50
- }
51
- return map;
52
- }
53
-
54
- function collectModelsForJava(endpointsByController) {
55
- // Produces one Entity per controller + DTOs per endpoint body/response if provided by analyzer.
56
- const entities = new Map(); // controllerName -> {name, fields}
57
- const dtos = new Map(); // dtoName -> {name, fields}
58
-
59
- for (const [controllerName, eps] of endpointsByController.entries()) {
60
- if (controllerName === 'Default') continue;
61
-
62
- // Entity fields heuristic: merge requestBody fields across endpoints
63
- const mergedFields = {};
64
- for (const ep of eps) {
65
- if (ep.requestBody?.fields) {
66
- for (const [k, t] of Object.entries(ep.requestBody.fields)) mergedFields[k] = t;
67
- }
68
- }
69
-
70
- if (Object.keys(mergedFields).length > 0) {
71
- entities.set(controllerName, { name: controllerName, fields: mergedFields });
72
- }
73
-
74
- // DTOs from analyzer if exists
75
- for (const ep of eps) {
76
- if (ep.requestBody?.modelName && ep.requestBody?.fields) {
77
- dtos.set(ep.requestBody.modelName, { name: ep.requestBody.modelName, fields: ep.requestBody.fields });
78
- }
79
- if (ep.responseBody?.modelName && ep.responseBody?.fields) {
80
- dtos.set(ep.responseBody.modelName, { name: ep.responseBody.modelName, fields: ep.responseBody.fields });
81
- }
82
- }
83
- }
84
-
85
- return { entities, dtos };
86
- }
87
-
88
- async function upsertApplicationProperties(projectDir, artifactId) {
89
- const propsPath = path.join(projectDir, 'src', 'main', 'resources', 'application.properties');
90
- if (!await fs.pathExists(propsPath)) return;
91
-
92
- const start = '# <backlist:db>';
93
- const end = '# </backlist:db>';
94
-
95
- const dbProps = [
96
- start,
97
- `spring.datasource.url=jdbc:postgresql://localhost:5432/${artifactId}`,
98
- `spring.datasource.username=postgres`,
99
- `spring.datasource.password=password`,
100
- `spring.jpa.hibernate.ddl-auto=update`,
101
- `spring.jpa.show-sql=true`,
102
- end,
103
- ''
104
- ].join('\n');
105
-
106
- const current = await fs.readFile(propsPath, 'utf-8');
107
-
108
- if (current.includes(start) && current.includes(end)) {
109
- // replace existing block
110
- const replaced = current.replace(new RegExp(`${start}[\\s\\S]*?${end}`), dbProps.trim());
111
- await fs.writeFile(propsPath, replaced);
112
- } else {
113
- await fs.appendFile(propsPath, `\n\n${dbProps}`);
114
- }
115
- }
116
-
117
47
  async function generateJavaProject(options) {
118
48
  const { projectDir, projectName, frontendSrcDir } = options;
119
49
  const groupId = 'com.backlist.generated';
120
50
  const artifactId = sanitizeArtifactId(projectName || 'backend');
121
- const basePackage = `${groupId}.${artifactId.replace(/-/g, '')}`;
51
+ const name = projectName || 'backend';
122
52
 
123
53
  try {
124
- console.log(chalk.blue(' -> Downloading base Spring Boot project from Initializr...'));
54
+ console.log(chalk.blue(' -> Contacting Spring Initializr to download a base Spring Boot project...'));
125
55
 
126
- const deps = ['web', 'data-jpa', 'lombok', 'postgresql'];
56
+ // Primary attempt: current stable Boot version. If this fails, we’ll retry without bootVersion.
57
+ const deps = ['web', 'data-jpa', 'lombok', 'postgresql']; // valid Initializr ids
127
58
 
128
59
  let response;
129
60
  try {
130
61
  response = await downloadInitializrZip({
131
- groupId, artifactId, name: projectName || 'backend',
132
- bootVersion: '3.3.4',
62
+ groupId,
63
+ artifactId,
64
+ name,
65
+ bootVersion: '3.3.4', // current stable; adjust as needed
133
66
  dependencies: deps
134
67
  });
135
68
  } catch (err) {
136
- console.log(chalk.yellow(' -> Retry without fixed bootVersion...'));
137
- response = await downloadInitializrZip({
138
- groupId, artifactId, name: projectName || 'backend',
139
- bootVersion: '',
140
- dependencies: deps
141
- });
69
+ // Fallback remove bootVersion and also try smaller dependency set if needed
70
+ const fallbackDeps = ['web', 'data-jpa', 'lombok'];
71
+ try {
72
+ console.log(chalk.yellow(' -> Initial attempt failed. Retrying with default Boot version...'));
73
+ response = await downloadInitializrZip({
74
+ groupId,
75
+ artifactId,
76
+ name,
77
+ bootVersion: '', // let Initializr pick latest
78
+ dependencies: deps
79
+ });
80
+ } catch {
81
+ console.log(chalk.yellow(' -> Second attempt failed. Retrying with minimal dependencies...'));
82
+ response = await downloadInitializrZip({
83
+ groupId,
84
+ artifactId,
85
+ name,
86
+ bootVersion: '',
87
+ dependencies: fallbackDeps
88
+ });
89
+ }
142
90
  }
143
91
 
144
- console.log(chalk.blue(' -> Unzipping...'));
92
+ console.log(chalk.blue(' -> Unzipping the Spring Boot project...'));
145
93
  await extractZipStream(response.data, projectDir);
146
94
 
147
- console.log(chalk.blue(' -> Analyzing frontend (AST) for endpoints/contracts...'));
95
+ // Analyze frontend and plan entities/controllers
148
96
  const endpoints = await analyzeFrontend(frontendSrcDir);
149
-
150
- const endpointsByController = groupByController(endpoints);
151
- const { entities, dtos } = collectModelsForJava(endpointsByController);
152
-
153
- console.log(chalk.green(` -> Found ${Array.isArray(endpoints) ? endpoints.length : 0} endpoints`));
154
- console.log(chalk.green(` -> Will generate ${entities.size} entities, ${dtos.size} DTOs, ${endpointsByController.size} controllers`));
155
-
156
- // Compute java src root
157
- const javaSrcRoot = path.join(
158
- projectDir,
159
- 'src', 'main', 'java',
160
- ...groupId.split('.'),
161
- artifactId.replace(/-/g, '')
162
- );
163
-
164
- const entityDir = path.join(javaSrcRoot, 'model');
165
- const dtoDir = path.join(javaSrcRoot, 'dto');
166
- const repoDir = path.join(javaSrcRoot, 'repository');
167
- const controllerDir = path.join(javaSrcRoot, 'controller');
168
-
169
- await fs.ensureDir(entityDir);
170
- await fs.ensureDir(dtoDir);
171
- await fs.ensureDir(repoDir);
172
- await fs.ensureDir(controllerDir);
173
-
174
- // Entities + Repos
175
- for (const ent of entities.values()) {
176
- await renderAndWrite(
177
- getTemplatePath('java-spring/partials/Entity.java.ejs'),
178
- path.join(entityDir, `${ent.name}.java`),
179
- { basePackage, model: ent }
180
- );
181
-
182
- await renderAndWrite(
183
- getTemplatePath('java-spring/partials/Repository.java.ejs'),
184
- path.join(repoDir, `${ent.name}Repository.java`),
185
- { basePackage, entityName: ent.name }
186
- );
187
- }
188
-
189
- // DTOs
190
- for (const dto of dtos.values()) {
191
- await renderAndWrite(
192
- getTemplatePath('java-spring/partials/Dto.java.ejs'),
193
- path.join(dtoDir, `${dto.name}.java`),
194
- { basePackage, dto }
195
- );
97
+ const modelsToGenerate = new Map();
98
+ (Array.isArray(endpoints) ? endpoints : []).forEach(ep => {
99
+ if (ep && ep.schemaFields && ep.controllerName && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
100
+ modelsToGenerate.set(ep.controllerName, {
101
+ name: ep.controllerName,
102
+ fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type }))
103
+ });
104
+ }
105
+ });
106
+
107
+ // Generate Entities/Repositories/Controllers (basic)
108
+ if (modelsToGenerate.size > 0) {
109
+ console.log(chalk.blue(' -> Generating Java entities, repositories, and controllers...'));
110
+
111
+ // Spring Initializr zips project as <artifactId> root folder; if you extracted to projectDir,
112
+ // files are already in the right place. Compute Java src path:
113
+ const javaSrcRoot = path.join(projectDir, 'src', 'main', 'java', ...groupId.split('.'), artifactId.replace(/-/g, ''));
114
+ const entityDir = path.join(javaSrcRoot, 'model');
115
+ const repoDir = path.join(javaSrcRoot, 'repository');
116
+ const controllerDir = path.join(javaSrcRoot, 'controller');
117
+
118
+ await fs.ensureDir(entityDir);
119
+ await fs.ensureDir(repoDir);
120
+ await fs.ensureDir(controllerDir);
121
+
122
+ for (const [modelName, modelData] of modelsToGenerate.entries()) {
123
+ await renderAndWrite(
124
+ getTemplatePath('java-spring/partials/Entity.java.ejs'),
125
+ path.join(entityDir, `${modelName}.java`),
126
+ { group: groupId, projectName: artifactId.replace(/-/g, ''), modelName, model: modelData }
127
+ );
128
+ await renderAndWrite(
129
+ getTemplatePath('java-spring/partials/Repository.java.ejs'),
130
+ path.join(repoDir, `${modelName}Repository.java`),
131
+ { group: groupId, projectName: artifactId.replace(/-/g, ''), modelName }
132
+ );
133
+ await renderAndWrite(
134
+ getTemplatePath('java-spring/partials/Controller.java.ejs'),
135
+ path.join(controllerDir, `${modelName}Controller.java`),
136
+ { group: groupId, projectName: artifactId.replace(/-/g, ''), controllerName: modelName, model: modelData }
137
+ );
138
+ }
196
139
  }
197
140
 
198
- // Controllers from endpoints
199
- for (const [controllerName, eps] of endpointsByController.entries()) {
200
- if (controllerName === 'Default') continue;
201
-
202
- await renderAndWrite(
203
- getTemplatePath('java-spring/partials/Controller.FromEndpoints.java.ejs'),
204
- path.join(controllerDir, `${controllerName}Controller.java`),
205
- {
206
- basePackage,
207
- controllerName,
208
- endpoints: eps,
209
- hasEntity: entities.has(controllerName)
210
- }
211
- );
141
+ // Append DB config (PostgreSQL) to application.properties (non-fatal if fails)
142
+ try {
143
+ const propsPath = path.join(projectDir, 'src', 'main', 'resources', 'application.properties');
144
+ const dbProps = [
145
+ `\n\n# --- Auto-generated by create-backlist ---`,
146
+ `spring.datasource.url=jdbc:postgresql://localhost:5432/${artifactId}`,
147
+ `spring.datasource.username=postgres`,
148
+ `spring.datasource.password=password`,
149
+ `spring.jpa.hibernate.ddl-auto=update`,
150
+ `spring.jpa.show-sql=true`,
151
+ ].join('\n');
152
+ await fs.appendFile(propsPath, dbProps);
153
+ } catch (e) {
154
+ console.log(chalk.yellow(' -> Could not update application.properties (continuing).'));
212
155
  }
213
156
 
214
- // application.properties idempotent update
215
- await upsertApplicationProperties(projectDir, artifactId);
216
-
217
157
  console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
218
158
  console.log(chalk.yellow('\nNext steps:'));
219
159
  console.log(chalk.cyan(` cd ${path.basename(projectDir)}`));
220
- console.log(chalk.cyan(' ./mvnw spring-boot:run'));
160
+ console.log(chalk.cyan(' ./mvnw spring-boot:run # or use your IDE to run Application class'));
221
161
 
222
162
  } catch (error) {
223
163
  if (error.response && error.response.status) {
164
+ console.error(chalk.red(` -> Initializr error status: ${error.response.status}`));
165
+ if (error.response.data) {
166
+ try {
167
+ // Try read error text body for hints
168
+ const text = (await (async () => {
169
+ let buf = '';
170
+ for await (const chunk of error.response.data) buf += chunk.toString();
171
+ return buf;
172
+ })());
173
+ console.error(chalk.yellow(' -> Initializr response body:'), text);
174
+ } catch {}
175
+ }
224
176
  throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
225
177
  }
226
178
  throw error;