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