create-backlist 6.0.0 → 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 (45) 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 +157 -109
  7. package/src/generators/node.js +262 -85
  8. package/src/generators/template.js +38 -2
  9. package/src/scanner/index.js +99 -0
  10. package/src/templates/dotnet/partials/Controller.cs.ejs +7 -14
  11. package/src/templates/dotnet/partials/Dto.cs.ejs +8 -0
  12. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +7 -2
  13. package/src/templates/java-spring/partials/AuthController.java.ejs +23 -10
  14. package/src/templates/java-spring/partials/Controller.java.ejs +17 -6
  15. package/src/templates/java-spring/partials/Dockerfile.ejs +6 -1
  16. package/src/templates/java-spring/partials/Entity.java.ejs +15 -5
  17. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +30 -7
  18. package/src/templates/java-spring/partials/JwtService.java.ejs +38 -10
  19. package/src/templates/java-spring/partials/Repository.java.ejs +10 -1
  20. package/src/templates/java-spring/partials/Service.java.ejs +45 -7
  21. package/src/templates/java-spring/partials/User.java.ejs +17 -4
  22. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
  23. package/src/templates/java-spring/partials/UserRepository.java.ejs +8 -0
  24. package/src/templates/java-spring/partials/docker-compose.yml.ejs +16 -8
  25. package/src/templates/node-ts-express/base/server.ts +12 -5
  26. package/src/templates/node-ts-express/base/tsconfig.json +13 -3
  27. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +17 -7
  28. package/src/templates/node-ts-express/partials/App.test.ts.ejs +27 -27
  29. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +56 -62
  30. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +21 -10
  31. package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
  32. package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
  33. package/src/templates/node-ts-express/partials/Dockerfile.ejs +9 -11
  34. package/src/templates/node-ts-express/partials/Model.cs.ejs +25 -7
  35. package/src/templates/node-ts-express/partials/Model.ts.ejs +20 -12
  36. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +72 -55
  37. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +27 -12
  38. package/src/templates/node-ts-express/partials/README.md.ejs +9 -12
  39. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +44 -64
  40. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +31 -16
  41. package/src/templates/node-ts-express/partials/package.json.ejs +3 -1
  42. package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +4 -0
  43. package/src/templates/node-ts-express/partials/routes.ts.ejs +35 -24
  44. package/src/utils.js +19 -4
  45. 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,5 +1,4 @@
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
4
  const axios = require('axios');
@@ -8,8 +7,10 @@ const { analyzeFrontend } = require('../analyzer');
8
7
  const { renderAndWrite, getTemplatePath } = require('./template');
9
8
 
10
9
  function sanitizeArtifactId(name) {
11
- // Lowercase, keep letters, numbers and dashes; replace others with dashes
12
- return String(name || 'backend').toLowerCase().replace(/[^a-z0-9\-]/g, '-').replace(/-+/g, '-');
10
+ return String(name || 'backend')
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9\-]/g, '-')
13
+ .replace(/-+/g, '-');
13
14
  }
14
15
 
15
16
  async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, dependencies }) {
@@ -19,20 +20,16 @@ async function downloadInitializrZip({ groupId, artifactId, name, bootVersion, d
19
20
  groupId,
20
21
  artifactId,
21
22
  name,
22
- packageName: `${groupId}.${artifactId.replace(/-/g, '')}`, // com.example.myapp
23
+ packageName: `${groupId}.${artifactId.replace(/-/g, '')}`,
23
24
  dependencies: dependencies.join(','),
24
25
  });
25
-
26
26
  if (bootVersion) params.set('bootVersion', bootVersion);
27
27
 
28
28
  const url = `https://start.spring.io/starter.zip?${params.toString()}`;
29
-
30
- const res = await axios.get(url, {
29
+ return axios.get(url, {
31
30
  responseType: 'stream',
32
31
  headers: { Accept: 'application/zip' }
33
32
  });
34
-
35
- return res;
36
33
  }
37
34
 
38
35
  async function extractZipStream(stream, dest) {
@@ -44,135 +41,186 @@ async function extractZipStream(stream, dest) {
44
41
  });
45
42
  }
46
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
+
47
117
  async function generateJavaProject(options) {
48
118
  const { projectDir, projectName, frontendSrcDir } = options;
49
119
  const groupId = 'com.backlist.generated';
50
120
  const artifactId = sanitizeArtifactId(projectName || 'backend');
51
- const name = projectName || 'backend';
121
+ const basePackage = `${groupId}.${artifactId.replace(/-/g, '')}`;
52
122
 
53
123
  try {
54
- console.log(chalk.blue(' -> Contacting Spring Initializr to download a base Spring Boot project...'));
124
+ console.log(chalk.blue(' -> Downloading base Spring Boot project from Initializr...'));
55
125
 
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
126
+ const deps = ['web', 'data-jpa', 'lombok', 'postgresql'];
58
127
 
59
128
  let response;
60
129
  try {
61
130
  response = await downloadInitializrZip({
62
- groupId,
63
- artifactId,
64
- name,
65
- bootVersion: '3.3.4', // current stable; adjust as needed
131
+ groupId, artifactId, name: projectName || 'backend',
132
+ bootVersion: '3.3.4',
66
133
  dependencies: deps
67
134
  });
68
135
  } catch (err) {
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
- }
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
+ });
90
142
  }
91
143
 
92
- console.log(chalk.blue(' -> Unzipping the Spring Boot project...'));
144
+ console.log(chalk.blue(' -> Unzipping...'));
93
145
  await extractZipStream(response.data, projectDir);
94
146
 
95
- // Analyze frontend and plan entities/controllers
147
+ console.log(chalk.blue(' -> Analyzing frontend (AST) for endpoints/contracts...'));
96
148
  const endpoints = await analyzeFrontend(frontendSrcDir);
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
- }
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
+ );
139
187
  }
140
188
 
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).'));
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
+ );
155
196
  }
156
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);
216
+
157
217
  console.log(chalk.green(' -> Java (Spring Boot) backend generation is complete!'));
158
218
  console.log(chalk.yellow('\nNext steps:'));
159
219
  console.log(chalk.cyan(` cd ${path.basename(projectDir)}`));
160
- console.log(chalk.cyan(' ./mvnw spring-boot:run # or use your IDE to run Application class'));
220
+ console.log(chalk.cyan(' ./mvnw spring-boot:run'));
161
221
 
162
222
  } catch (error) {
163
223
  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
- }
176
224
  throw new Error(`Failed to download from Spring Initializr. Status: ${error.response.status}`);
177
225
  }
178
226
  throw error;