create-backlist 5.0.7 → 6.0.2

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 (58) hide show
  1. package/bin/backlist.js +227 -0
  2. package/package.json +10 -4
  3. package/src/analyzer.js +210 -89
  4. package/src/db/prisma.ts +4 -0
  5. package/src/generators/dotnet.js +120 -94
  6. package/src/generators/java.js +205 -75
  7. package/src/generators/node.js +262 -85
  8. package/src/generators/python.js +54 -25
  9. package/src/generators/template.js +38 -2
  10. package/src/scanner/index.js +99 -0
  11. package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
  12. package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
  13. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +30 -0
  14. package/src/templates/java-spring/partials/AuthController.java.ejs +62 -0
  15. package/src/templates/java-spring/partials/Controller.java.ejs +40 -50
  16. package/src/templates/java-spring/partials/Dockerfile.ejs +16 -0
  17. package/src/templates/java-spring/partials/Entity.java.ejs +16 -15
  18. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +66 -0
  19. package/src/templates/java-spring/partials/JwtService.java.ejs +58 -0
  20. package/src/templates/java-spring/partials/Repository.java.ejs +9 -3
  21. package/src/templates/java-spring/partials/SecurityConfig.java.ejs +44 -0
  22. package/src/templates/java-spring/partials/Service.java.ejs +69 -0
  23. package/src/templates/java-spring/partials/User.java.ejs +33 -0
  24. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +33 -0
  25. package/src/templates/java-spring/partials/UserRepository.java.ejs +20 -0
  26. package/src/templates/java-spring/partials/docker-compose.yml.ejs +35 -0
  27. package/src/templates/node-ts-express/base/server.ts +12 -5
  28. package/src/templates/node-ts-express/base/tsconfig.json +13 -3
  29. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
  30. package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
  31. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
  32. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
  33. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  34. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  35. package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
  36. package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
  37. package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
  38. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
  39. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
  40. package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
  41. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
  42. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
  43. package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
  44. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
  45. package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
  46. package/src/templates/python-fastapi/Dockerfile.ejs +8 -0
  47. package/src/templates/python-fastapi/app/core/config.py.ejs +8 -0
  48. package/src/templates/python-fastapi/app/core/security.py.ejs +8 -0
  49. package/src/templates/python-fastapi/app/db.py.ejs +7 -0
  50. package/src/templates/python-fastapi/app/main.py.ejs +24 -0
  51. package/src/templates/python-fastapi/app/models/user.py.ejs +9 -0
  52. package/src/templates/python-fastapi/app/routers/auth.py.ejs +33 -0
  53. package/src/templates/python-fastapi/app/routers/model_routes.py.ejs +72 -0
  54. package/src/templates/python-fastapi/app/schemas/user.py.ejs +16 -0
  55. package/src/templates/python-fastapi/docker-compose.yml.ejs +19 -0
  56. package/src/templates/python-fastapi/requirements.txt.ejs +5 -1
  57. package/src/utils.js +19 -4
  58. package/bin/index.js +0 -141
@@ -5,127 +5,153 @@ 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
+
8
73
  async function generateDotnetProject(options) {
9
74
  const { projectDir, projectName, frontendSrcDir } = options;
10
75
 
11
76
  try {
12
- // --- Step 1: Analysis & Model Identification ---
13
- console.log(chalk.blue(' -> Analyzing frontend for C# backend...'));
77
+ console.log(chalk.blue(' -> Analyzing frontend for C# backend (AST)...'));
14
78
  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
- });
25
79
 
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
- }
80
+ const byController = groupByController(endpoints);
81
+ const dtoModels = collectDtoModels(endpoints);
31
82
 
32
- // --- Step 2: Create Base .NET Project using `dotnet new` ---
83
+ console.log(chalk.green(` -> Found ${endpoints.length} endpoints, ${dtoModels.size} DTO models, ${byController.size} controllers.`));
84
+
85
+ // Scaffold base project
33
86
  console.log(chalk.blue(' -> Scaffolding .NET Core Web API project...'));
34
87
  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
- }
48
88
 
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
-
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()) {
65
100
  await renderAndWrite(
66
- getTemplatePath('dotnet/partials/DbContext.cs.ejs'),
67
- path.join(dataDir, 'ApplicationDbContext.cs'),
68
- { projectName, modelsToGenerate: Array.from(modelsToGenerate.values()) }
101
+ getTemplatePath('dotnet/partials/Dto.cs.ejs'),
102
+ path.join(dtoDir, `${model.name}.cs`),
103
+ { projectName, model }
69
104
  );
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
+ );
70
121
  }
71
122
 
