create-wabck 1.0.0
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/README.md +79 -0
- package/package.json +48 -0
- package/src/generator.js +201 -0
- package/src/index.js +118 -0
- package/src/installer.js +62 -0
- package/src/prompts.js +113 -0
- package/src/templates/base.js +387 -0
- package/src/templates/config.js +271 -0
- package/src/templates/controllers.js +99 -0
- package/src/templates/docker.js +205 -0
- package/src/templates/graphql.js +157 -0
- package/src/templates/helpers.js +117 -0
- package/src/templates/index.js +78 -0
- package/src/templates/middlewares.js +294 -0
- package/src/templates/models.js +318 -0
- package/src/templates/routes.js +179 -0
- package/src/templates/services.js +252 -0
- package/src/templates/typescript.js +96 -0
- package/src/templates/utils.js +109 -0
- package/src/templates/validators.js +183 -0
- package/src/utils/fs.js +41 -0
- package/src/utils/logger.js +89 -0
- package/src/utils/pkg.js +158 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Base Templates — app.js/ts, server.js/ts, and all root config files
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const ext = (isTS) => (isTS ? 'js' : 'js'); // ESM always uses .js imports
|
|
8
|
+
|
|
9
|
+
/** Generate src/app.js or src/app.ts */
|
|
10
|
+
export function app(options) {
|
|
11
|
+
const { apiStyle, language } = options;
|
|
12
|
+
const isTS = language === 'typescript';
|
|
13
|
+
|
|
14
|
+
const typeImport = isTS ? `import { Application } from 'express';\n` : '';
|
|
15
|
+
const appType = isTS ? ': Application' : '';
|
|
16
|
+
|
|
17
|
+
const graphqlBlock =
|
|
18
|
+
apiStyle === 'graphql'
|
|
19
|
+
? `
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// GraphQL — Apollo Server 4 mounted on /graphql
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
import { expressMiddleware } from '@as-integrations/express4';
|
|
24
|
+
import { createApolloServer } from './graphql/server.js';
|
|
25
|
+
|
|
26
|
+
const apolloServer = createApolloServer();
|
|
27
|
+
await apolloServer.start();
|
|
28
|
+
app.use('/graphql', expressMiddleware(apolloServer, {
|
|
29
|
+
context: async ({ req }) => ({ req }),
|
|
30
|
+
}));
|
|
31
|
+
`
|
|
32
|
+
: '';
|
|
33
|
+
|
|
34
|
+
return `${typeImport}import express${isTS ? ', { Application }' : ''} from 'express';
|
|
35
|
+
import cors from 'cors';
|
|
36
|
+
import helmet from 'helmet';
|
|
37
|
+
import compression from 'compression';
|
|
38
|
+
import morgan from 'morgan';
|
|
39
|
+
import cookieParser from 'cookie-parser';
|
|
40
|
+
import { rateLimiter } from './middlewares/rateLimiter.middleware.js';
|
|
41
|
+
import { errorMiddleware } from './middlewares/error.middleware.js';
|
|
42
|
+
import { notFoundMiddleware } from './middlewares/notFound.middleware.js';
|
|
43
|
+
import routes from './routes/index.js';
|
|
44
|
+
|
|
45
|
+
const app${appType} = express();
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Security Middleware
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
app.use(helmet());
|
|
51
|
+
app.use(
|
|
52
|
+
cors({
|
|
53
|
+
origin: process.env.CORS_ORIGIN || '*',
|
|
54
|
+
credentials: true,
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
app.use(rateLimiter);
|
|
58
|
+
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
// Body Parsing
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
app.use(express.json({ limit: '16kb' }));
|
|
63
|
+
app.use(express.urlencoded({ extended: true, limit: '16kb' }));
|
|
64
|
+
app.use(cookieParser());
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Logging & Compression
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
app.use(compression());
|
|
70
|
+
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
|
71
|
+
${graphqlBlock}
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// API Routes — versioned under /api/v1
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
app.use('/api/v1', routes);
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
// Error Handling — Order matters: 404 must come before the error handler
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
app.use(notFoundMiddleware);
|
|
81
|
+
app.use(errorMiddleware);
|
|
82
|
+
|
|
83
|
+
export default app;
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Generate src/server.js or src/server.ts */
|
|
88
|
+
export function server(options) {
|
|
89
|
+
const { database, language, projectName } = options;
|
|
90
|
+
const isTS = language === 'typescript';
|
|
91
|
+
const hasDB = database !== 'none';
|
|
92
|
+
|
|
93
|
+
const dbImport = hasDB ? `import connectDB from './config/db.js';\n` : '';
|
|
94
|
+
const dbConnect = hasDB ? ` await connectDB();\n` : '';
|
|
95
|
+
|
|
96
|
+
return `import 'dotenv/config';
|
|
97
|
+
import app from './app.js';
|
|
98
|
+
${dbImport}
|
|
99
|
+
const PORT = process.env.PORT || 5000;
|
|
100
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Bootstrap — Connect to DB then start HTTP server
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
const startServer = async ()${isTS ? ': Promise<void>' : ''} => {
|
|
106
|
+
try {
|
|
107
|
+
${dbConnect} const server = app.listen(PORT, HOST, () => {
|
|
108
|
+
console.log(\`\`);
|
|
109
|
+
console.log(\` 🚀 ${projectName} is running!\`);
|
|
110
|
+
console.log(\` ➜ Local: http://localhost:\${PORT}\`);
|
|
111
|
+
console.log(\` ➜ Health: http://localhost:\${PORT}/api/v1/health\`);
|
|
112
|
+
console.log(\`\`);
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── Graceful Shutdown ──────────────────────────────────────────────────
|
|
117
|
+
const shutdown = (signal${isTS ? ': string' : ''}) => {
|
|
118
|
+
console.log(\`\\n Received \${signal}. Gracefully shutting down…\`);
|
|
119
|
+
server.close(() => {
|
|
120
|
+
console.log(' HTTP server closed.');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
126
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
127
|
+
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Failed to start server:', error);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
startServer();
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Generate .env */
|
|
139
|
+
export function envFile(options) {
|
|
140
|
+
const { database, auth, projectName } = options;
|
|
141
|
+
|
|
142
|
+
// Auto-generate a cryptographically secure JWT secret at scaffold time
|
|
143
|
+
const jwtSecret = randomBytes(32).toString('hex');
|
|
144
|
+
|
|
145
|
+
const dbVars = {
|
|
146
|
+
mongodb: 'MONGODB_URI=mongodb://localhost:27017/${projectName}',
|
|
147
|
+
postgresql: 'DATABASE_URL=postgresql://postgres:password@localhost:5432/${projectName}',
|
|
148
|
+
mysql: `DB_HOST=localhost\nDB_PORT=3306\nDB_USER=root\nDB_PASSWORD=password\nDB_NAME=${projectName}`,
|
|
149
|
+
sqlite: `SQLITE_PATH=./database.sqlite`,
|
|
150
|
+
none: '',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const authVars = {
|
|
154
|
+
jwt: `\nJWT_SECRET=${jwtSecret}\nJWT_EXPIRES_IN=7d\nJWT_REFRESH_EXPIRES_IN=30d`,
|
|
155
|
+
clerk: `\nCLERK_SECRET_KEY=sk_test_your-clerk-secret-key`,
|
|
156
|
+
firebase: `\nFIREBASE_PROJECT_ID=your-firebase-project-id\nFIREBASE_PRIVATE_KEY=your-firebase-private-key\nFIREBASE_CLIENT_EMAIL=your-firebase-client-email`,
|
|
157
|
+
none: '',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return `# ─────────────────────────────────────────────
|
|
161
|
+
# ${projectName} — Environment Variables
|
|
162
|
+
# ─────────────────────────────────────────────
|
|
163
|
+
# Copy this file to .env and fill in the values.
|
|
164
|
+
# JWT_SECRET below was auto-generated at scaffold time — keep it secret!
|
|
165
|
+
|
|
166
|
+
NODE_ENV=development
|
|
167
|
+
PORT=5000
|
|
168
|
+
HOST=0.0.0.0
|
|
169
|
+
CORS_ORIGIN=*
|
|
170
|
+
|
|
171
|
+
# Database
|
|
172
|
+
${(dbVars[database] || '').replace('${projectName}', projectName)}
|
|
173
|
+
${authVars[auth] || ''}
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Generate .env.example — safe to commit; uses a placeholder instead of the real secret */
|
|
178
|
+
export function envExample(options) {
|
|
179
|
+
const { database, auth, projectName } = options;
|
|
180
|
+
|
|
181
|
+
const dbVars = {
|
|
182
|
+
mongodb: `MONGODB_URI=mongodb://localhost:27017/${projectName}`,
|
|
183
|
+
postgresql: `DATABASE_URL=postgresql://postgres:password@localhost:5432/${projectName}`,
|
|
184
|
+
mysql: `DB_HOST=localhost\nDB_PORT=3306\nDB_USER=root\nDB_PASSWORD=password\nDB_NAME=${projectName}`,
|
|
185
|
+
sqlite: `SQLITE_PATH=./database.sqlite`,
|
|
186
|
+
none: '',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const authVars = {
|
|
190
|
+
jwt: `\nJWT_SECRET=change-me-generate-with-openssl-rand-hex-32\nJWT_EXPIRES_IN=7d\nJWT_REFRESH_EXPIRES_IN=30d`,
|
|
191
|
+
clerk: `\nCLERK_SECRET_KEY=sk_test_your-clerk-secret-key`,
|
|
192
|
+
firebase: `\nFIREBASE_PROJECT_ID=your-firebase-project-id\nFIREBASE_PRIVATE_KEY=your-firebase-private-key\nFIREBASE_CLIENT_EMAIL=your-firebase-client-email`,
|
|
193
|
+
none: '',
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return `# ─────────────────────────────────────────────
|
|
197
|
+
# ${projectName} — Environment Variables (EXAMPLE)
|
|
198
|
+
# ─────────────────────────────────────────────
|
|
199
|
+
# This file is safe to commit. Copy to .env and replace placeholder values.
|
|
200
|
+
# Your actual .env already has a generated JWT_SECRET.
|
|
201
|
+
|
|
202
|
+
NODE_ENV=development
|
|
203
|
+
PORT=5000
|
|
204
|
+
HOST=0.0.0.0
|
|
205
|
+
CORS_ORIGIN=*
|
|
206
|
+
|
|
207
|
+
# Database
|
|
208
|
+
${(dbVars[database] || '').replace('${projectName}', projectName)}
|
|
209
|
+
${authVars[auth] || ''}
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Generate .gitignore for the generated project */
|
|
214
|
+
export function gitignore() {
|
|
215
|
+
return `# ─── Dependencies ──────────────────────────────
|
|
216
|
+
node_modules/
|
|
217
|
+
.pnp
|
|
218
|
+
.pnp.js
|
|
219
|
+
|
|
220
|
+
# ─── Build Outputs ──────────────────────────────
|
|
221
|
+
dist/
|
|
222
|
+
build/
|
|
223
|
+
*.tsbuildinfo
|
|
224
|
+
|
|
225
|
+
# ─── Environment Variables ──────────────────────
|
|
226
|
+
.env
|
|
227
|
+
.env.local
|
|
228
|
+
.env.*.local
|
|
229
|
+
# .env.example is safe to commit
|
|
230
|
+
|
|
231
|
+
# ─── Logs ───────────────────────────────────────
|
|
232
|
+
logs/
|
|
233
|
+
*.log
|
|
234
|
+
npm-debug.log*
|
|
235
|
+
yarn-debug.log*
|
|
236
|
+
yarn-error.log*
|
|
237
|
+
pnpm-debug.log*
|
|
238
|
+
|
|
239
|
+
# ─── OS / Editor ────────────────────────────────
|
|
240
|
+
.DS_Store
|
|
241
|
+
Thumbs.db
|
|
242
|
+
.idea/
|
|
243
|
+
.vscode/
|
|
244
|
+
|
|
245
|
+
# ─── Database ───────────────────────────────────
|
|
246
|
+
*.sqlite
|
|
247
|
+
*.sqlite3
|
|
248
|
+
database.sqlite
|
|
249
|
+
|
|
250
|
+
# ─── Testing ────────────────────────────────────
|
|
251
|
+
coverage/
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Generate .prettierrc */
|
|
256
|
+
export function prettierrc() {
|
|
257
|
+
return JSON.stringify(
|
|
258
|
+
{
|
|
259
|
+
semi: true,
|
|
260
|
+
singleQuote: true,
|
|
261
|
+
trailingComma: 'es5',
|
|
262
|
+
printWidth: 100,
|
|
263
|
+
tabWidth: 2,
|
|
264
|
+
useTabs: false,
|
|
265
|
+
bracketSpacing: true,
|
|
266
|
+
arrowParens: 'always',
|
|
267
|
+
},
|
|
268
|
+
null,
|
|
269
|
+
2
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Generate eslint.config.js */
|
|
274
|
+
export function eslintConfig(options) {
|
|
275
|
+
const isTS = options.language === 'typescript';
|
|
276
|
+
return `import js from '@eslint/js';
|
|
277
|
+
import prettier from 'eslint-plugin-prettier';
|
|
278
|
+
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
279
|
+
|
|
280
|
+
export default [
|
|
281
|
+
js.configs.recommended,
|
|
282
|
+
eslintConfigPrettier,
|
|
283
|
+
{
|
|
284
|
+
plugins: { prettier },
|
|
285
|
+
rules: {
|
|
286
|
+
'prettier/prettier': 'error',
|
|
287
|
+
'no-console': 'warn',
|
|
288
|
+
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
289
|
+
'no-duplicate-imports': 'error',
|
|
290
|
+
'prefer-const': 'error',
|
|
291
|
+
'no-var': 'error',
|
|
292
|
+
},
|
|
293
|
+
languageOptions: {
|
|
294
|
+
ecmaVersion: 2022,
|
|
295
|
+
sourceType: 'module',
|
|
296
|
+
globals: {
|
|
297
|
+
process: 'readonly',
|
|
298
|
+
console: 'readonly',
|
|
299
|
+
Buffer: 'readonly',
|
|
300
|
+
URL: 'readonly',
|
|
301
|
+
setTimeout: 'readonly',
|
|
302
|
+
clearTimeout: 'readonly',
|
|
303
|
+
setInterval: 'readonly',
|
|
304
|
+
clearInterval: 'readonly',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Generate README.md */
|
|
313
|
+
export function readme(options) {
|
|
314
|
+
const { projectName, language, database, auth, validation, apiStyle, docker, packageManager } = options;
|
|
315
|
+
const isTS = language === 'typescript';
|
|
316
|
+
const pm = packageManager;
|
|
317
|
+
|
|
318
|
+
return `# ${projectName}
|
|
319
|
+
|
|
320
|
+
> Production-ready Express.js API — scaffolded by [wabck](https://github.com/wabck/wabck)
|
|
321
|
+
|
|
322
|
+
## 🚀 Quick Start
|
|
323
|
+
|
|
324
|
+
\`\`\`bash
|
|
325
|
+
# Install dependencies
|
|
326
|
+
${pm} ${pm === 'yarn' ? '' : 'install'}
|
|
327
|
+
|
|
328
|
+
# Copy environment variables
|
|
329
|
+
cp .env.example .env
|
|
330
|
+
|
|
331
|
+
# Start development server
|
|
332
|
+
${pm} run dev
|
|
333
|
+
\`\`\`
|
|
334
|
+
|
|
335
|
+
The API will be available at **http://localhost:5000**
|
|
336
|
+
|
|
337
|
+
## 📁 Project Structure
|
|
338
|
+
|
|
339
|
+
\`\`\`
|
|
340
|
+
${projectName}/
|
|
341
|
+
├── src/
|
|
342
|
+
│ ├── config/ # DB connection, env validation
|
|
343
|
+
│ ├── controllers/ # Request handlers
|
|
344
|
+
│ ├── middlewares/ # Error, auth, rate-limiter
|
|
345
|
+
│ ├── models/ # Data models
|
|
346
|
+
│ ├── routes/ # Route definitions
|
|
347
|
+
│ ├── services/ # Business logic
|
|
348
|
+
│ ├── utils/ # asyncHandler, ApiError, ApiResponse
|
|
349
|
+
${validation !== 'none' ? `│ ├── validators/ # Input validation\n` : ''}│ ├── helpers/ # Token, etc.
|
|
350
|
+
│ ├── constants/ # App-wide constants
|
|
351
|
+
│ ├── app.${isTS ? 'ts' : 'js'} # Express app
|
|
352
|
+
│ └── server.${isTS ? 'ts' : 'js'} # Entry point
|
|
353
|
+
├── .env.example
|
|
354
|
+
├── .prettierrc
|
|
355
|
+
└── eslint.config.js
|
|
356
|
+
\`\`\`
|
|
357
|
+
|
|
358
|
+
## 🛠 Stack
|
|
359
|
+
|
|
360
|
+
| Feature | Choice |
|
|
361
|
+
|---------------|---------------------------|
|
|
362
|
+
| Language | ${isTS ? 'TypeScript' : 'JavaScript (ES Modules)'} |
|
|
363
|
+
| Database | ${database === 'none' ? '—' : database} |
|
|
364
|
+
| Auth | ${auth === 'none' ? '—' : auth} |
|
|
365
|
+
${validation !== 'none' ? `| Validation | ${validation} |\n` : ''}| API Style | ${apiStyle === 'graphql' ? 'GraphQL (Apollo Server 4)' : 'REST'} |
|
|
366
|
+
| Docker | ${docker ? '✔ Dockerfile + docker-compose' : '—'} |
|
|
367
|
+
|
|
368
|
+
## 🔗 Endpoints
|
|
369
|
+
|
|
370
|
+
| Method | Path | Description |
|
|
371
|
+
|--------|--------------------|--------------------|
|
|
372
|
+
| GET | \`/\` | Root health ping |
|
|
373
|
+
| GET | \`/api/v1/health\` | Detailed health |
|
|
374
|
+
| GET | \`/api/v1/users\` | List users |
|
|
375
|
+
| POST | \`/api/v1/users\` | Create user |
|
|
376
|
+
| GET | \`/api/v1/users/:id\` | Get user by ID |
|
|
377
|
+
| PUT | \`/api/v1/users/:id\` | Update user |
|
|
378
|
+
| DELETE | \`/api/v1/users/:id\` | Delete user |
|
|
379
|
+
${auth === 'jwt' ? `| POST | \`/api/v1/auth/register\` | Register |\n| POST | \`/api/v1/auth/login\` | Login |` : ''}
|
|
380
|
+
|
|
381
|
+
${docker ? `## 🐳 Docker\n\n\`\`\`bash\ndocker compose up --build\n\`\`\`\n` : ''}
|
|
382
|
+
${isTS ? `## 🏗 Build\n\n\`\`\`bash\n${pm} run build\n${pm} start\n\`\`\`\n` : ''}
|
|
383
|
+
## 📜 License
|
|
384
|
+
|
|
385
|
+
MIT
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Config Templates — db.js, env.js, swagger.js
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/** Generate src/config/db.js or db.ts */
|
|
6
|
+
export function db(options) {
|
|
7
|
+
const { database, language } = options;
|
|
8
|
+
const isTS = language === 'typescript';
|
|
9
|
+
|
|
10
|
+
switch (database) {
|
|
11
|
+
// ── MongoDB / Mongoose ──────────────────────────────────────────────────
|
|
12
|
+
case 'mongodb':
|
|
13
|
+
return `import mongoose from 'mongoose';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Establishes a connection to MongoDB using Mongoose.
|
|
17
|
+
* Uses DATABASE_URL (or MONGODB_URI) from environment variables.
|
|
18
|
+
* Exits the process if the connection fails.
|
|
19
|
+
*/
|
|
20
|
+
const connectDB = async ()${isTS ? ': Promise<void>' : ''} => {
|
|
21
|
+
try {
|
|
22
|
+
const uri = process.env.MONGODB_URI || process.env.DATABASE_URL;
|
|
23
|
+
if (!uri) throw new Error('MONGODB_URI is not defined in .env');
|
|
24
|
+
|
|
25
|
+
const conn = await mongoose.connect(uri, {
|
|
26
|
+
// Mongoose 7+ uses the new connection engine by default
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log(\`✔ MongoDB connected: \${conn.connection.host}\`);
|
|
30
|
+
|
|
31
|
+
// ── Mongoose connection events ────────────────────────────────────────
|
|
32
|
+
mongoose.connection.on('error', (err) => {
|
|
33
|
+
console.error('MongoDB connection error:', err);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
mongoose.connection.on('disconnected', () => {
|
|
37
|
+
console.warn('MongoDB disconnected');
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('MongoDB connection failed:', error${isTS ? ' as Error' : ''}.message || error);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default connectDB;
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// ── PostgreSQL / Prisma ─────────────────────────────────────────────────
|
|
49
|
+
case 'postgresql':
|
|
50
|
+
return `import { PrismaClient } from '@prisma/client';
|
|
51
|
+
|
|
52
|
+
const globalForPrisma = globalThis${isTS ? ' as unknown as { prisma: PrismaClient }' : ''};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* PrismaClient singleton — prevents multiple instances in development
|
|
56
|
+
* due to hot-reloading (Next.js pattern, works for Node as well).
|
|
57
|
+
*/
|
|
58
|
+
export const prisma =
|
|
59
|
+
globalForPrisma.prisma ??
|
|
60
|
+
new PrismaClient({
|
|
61
|
+
log: process.env.NODE_ENV === 'development'
|
|
62
|
+
? ['query', 'info', 'warn', 'error']
|
|
63
|
+
: ['error'],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
67
|
+
globalForPrisma.prisma = prisma;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Establishes a connection to PostgreSQL via Prisma.
|
|
72
|
+
* Exits the process if the connection fails.
|
|
73
|
+
*/
|
|
74
|
+
const connectDB = async ()${isTS ? ': Promise<void>' : ''} => {
|
|
75
|
+
try {
|
|
76
|
+
await prisma.$connect();
|
|
77
|
+
console.log('✔ PostgreSQL connected via Prisma');
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('PostgreSQL connection failed:', error${isTS ? ' as Error' : ''}.message || error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default connectDB;
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
// ── MySQL / mysql2 ──────────────────────────────────────────────────────
|
|
88
|
+
case 'mysql':
|
|
89
|
+
return `import mysql from 'mysql2/promise';
|
|
90
|
+
|
|
91
|
+
let pool${isTS ? ': mysql.Pool' : ''};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a mysql2 connection pool and verifies connectivity.
|
|
95
|
+
* Pool is exported for use across the application.
|
|
96
|
+
*/
|
|
97
|
+
const connectDB = async ()${isTS ? ': Promise<void>' : ''} => {
|
|
98
|
+
try {
|
|
99
|
+
pool = mysql.createPool({
|
|
100
|
+
host: process.env.DB_HOST || 'localhost',
|
|
101
|
+
port: Number(process.env.DB_PORT) || 3306,
|
|
102
|
+
user: process.env.DB_USER || 'root',
|
|
103
|
+
password: process.env.DB_PASSWORD || '',
|
|
104
|
+
database: process.env.DB_NAME || 'mydb',
|
|
105
|
+
waitForConnections: true,
|
|
106
|
+
connectionLimit: 10,
|
|
107
|
+
queueLimit: 0,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Verify the connection
|
|
111
|
+
await pool.query('SELECT 1');
|
|
112
|
+
console.log(\`✔ MySQL connected — db: \${process.env.DB_NAME || 'mydb'}\`);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('MySQL connection failed:', error${isTS ? ' as Error' : ''}.message || error);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export { pool };
|
|
120
|
+
export default connectDB;
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
// ── SQLite / better-sqlite3 ─────────────────────────────────────────────
|
|
124
|
+
case 'sqlite':
|
|
125
|
+
return `import Database from 'better-sqlite3';
|
|
126
|
+
import path from 'path';
|
|
127
|
+
import { fileURLToPath } from 'url';
|
|
128
|
+
|
|
129
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
130
|
+
const __dirname = path.dirname(__filename);
|
|
131
|
+
|
|
132
|
+
let db${isTS ? ': Database.Database' : ''};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Opens the SQLite database file and runs a quick smoke test.
|
|
136
|
+
* better-sqlite3 is synchronous — no await needed for queries.
|
|
137
|
+
*/
|
|
138
|
+
const connectDB = async ()${isTS ? ': Promise<void>' : ''} => {
|
|
139
|
+
try {
|
|
140
|
+
const dbPath = process.env.SQLITE_PATH
|
|
141
|
+
? path.resolve(process.env.SQLITE_PATH)
|
|
142
|
+
: path.join(__dirname, '..', '..', 'database.sqlite');
|
|
143
|
+
|
|
144
|
+
db = new Database(dbPath);
|
|
145
|
+
|
|
146
|
+
// Enable WAL mode for better concurrent read performance
|
|
147
|
+
db.pragma('journal_mode = WAL');
|
|
148
|
+
|
|
149
|
+
// Smoke-test the connection
|
|
150
|
+
db.prepare('SELECT 1').get();
|
|
151
|
+
console.log(\`✔ SQLite connected — \${dbPath}\`);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('SQLite connection failed:', error${isTS ? ' as Error' : ''}.message || error);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export { db };
|
|
159
|
+
export default connectDB;
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
default:
|
|
163
|
+
return `// No database selected — this file is intentionally empty.\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Generate src/config/env.js — validates required environment variables */
|
|
168
|
+
export function envConfig(options) {
|
|
169
|
+
const { database, auth, language } = options;
|
|
170
|
+
const isTS = language === 'typescript';
|
|
171
|
+
|
|
172
|
+
const requiredVars = [`'PORT'`, `'NODE_ENV'`];
|
|
173
|
+
if (database === 'mongodb') requiredVars.push(`'MONGODB_URI'`);
|
|
174
|
+
if (database === 'postgresql') requiredVars.push(`'DATABASE_URL'`);
|
|
175
|
+
if (database === 'mysql') {
|
|
176
|
+
requiredVars.push(`'DB_HOST'`, `'DB_USER'`, `'DB_NAME'`);
|
|
177
|
+
}
|
|
178
|
+
if (auth === 'jwt') requiredVars.push(`'JWT_SECRET'`);
|
|
179
|
+
if (auth === 'clerk') requiredVars.push(`'CLERK_SECRET_KEY'`);
|
|
180
|
+
if (auth === 'firebase') requiredVars.push(`'FIREBASE_PROJECT_ID'`, `'FIREBASE_PRIVATE_KEY'`, `'FIREBASE_CLIENT_EMAIL'`);
|
|
181
|
+
|
|
182
|
+
return `/**
|
|
183
|
+
* Environment Configuration & Validation
|
|
184
|
+
*
|
|
185
|
+
* Validates that all required environment variables are present at startup.
|
|
186
|
+
* Import this module early (before any DB or server init).
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
const REQUIRED_VARS = [${requiredVars.join(', ')}];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Validates environment variables and exits if any are missing.
|
|
193
|
+
*/
|
|
194
|
+
export function validateEnv()${isTS ? ': void' : ''} {
|
|
195
|
+
const missing = REQUIRED_VARS.filter((key) => !process.env[key]);
|
|
196
|
+
|
|
197
|
+
if (missing.length > 0) {
|
|
198
|
+
console.error('\\n Missing required environment variables:');
|
|
199
|
+
missing.forEach((key) => console.error(\` ✖ \${key}\`));
|
|
200
|
+
console.error('\\n Check your .env file.\\n');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Typed environment config object for convenient access.
|
|
207
|
+
*/
|
|
208
|
+
export const config = {
|
|
209
|
+
port: Number(process.env.PORT) || 5000,
|
|
210
|
+
host: process.env.HOST || '0.0.0.0',
|
|
211
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
212
|
+
isProd: process.env.NODE_ENV === 'production',
|
|
213
|
+
corsOrigin: process.env.CORS_ORIGIN || '*',
|
|
214
|
+
${database === 'mongodb' ? " mongoUri: process.env.MONGODB_URI || '',\n" : ''}${database === 'postgresql' ? " databaseUrl: process.env.DATABASE_URL || '',\n" : ''}${auth === 'jwt' ? " jwtSecret: process.env.JWT_SECRET || '',\n jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',\n" : ''}};
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Generate src/config/swagger.js */
|
|
219
|
+
export function swagger(options) {
|
|
220
|
+
const { projectName, language } = options;
|
|
221
|
+
const isTS = language === 'typescript';
|
|
222
|
+
|
|
223
|
+
return `import swaggerJsdoc from 'swagger-jsdoc';
|
|
224
|
+
import { readFileSync } from 'fs';
|
|
225
|
+
import { fileURLToPath } from 'url';
|
|
226
|
+
import path from 'path';
|
|
227
|
+
|
|
228
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
229
|
+
const __dirname = path.dirname(__filename);
|
|
230
|
+
|
|
231
|
+
// Read version from package.json
|
|
232
|
+
let version = '1.0.0';
|
|
233
|
+
try {
|
|
234
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
235
|
+
version = pkg.version;
|
|
236
|
+
} catch { /* use default */ }
|
|
237
|
+
|
|
238
|
+
const options = {
|
|
239
|
+
definition: {
|
|
240
|
+
openapi: '3.0.0',
|
|
241
|
+
info: {
|
|
242
|
+
title: '${projectName} API',
|
|
243
|
+
version,
|
|
244
|
+
description: 'Auto-generated API documentation for ${projectName}',
|
|
245
|
+
license: { name: 'MIT' },
|
|
246
|
+
contact: { name: '${projectName} Support' },
|
|
247
|
+
},
|
|
248
|
+
servers: [
|
|
249
|
+
{ url: \`http://localhost:\${process.env.PORT || 5000}/api/v1\`, description: 'Development' },
|
|
250
|
+
],
|
|
251
|
+
components: {
|
|
252
|
+
securitySchemes: {
|
|
253
|
+
bearerAuth: {
|
|
254
|
+
type: 'http',
|
|
255
|
+
scheme: 'bearer',
|
|
256
|
+
bearerFormat: 'JWT',
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
security: [{ bearerAuth: [] }],
|
|
261
|
+
},
|
|
262
|
+
// Pick up JSDoc @swagger annotations from all route files
|
|
263
|
+
apis: [
|
|
264
|
+
path.join(__dirname, '../routes/*.js'),
|
|
265
|
+
path.join(__dirname, '../routes/*.ts'),
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const swaggerSpec = swaggerJsdoc(options);
|
|
270
|
+
`;
|
|
271
|
+
}
|