create-backlist 6.0.6 → 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.
- package/bin/index.js +141 -0
- package/package.json +4 -10
- package/src/analyzer.js +105 -308
- package/src/generators/dotnet.js +94 -120
- package/src/generators/java.js +109 -157
- package/src/generators/node.js +85 -262
- package/src/generators/template.js +2 -38
- package/src/templates/dotnet/partials/Controller.cs.ejs +14 -7
- package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +2 -7
- package/src/templates/java-spring/partials/AuthController.java.ejs +10 -23
- package/src/templates/java-spring/partials/Controller.java.ejs +6 -17
- package/src/templates/java-spring/partials/Dockerfile.ejs +1 -6
- package/src/templates/java-spring/partials/Entity.java.ejs +5 -15
- package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +7 -30
- package/src/templates/java-spring/partials/JwtService.java.ejs +10 -38
- package/src/templates/java-spring/partials/Repository.java.ejs +1 -10
- package/src/templates/java-spring/partials/Service.java.ejs +7 -45
- package/src/templates/java-spring/partials/User.java.ejs +4 -17
- package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +4 -10
- package/src/templates/java-spring/partials/UserRepository.java.ejs +0 -8
- package/src/templates/java-spring/partials/docker-compose.yml.ejs +8 -16
- package/src/templates/node-ts-express/base/server.ts +6 -13
- package/src/templates/node-ts-express/base/tsconfig.json +3 -13
- package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +7 -17
- package/src/templates/node-ts-express/partials/App.test.ts.ejs +26 -49
- package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +62 -56
- package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +10 -21
- package/src/templates/node-ts-express/partials/Controller.ts.ejs +40 -40
- package/src/templates/node-ts-express/partials/DbContext.cs.ejs +3 -3
- package/src/templates/node-ts-express/partials/Dockerfile.ejs +11 -9
- package/src/templates/node-ts-express/partials/Model.cs.ejs +7 -25
- package/src/templates/node-ts-express/partials/Model.ts.ejs +12 -20
- package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +55 -72
- package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +12 -27
- package/src/templates/node-ts-express/partials/README.md.ejs +12 -9
- package/src/templates/node-ts-express/partials/Seeder.ts.ejs +64 -44
- package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +16 -31
- package/src/templates/node-ts-express/partials/package.json.ejs +1 -3
- package/src/templates/node-ts-express/partials/routes.ts.ejs +24 -35
- package/src/utils.js +4 -19
- package/bin/backlist.js +0 -227
- package/src/db/prisma.ts +0 -4
- package/src/scanner/analyzeFrontend.js +0 -146
- package/src/scanner/index.js +0 -99
- package/src/templates/dotnet/partials/Dto.cs.ejs +0 -8
- package/src/templates/node-ts-express/partials/prismaClient.ts.ejs +0 -4
package/src/generators/node.js
CHANGED
|
@@ -7,70 +7,27 @@ const { analyzeFrontend } = require('../analyzer');
|
|
|
7
7
|
const { renderAndWrite, getTemplatePath } = require('./template');
|
|
8
8
|
|
|
9
9
|
async function generateNodeProject(options) {
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
16
|
+
console.log(chalk.blue(' -> Analyzing frontend for API endpoints...'));
|
|
25
17
|
const endpoints = await analyzeFrontend(frontendSrcDir);
|
|
26
18
|
if (endpoints.length > 0) console.log(chalk.green(` -> Found ${endpoints.length} endpoints.`));
|
|
27
19
|
else console.log(chalk.yellow(' -> No API endpoints found. A basic project will be created.'));
|
|
28
20
|
|
|
29
|
-
//
|
|
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);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- Step 2: Identify Models to Generate (merge fields per controller) ---
|
|
21
|
+
// --- Step 2: Identify Models to Generate ---
|
|
39
22
|
const modelsToGenerate = new Map();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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()),
|
|
57
|
-
});
|
|
23
|
+
endpoints.forEach(ep => {
|
|
24
|
+
if (ep.schemaFields && ep.controllerName !== 'Default' && !modelsToGenerate.has(ep.controllerName)) {
|
|
25
|
+
modelsToGenerate.set(ep.controllerName, { name: ep.controllerName, fields: Object.entries(ep.schemaFields).map(([key, type]) => ({ name: key, type, isUnique: key === 'email' })) });
|
|
58
26
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Ensure User model if auth enabled
|
|
27
|
+
});
|
|
62
28
|
if (addAuth && !modelsToGenerate.has('User')) {
|
|
63
29
|
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', []);
|
|
30
|
+
modelsToGenerate.set('User', { name: 'User', fields: [{ name: 'name', type: 'String' }, { name: 'email', type: 'String', isUnique: true }, { name: 'password', type: 'String' }] });
|
|
74
31
|
}
|
|
75
32
|
|
|
76
33
|
// --- Step 3: Base Scaffolding ---
|
|
@@ -79,38 +36,28 @@ async function generateNodeProject(options) {
|
|
|
79
36
|
await fs.ensureDir(destSrcDir);
|
|
80
37
|
await fs.copy(getTemplatePath('node-ts-express/base/server.ts'), path.join(destSrcDir, 'server.ts'));
|
|
81
38
|
await fs.copy(getTemplatePath('node-ts-express/base/tsconfig.json'), path.join(projectDir, 'tsconfig.json'));
|
|
82
|
-
|
|
39
|
+
|
|
83
40
|
// --- Step 4: Prepare and Write package.json ---
|
|
84
|
-
const packageJsonContent = JSON.parse(
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
if (dbType === 'mongoose') {
|
|
89
|
-
packageJsonContent.dependencies['mongoose'] = '^7.6.3';
|
|
90
|
-
}
|
|
91
|
-
|
|
41
|
+
const packageJsonContent = JSON.parse(await ejs.renderFile(getTemplatePath('node-ts-express/partials/package.json.ejs'), { projectName }));
|
|
42
|
+
|
|
43
|
+
if (dbType === 'mongoose') packageJsonContent.dependencies['mongoose'] = '^7.6.3';
|
|
92
44
|
if (dbType === 'prisma') {
|
|
93
45
|
packageJsonContent.dependencies['@prisma/client'] = '^5.6.0';
|
|
94
46
|
packageJsonContent.devDependencies['prisma'] = '^5.6.0';
|
|
95
|
-
|
|
96
|
-
packageJsonContent.prisma = { seed: `ts-node prisma/seed.ts` };
|
|
47
|
+
packageJsonContent.prisma = { seed: `ts-node ${addSeeder ? 'scripts/seeder.ts' : 'prisma/seed.ts'}` };
|
|
97
48
|
}
|
|
98
|
-
|
|
99
49
|
if (addAuth) {
|
|
100
50
|
packageJsonContent.dependencies['jsonwebtoken'] = '^9.0.2';
|
|
101
51
|
packageJsonContent.dependencies['bcryptjs'] = '^2.4.3';
|
|
102
52
|
packageJsonContent.devDependencies['@types/jsonwebtoken'] = '^9.0.5';
|
|
103
53
|
packageJsonContent.devDependencies['@types/bcryptjs'] = '^2.4.6';
|
|
104
54
|
}
|
|
105
|
-
|
|
106
|
-
// Seeder deps only if mongoose seeder enabled
|
|
107
|
-
if (addSeeder && dbType === 'mongoose') {
|
|
55
|
+
if (addSeeder) {
|
|
108
56
|
packageJsonContent.devDependencies['@faker-js/faker'] = '^8.3.1';
|
|
109
57
|
if (!packageJsonContent.dependencies['chalk']) packageJsonContent.dependencies['chalk'] = '^4.1.2';
|
|
110
58
|
packageJsonContent.scripts['seed'] = `ts-node scripts/seeder.ts`;
|
|
111
59
|
packageJsonContent.scripts['destroy'] = `ts-node scripts/seeder.ts -d`;
|
|
112
60
|
}
|
|
113
|
-
|
|
114
61
|
if (extraFeatures.includes('testing')) {
|
|
115
62
|
packageJsonContent.devDependencies['jest'] = '^29.7.0';
|
|
116
63
|
packageJsonContent.devDependencies['supertest'] = '^6.3.3';
|
|
@@ -119,255 +66,131 @@ async function generateNodeProject(options) {
|
|
|
119
66
|
packageJsonContent.devDependencies['ts-jest'] = '^29.1.1';
|
|
120
67
|
packageJsonContent.scripts['test'] = 'jest --detectOpenHandles --forceExit';
|
|
121
68
|
}
|
|
122
|
-
|
|
123
69
|
if (extraFeatures.includes('swagger')) {
|
|
124
70
|
packageJsonContent.dependencies['swagger-ui-express'] = '^5.0.0';
|
|
125
71
|
packageJsonContent.dependencies['swagger-jsdoc'] = '^6.2.8';
|
|
126
72
|
packageJsonContent.devDependencies['@types/swagger-ui-express'] = '^4.1.6';
|
|
127
73
|
}
|
|
128
|
-
|
|
129
74
|
await fs.writeJson(path.join(projectDir, 'package.json'), packageJsonContent, { spaces: 2 });
|
|
130
|
-
|
|
131
|
-
// --- Step 5: Generate DB-specific files &
|
|
132
|
-
await fs.ensureDir(path.join(destSrcDir, 'controllers'));
|
|
133
|
-
|
|
75
|
+
|
|
76
|
+
// --- Step 5: Generate DB-specific files & Controllers ---
|
|
134
77
|
if (modelsToGenerate.size > 0) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
78
|
+
await fs.ensureDir(path.join(destSrcDir, 'controllers'));
|
|
79
|
+
if (dbType === 'mongoose') {
|
|
80
|
+
console.log(chalk.blue(' -> Generating Mongoose models and controllers...'));
|
|
81
|
+
await fs.ensureDir(path.join(destSrcDir, 'models'));
|
|
82
|
+
for (const [modelName, modelData] of modelsToGenerate.entries()) {
|
|
83
|
+
const schema = modelData.fields.reduce((acc, field) => { acc[field.name] = field.type; return acc; }, {});
|
|
84
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/Model.ts.ejs'), path.join(destSrcDir, 'models', `${modelName}.model.ts`), { modelName, schema, projectName });
|
|
85
|
+
}
|
|
86
|
+
} else if (dbType === 'prisma') {
|
|
87
|
+
console.log(chalk.blue(' -> Generating Prisma schema...'));
|
|
88
|
+
await fs.ensureDir(path.join(projectDir, 'prisma'));
|
|
89
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/PrismaSchema.prisma.ejs'), path.join(projectDir, 'prisma', 'schema.prisma'), { modelsToGenerate: Array.from(modelsToGenerate.values()) });
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.blue(' -> Generating controllers...'));
|
|
92
|
+
for (const [modelName] of modelsToGenerate.entries()) {
|
|
93
|
+
const templateFile = dbType === 'mongoose' ? 'Controller.ts.ejs' : 'PrismaController.ts.ejs';
|
|
94
|
+
await renderAndWrite(getTemplatePath(`node-ts-express/partials/${templateFile}`), path.join(destSrcDir, 'controllers', `${modelName}.controller.ts`), { modelName, projectName });
|
|
152
95
|
}
|
|
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
96
|
}
|
|
189
|
-
|
|
190
|
-
// --- Step 6: Authentication Boilerplate ---
|
|
97
|
+
|
|
98
|
+
// --- Step 6: Generate Authentication Boilerplate ---
|
|
191
99
|
if (addAuth) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
getTemplatePath('node-ts-express/partials/Auth.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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);
|
|
100
|
+
console.log(chalk.blue(' -> Generating authentication boilerplate...'));
|
|
101
|
+
await fs.ensureDir(path.join(destSrcDir, 'routes'));
|
|
102
|
+
await fs.ensureDir(path.join(destSrcDir, 'middleware'));
|
|
103
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.controller.ts.ejs'), path.join(destSrcDir, 'controllers', 'Auth.controller.ts'), { dbType, projectName });
|
|
104
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.routes.ts.ejs'), path.join(destSrcDir, 'routes', 'Auth.routes.ts'), { projectName });
|
|
105
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/Auth.middleware.ts.ejs'), path.join(destSrcDir, 'middleware', 'Auth.middleware.ts'), { projectName });
|
|
106
|
+
|
|
107
|
+
if (dbType === 'mongoose') {
|
|
108
|
+
const userModelPath = path.join(destSrcDir, 'models', 'User.model.ts');
|
|
109
|
+
if (await fs.pathExists(userModelPath)) {
|
|
110
|
+
let userModelContent = await fs.readFile(userModelPath, 'utf-8');
|
|
111
|
+
if (!userModelContent.includes('bcryptjs')) {
|
|
112
|
+
userModelContent = userModelContent.replace(`import mongoose, { Schema, Document } from 'mongoose';`, `import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';`);
|
|
113
|
+
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`;
|
|
114
|
+
userModelContent = userModelContent.replace(`// Create and export the Model`, `${preSaveHook}\n// Create and export the Model`);
|
|
115
|
+
await fs.writeFile(userModelPath, userModelContent);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
244
118
|
}
|
|
245
|
-
}
|
|
246
119
|
}
|
|
247
120
|
|
|
248
|
-
// --- Step 7: Seeder Script
|
|
249
|
-
if (addSeeder
|
|
250
|
-
console.log(chalk.blue(' -> Generating database seeder script
|
|
121
|
+
// --- Step 7: Generate Seeder Script ---
|
|
122
|
+
if (addSeeder) {
|
|
123
|
+
console.log(chalk.blue(' -> Generating database seeder script...'));
|
|
251
124
|
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
|
-
);
|
|
125
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/Seeder.ts.ejs'), path.join(projectDir, 'scripts', 'seeder.ts'), { projectName });
|
|
257
126
|
}
|
|
258
127
|
|
|
259
|
-
// --- Step 8: Extra Features ---
|
|
128
|
+
// --- Step 8: Generate Extra Features ---
|
|
260
129
|
if (extraFeatures.includes('docker')) {
|
|
261
130
|
console.log(chalk.blue(' -> Generating Docker files...'));
|
|
262
|
-
await renderAndWrite(
|
|
263
|
-
|
|
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
|
-
);
|
|
131
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/Dockerfile.ejs'), path.join(projectDir, 'Dockerfile'), { dbType, port });
|
|
132
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/docker-compose.yml.ejs'), path.join(projectDir, 'docker-compose.yml'), { projectName, dbType, port });
|
|
272
133
|
}
|
|
273
|
-
|
|
274
134
|
if (extraFeatures.includes('swagger')) {
|
|
275
135
|
console.log(chalk.blue(' -> Generating API documentation setup...'));
|
|
276
136
|
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
|
-
);
|
|
137
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/ApiDocs.ts.ejs'), path.join(destSrcDir, 'utils', 'swagger.ts'), { projectName, port });
|
|
282
138
|
}
|
|
283
|
-
|
|
284
139
|
if (extraFeatures.includes('testing')) {
|
|
285
140
|
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`;
|
|
141
|
+
const jestConfig = `/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n verbose: true,\n};`;
|
|
293
142
|
await fs.writeFile(path.join(projectDir, 'jest.config.js'), jestConfig);
|
|
294
|
-
|
|
295
143
|
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
|
-
);
|
|
144
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/App.test.ts.ejs'), path.join(projectDir, 'src', '__tests__', 'api.test.ts'), { addAuth });
|
|
301
145
|
}
|
|
302
146
|
|
|
303
|
-
// --- Step 9: Generate Main Route File & Inject into
|
|
304
|
-
await renderAndWrite(
|
|
305
|
-
|
|
306
|
-
path.join(destSrcDir, 'routes.ts'),
|
|
307
|
-
{ endpoints, addAuth, dbType }
|
|
308
|
-
);
|
|
309
|
-
|
|
147
|
+
// --- Step 9: Generate Main Route File & Inject Logic into Server ---
|
|
148
|
+
await renderAndWrite(getTemplatePath('node-ts-express/partials/routes.ts.ejs'), path.join(destSrcDir, 'routes.ts'), { endpoints, addAuth, dbType });
|
|
149
|
+
|
|
310
150
|
let serverFileContent = await fs.readFile(path.join(destSrcDir, 'server.ts'), 'utf-8');
|
|
151
|
+
let dbConnectionCode = '', swaggerInjector = '', authRoutesInjector = '';
|
|
311
152
|
|
|
312
|
-
// Mongoose db connect injection only; prisma uses src/db/prisma.ts
|
|
313
|
-
let dbConnectionCode = '';
|
|
314
153
|
if (dbType === 'mongoose') {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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`;
|
|
154
|
+
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`;
|
|
155
|
+
} else if (dbType === 'prisma') {
|
|
156
|
+
dbConnectionCode = `\nimport { PrismaClient } from '@prisma/client';\nexport const prisma = new PrismaClient();\n`;
|
|
323
157
|
}
|
|
324
|
-
|
|
325
|
-
let swaggerInjector = '';
|
|
326
158
|
if (extraFeatures.includes('swagger')) {
|
|
327
|
-
|
|
159
|
+
swaggerInjector = `\nimport { setupSwagger } from './utils/swagger';\nsetupSwagger(app);\n`;
|
|
328
160
|
}
|
|
329
|
-
|
|
330
|
-
let authRoutesInjector = '';
|
|
331
161
|
if (addAuth) {
|
|
332
|
-
|
|
162
|
+
authRoutesInjector = `import authRoutes from './routes/Auth.routes';\napp.use('/api/auth', authRoutes);\n\n`;
|
|
333
163
|
}
|
|
334
164
|
|
|
335
165
|
serverFileContent = serverFileContent
|
|
336
|
-
.replace(
|
|
337
|
-
.replace('// INJECT:ROUTES', `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes)
|
|
338
|
-
|
|
339
|
-
// place swagger setup before listen
|
|
166
|
+
.replace("dotenv.config();", `dotenv.config();${dbConnectionCode}`)
|
|
167
|
+
.replace('// INJECT:ROUTES', `${authRoutesInjector}import apiRoutes from './routes';\napp.use('/api', apiRoutes);`);
|
|
168
|
+
|
|
340
169
|
const listenRegex = /(app\.listen\()/;
|
|
341
170
|
serverFileContent = serverFileContent.replace(listenRegex, `${swaggerInjector}\n$1`);
|
|
342
|
-
|
|
343
171
|
await fs.writeFile(path.join(destSrcDir, 'server.ts'), serverFileContent);
|
|
344
172
|
|
|
345
|
-
// --- Step 10: Install Dependencies & Post-install ---
|
|
173
|
+
// --- Step 10: Install Dependencies & Run Post-install Scripts ---
|
|
346
174
|
console.log(chalk.magenta(' -> Installing dependencies... This may take a moment.'));
|
|
347
175
|
await execa('npm', ['install'], { cwd: projectDir });
|
|
348
|
-
|
|
349
176
|
if (dbType === 'prisma') {
|
|
350
177
|
console.log(chalk.blue(' -> Running `prisma generate`...'));
|
|
351
178
|
await execa('npx', ['prisma', 'generate'], { cwd: projectDir });
|
|
352
179
|
}
|
|
353
|
-
|
|
354
|
-
// --- Step 11: Final Files (.env.example) ---
|
|
180
|
+
|
|
181
|
+
// --- Step 11: Generate Final Files (.env.example) ---
|
|
355
182
|
let envContent = `PORT=${port}\n`;
|
|
356
|
-
|
|
357
183
|
if (dbType === 'mongoose') {
|
|
358
|
-
|
|
184
|
+
envContent += `MONGO_URI=mongodb://root:example@db:27017/${projectName}?authSource=admin\n`;
|
|
359
185
|
} else if (dbType === 'prisma') {
|
|
360
|
-
|
|
186
|
+
envContent += `DATABASE_URL="postgresql://user:password@db:5432/${projectName}?schema=public"\n`;
|
|
361
187
|
}
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
if (extraFeatures.includes('docker') && dbType === 'prisma') {
|
|
366
|
-
envContent += `\n# Docker-compose credentials\nDB_USER=postgres\nDB_PASSWORD=password\nDB_NAME=${projectName}\n`;
|
|
188
|
+
if (addAuth) envContent += `JWT_SECRET=your_super_secret_jwt_key_12345\n`;
|
|
189
|
+
if (extraFeatures.includes('docker')) {
|
|
190
|
+
envContent += `\n# Docker-compose credentials (used in docker-compose.yml)\nDB_USER=user\nDB_PASSWORD=password\nDB_NAME=${projectName}`;
|
|
367
191
|
}
|
|
368
|
-
|
|
369
192
|
await fs.writeFile(path.join(projectDir, '.env.example'), envContent);
|
|
370
|
-
|
|
193
|
+
|
|
371
194
|
} catch (error) {
|
|
372
195
|
throw error;
|
|
373
196
|
}
|
|
@@ -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,
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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").
|
|
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
|
}
|
|
@@ -4,7 +4,6 @@ package <%= group %>.<%= projectName %>.controller;
|
|
|
4
4
|
import <%= group %>.<%= projectName %>.model.User;
|
|
5
5
|
import <%= group %>.<%= projectName %>.repository.UserRepository;
|
|
6
6
|
import <%= group %>.<%= projectName %>.security.JwtService;
|
|
7
|
-
|
|
8
7
|
import org.springframework.http.ResponseEntity;
|
|
9
8
|
import org.springframework.http.HttpStatus;
|
|
10
9
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
@@ -22,41 +21,29 @@ public class AuthController {
|
|
|
22
21
|
private final JwtService jwt;
|
|
23
22
|
|
|
24
23
|
public AuthController(UserRepository repo, PasswordEncoder encoder, JwtService jwt) {
|
|
25
|
-
this.repo = repo;
|
|
26
|
-
this.encoder = encoder;
|
|
27
|
-
this.jwt = jwt;
|
|
24
|
+
this.repo = repo; this.encoder = encoder; this.jwt = jwt;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
@PostMapping("/register")
|
|
31
|
-
public ResponseEntity<?> register(@RequestBody
|
|
32
|
-
if (repo.findByEmail(req.
|
|
28
|
+
public ResponseEntity<?> register(@RequestBody User req) {
|
|
29
|
+
if (repo.findByEmail(req.getEmail()).isPresent()) {
|
|
33
30
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("User already exists");
|
|
34
31
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
user.setEmail(req.email());
|
|
39
|
-
user.setPassword(encoder.encode(req.password()));
|
|
40
|
-
repo.save(user);
|
|
41
|
-
|
|
42
|
-
String token = jwt.generateToken(user.getEmail());
|
|
32
|
+
req.setPassword(encoder.encode(req.getPassword()));
|
|
33
|
+
repo.save(req);
|
|
34
|
+
String token = jwt.generateToken(req.getEmail());
|
|
43
35
|
return ResponseEntity.status(HttpStatus.CREATED).body(new TokenResponse(token));
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
@PostMapping("/login")
|
|
47
|
-
public ResponseEntity<?> login(@RequestBody
|
|
48
|
-
Optional<User> current = repo.findByEmail(req.
|
|
39
|
+
public ResponseEntity<?> login(@RequestBody User req) {
|
|
40
|
+
Optional<User> current = repo.findByEmail(req.getEmail());
|
|
49
41
|
if (current.isEmpty()) return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid credentials");
|
|
50
|
-
|
|
51
|
-
if (!encoder.matches(req.password(), current.get().getPassword())) {
|
|
42
|
+
if (!encoder.matches(req.getPassword(), current.get().getPassword()))
|
|
52
43
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid credentials");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
String token = jwt.generateToken(current.get().getEmail());
|
|
44
|
+
String token = jwt.generateToken(req.getEmail());
|
|
56
45
|
return ResponseEntity.ok(new TokenResponse(token));
|
|
57
46
|
}
|
|
58
47
|
|
|
59
|
-
public record AuthRequest(String name, String email, String password) {}
|
|
60
|
-
public record LoginRequest(String email, String password) {}
|
|
61
48
|
public record TokenResponse(String token) {}
|
|
62
49
|
}
|