72
- // --- Step 5: Configure Services in Program.cs ---
73
- console.log(chalk.blue(' -> Configuring services in Program.cs...'));
123
+ // Program.cs markers + CORS insert (idempotent)
124
+ console.log(chalk.blue(' -> Configuring Program.cs (idempotent markers)...'));
74
125
  const programCsPath = path.join(projectDir, 'Program.cs');
126
+ await ensureProgramMarkers(programCsPath);
127
+
75
128
  let programCsContent = await fs.readFile(programCsPath, 'utf-8');
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 = `
129
+
130
+ const corsBlock = `
85
131
  builder.Services.AddCors(options =>
86
132
  {
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();`);
133
+ options.AddDefaultPolicy(policy =>
134
+ policy.WithOrigins("http://localhost:3000", "http://localhost:5173")
135
+ .AllowAnyHeader()
136
+ .AllowAnyMethod()
137
+ );
138
+ });`.trim();
96
139
 
97
- await fs.writeFile(programCsPath, programCsContent);
140
+ programCsContent = insertBetweenMarkers(programCsContent, 'services', corsBlock);
141
+ programCsContent = insertBetweenMarkers(programCsContent, 'middleware', 'app.UseCors();');
98
142
 
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
- }
143
+ await fs.writeFile(programCsPath, programCsContent);
117
144
 
118
- // --- Step 7: Generate README ---
145
+ // README
119
146
  await renderAndWrite(
120
- getTemplatePath('dotnet/partials/README.md.ejs'),
121
- path.join(projectDir, 'README.md'),
122
- { projectName }
147
+ getTemplatePath('dotnet/partials/README.md.ejs'),
148
+ path.join(projectDir, 'README.md'),
149
+ { projectName }
123
150
  );
124
151
 
125
152
  console.log(chalk.green(' -> C# backend generation is complete!'));
126
153
 
127
154
  } catch (error) {
128
- // Re-throw the error to be caught by the main CLI handler
129
155
  throw error;
130
156
  }
131
157
  }
@@ -1,96 +1,226 @@
1
1
  const chalk = require('chalk');
2
- const { execa } = require('execa');
3
2
  const fs = require('fs-extra');
4
3
  const path = require('path');
5
- const axios = require('axios'); // For making HTTP requests
6
- const unzipper = require('unzipper'); // For extracting .zip files
4
+ const axios = require('axios');
5
+ const unzipper = require('unzipper');
7
6
  const { analyzeFrontend } = require('../analyzer');
8
7
  const { renderAndWrite, getTemplatePath } = require('./template');
9
8
 
9
+ function sanitizeArtifactId(name) {
10
+ return String(name || 'backend')
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9\-]/g, '-')
13
+ .replace(/-+/g, '-');
14
+ }
15
+
16
+ async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
17
+ const params = new URLSearchParams({
18
+ type: 'maven-project',
19
+ language: 'java',
20
+ groupId,
21
+ artifactId,
22
+ name,
23
+ packageName: `${groupId}.${artifactId.replace(/-/g, '')}`,
24
+ dependencies: dependencies.join(','),
25
+ });
26
+ if (bootVersion) params.set('bootVersion', bootVersion);
27
+
28
+ const url = `https://start.spring.io/starter.zip?${params.toString()}`;
29
+ return axios.get(url, {
30
+ responseType: 'stream',
31
+ headers: { Accept: 'application/zip' }
32
+ });
33
+ }
34
+
35
+ async function extractZipStream(stream, dest) {
36
+ await new Promise((resolve, reject) => {
37
+ const out = stream.pipe(unzipper.Extract({ path: dest }));
38
+ out.on('close', resolve);
39
+ out.on('finish', resolve);
40
+ out.on('error', reject);
41
+ });
42
+ }
43
+
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
+
10
117
  async function generateJavaProject(options) {
11
118
  const { projectDir, projectName, frontendSrcDir } = options;
12
- const group = 'com.backlist.generated'; // A default Java group ID
119
+ const groupId = 'com.backlist.generated';
120
+ const artifactId = sanitizeArtifactId(projectName || 'backend');
121
+ const basePackage = `${groupId}.${artifactId.replace(/-/g, '')}`;
13
122
 
14
123
  try {
15
- // --- Step 1: Download Base Project from Spring Initializr ---
16
- console.log(chalk.blue(' -> Contacting Spring Initializr to download a base Spring Boot project...'));
17
-
18
- // Define standard dependencies for a web API
19
- const dependencies = 'web,data-jpa,lombok,postgresql'; // Using PostgreSQL as an example DB driver
20
- const springInitializrUrl = `https://start.spring.io/starter.zip?type=maven-project&language=java&bootVersion=3.2.0&groupId=${group}&artifactId=${projectName}&name=${projectName}&dependencies=${dependencies}`;
21
-
22
- const response = await axios({
23
- url: springInitializrUrl,
24
- method: 'GET',
25
- responseType: 'stream'
26
- });
27
-
28
- // --- Step 2: Unzip the Downloaded Project ---
29
- console.log(chalk.blue(' -> Unzipping the Spring Boot project...'));
30
- await new Promise((resolve, reject) => {
31
- const stream = response.data.pipe(unzipper.Extract({ path: projectDir }));
32
- stream.on('finish', () => {
33
- console.log(chalk.gray(' -> Project unzipped successfully.'));
34
- resolve();
124
+ console.log(chalk.blue(' -> Downloading base Spring Boot project from Initializr...'));
125
+
126
+ const deps = ['web', 'data-jpa', 'lombok', 'postgresql'];
127
+
128
+ let response;
129
+ try {
130
+ response = await downloadInitializrZip({
131
+ groupId, artifactId, name: projectName || 'backend',
132
+ bootVersion: '3.3.4',
133
+ dependencies: deps
35
134
  });
36
- stream.on('error', reject);
37
- });
135
+ } 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
+ });
142
+ }
143
+
144
+ console.log(chalk.blue(' -> Unzipping...'));
145
+ await extractZipStream(response.data, projectDir);
38
146
 
39
- // --- Step 3: Analyze Frontend ---
147
+ console.log(chalk.blue(' -> Analyzing frontend (AST) for endpoints/contracts...'));
40
148
  const endpoints = await analyzeFrontend(frontendSrcDir);
41
- const modelsToGenerate = new Map();
42
- endpoints.forEach(ep => {
43
- if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
44
- modelsToGenerate.set(ep.controllerName, { name: ep.controllerName, fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type })) });
45
- }
46
- });
47
-
48
- // --- Step 4: Generate Java Entities and Controllers ---
49
- if (modelsToGenerate.size > 0) {
50
- console.log(chalk.blue(' -> Generating Java entities and controllers...'));
51
-
52
- const javaSrcPath = path.join(projectDir, 'src', 'main', 'java', ...group.split('.'), projectName);
53
- const entityDir = path.join(javaSrcPath, 'model');
54
- const controllerDir = path.join(javaSrcPath, 'controller');
55
- await fs.ensureDir(entityDir);
56
- await fs.ensureDir(controllerDir);
57
-
58
- for (const [modelName, modelData] of modelsToGenerate.entries()) {
59
- await renderAndWrite(
60
- getTemplatePath('java-spring/partials/Entity.java.ejs'),
61
- path.join(entityDir, `${modelName}.java`),
62
- { group, projectName, modelName, model: modelData }
63
- );
64
- await renderAndWrite(
65
- getTemplatePath('java-spring/partials/Controller.java.ejs'),
66
- path.join(controllerDir, `${modelName}Controller.java`),
67
- { group, projectName, controllerName: modelName }
68
- );
69
- }
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
+ );
70
187
  }
71
188
 
72
- // --- Step 5: Configure application.properties ---
73
- console.log(chalk.blue(' -> Configuring database in application.properties...'));
74
- const propsPath = path.join(projectDir, 'src', 'main', 'resources', 'application.properties');
75
- const dbProps = [
76
- `\n\n# --- Auto-generated by create-backlist ---`,
77
- `# --- Database Configuration (PostgreSQL) ---`,
78
- `spring.datasource.url=jdbc:postgresql://localhost:5432/${projectName}`,
79
- `spring.datasource.username=postgres`,
80
- `spring.datasource.password=password`,
81
- `spring.jpa.hibernate.ddl-auto=update`,
82
- `spring.jpa.show-sql=true`,
83
- ];
84
- await fs.appendFile(propsPath, dbProps.join('\n'));
85
-
86
- console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
87
- console.log(chalk.yellow('\nTo run your new Java backend:'));
88
- console.log(chalk.cyan(' 1. Open the project in a Java IDE (like IntelliJ IDEA or VS Code).'));
89
- console.log(chalk.cyan(' 2. Run the main application file.'));
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
+ );
196
+ }
197
+
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
+ );
212
+ }
213
+
214
+ // application.properties idempotent update
215
+ await upsertApplicationProperties(projectDir, artifactId);
90
216
 
217
+ console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
218
+ console.log(chalk.yellow('\nNext steps:'));
219
+ console.log(chalk.cyan(` cd ${path.basename(projectDir)}`));
220
+ console.log(chalk.cyan(' ./mvnw spring-boot:run'));
91
221
 
92
222
  } catch (error) {
93
- if (error.response) {
223
+ if (error.response && error.response.status) {
94
224
  throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
95
225
  }
96
226
  throw error;