servcraft 0.1.0 → 0.1.1
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/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
package/src/cli/commands/init.ts
CHANGED
|
@@ -5,12 +5,13 @@ import ora from 'ora';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import { ensureDir, writeFile,
|
|
8
|
+
import { ensureDir, writeFile, error, warn } from '../utils/helpers.js';
|
|
9
9
|
|
|
10
10
|
interface InitOptions {
|
|
11
11
|
name: string;
|
|
12
12
|
language: 'typescript' | 'javascript';
|
|
13
13
|
database: 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'none';
|
|
14
|
+
orm: 'prisma' | 'mongoose' | 'none';
|
|
14
15
|
validator: 'zod' | 'joi' | 'yup';
|
|
15
16
|
features: string[];
|
|
16
17
|
}
|
|
@@ -23,195 +24,223 @@ export const initCommand = new Command('init')
|
|
|
23
24
|
.option('--ts, --typescript', 'Use TypeScript (default)')
|
|
24
25
|
.option('--js, --javascript', 'Use JavaScript')
|
|
25
26
|
.option('--db <database>', 'Database type (postgresql, mysql, sqlite, mongodb, none)')
|
|
26
|
-
.action(
|
|
27
|
-
|
|
27
|
+
.action(
|
|
28
|
+
async (
|
|
29
|
+
name?: string,
|
|
30
|
+
cmdOptions?: { yes?: boolean; typescript?: boolean; javascript?: boolean; db?: string }
|
|
31
|
+
) => {
|
|
32
|
+
console.log(
|
|
33
|
+
chalk.blue(`
|
|
28
34
|
╔═══════════════════════════════════════════╗
|
|
29
35
|
║ ║
|
|
30
36
|
║ ${chalk.bold('🚀 Servcraft Project Generator')} ║
|
|
31
37
|
║ ║
|
|
32
38
|
╚═══════════════════════════════════════════╝
|
|
33
|
-
`)
|
|
34
|
-
|
|
35
|
-
let options: InitOptions;
|
|
36
|
-
|
|
37
|
-
if (cmdOptions?.yes) {
|
|
38
|
-
options = {
|
|
39
|
-
name: name || 'my-servcraft-app',
|
|
40
|
-
language: cmdOptions.javascript ? 'javascript' : 'typescript',
|
|
41
|
-
database: (cmdOptions.db as InitOptions['database']) || 'postgresql',
|
|
42
|
-
validator: 'zod',
|
|
43
|
-
features: ['auth', 'users', 'email'],
|
|
44
|
-
};
|
|
45
|
-
} else {
|
|
46
|
-
const answers = await inquirer.prompt([
|
|
47
|
-
{
|
|
48
|
-
type: 'input',
|
|
49
|
-
name: 'name',
|
|
50
|
-
message: 'Project name:',
|
|
51
|
-
default: name || 'my-servcraft-app',
|
|
52
|
-
validate: (input: string) => {
|
|
53
|
-
if (!/^[a-z0-9-_]+$/i.test(input)) {
|
|
54
|
-
return 'Project name can only contain letters, numbers, hyphens, and underscores';
|
|
55
|
-
}
|
|
56
|
-
return true;
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
type: 'list',
|
|
61
|
-
name: 'language',
|
|
62
|
-
message: 'Select language:',
|
|
63
|
-
choices: [
|
|
64
|
-
{ name: 'TypeScript (Recommended)', value: 'typescript' },
|
|
65
|
-
{ name: 'JavaScript', value: 'javascript' },
|
|
66
|
-
],
|
|
67
|
-
default: 'typescript',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
type: 'list',
|
|
71
|
-
name: 'database',
|
|
72
|
-
message: 'Select database:',
|
|
73
|
-
choices: [
|
|
74
|
-
{ name: 'PostgreSQL (Recommended)', value: 'postgresql' },
|
|
75
|
-
{ name: 'MySQL', value: 'mysql' },
|
|
76
|
-
{ name: 'SQLite (Development)', value: 'sqlite' },
|
|
77
|
-
{ name: 'MongoDB', value: 'mongodb' },
|
|
78
|
-
{ name: 'None (Add later)', value: 'none' },
|
|
79
|
-
],
|
|
80
|
-
default: 'postgresql',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
type: 'list',
|
|
84
|
-
name: 'validator',
|
|
85
|
-
message: 'Select validation library:',
|
|
86
|
-
choices: [
|
|
87
|
-
{ name: 'Zod (Recommended - TypeScript-first)', value: 'zod' },
|
|
88
|
-
{ name: 'Joi (Battle-tested, feature-rich)', value: 'joi' },
|
|
89
|
-
{ name: 'Yup (Inspired by Joi, lighter)', value: 'yup' },
|
|
90
|
-
],
|
|
91
|
-
default: 'zod',
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
type: 'checkbox',
|
|
95
|
-
name: 'features',
|
|
96
|
-
message: 'Select features to include:',
|
|
97
|
-
choices: [
|
|
98
|
-
{ name: 'Authentication (JWT)', value: 'auth', checked: true },
|
|
99
|
-
{ name: 'User Management', value: 'users', checked: true },
|
|
100
|
-
{ name: 'Email Service', value: 'email', checked: true },
|
|
101
|
-
{ name: 'Audit Logs', value: 'audit', checked: false },
|
|
102
|
-
{ name: 'File Upload', value: 'upload', checked: false },
|
|
103
|
-
{ name: 'Redis Cache', value: 'redis', checked: false },
|
|
104
|
-
],
|
|
105
|
-
},
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
options = answers as InitOptions;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const projectDir = path.resolve(process.cwd(), options.name);
|
|
112
|
-
const spinner = ora('Creating project...').start();
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
// Check if directory exists
|
|
116
|
-
try {
|
|
117
|
-
await fs.access(projectDir);
|
|
118
|
-
spinner.stop();
|
|
119
|
-
error(`Directory "${options.name}" already exists`);
|
|
120
|
-
return;
|
|
121
|
-
} catch {
|
|
122
|
-
// Directory doesn't exist, continue
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Create project directory
|
|
126
|
-
await ensureDir(projectDir);
|
|
127
|
-
|
|
128
|
-
spinner.text = 'Generating project files...';
|
|
129
|
-
|
|
130
|
-
// Generate package.json
|
|
131
|
-
const packageJson = generatePackageJson(options);
|
|
132
|
-
await writeFile(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
39
|
+
`)
|
|
40
|
+
);
|
|
133
41
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
42
|
+
let options: InitOptions;
|
|
43
|
+
|
|
44
|
+
if (cmdOptions?.yes) {
|
|
45
|
+
const db = (cmdOptions.db as InitOptions['database']) || 'postgresql';
|
|
46
|
+
options = {
|
|
47
|
+
name: name || 'my-servcraft-app',
|
|
48
|
+
language: cmdOptions.javascript ? 'javascript' : 'typescript',
|
|
49
|
+
database: db,
|
|
50
|
+
orm: db === 'mongodb' ? 'mongoose' : db === 'none' ? 'none' : 'prisma',
|
|
51
|
+
validator: 'zod',
|
|
52
|
+
features: ['auth', 'users', 'email'],
|
|
53
|
+
};
|
|
138
54
|
} else {
|
|
139
|
-
await
|
|
55
|
+
const answers = await inquirer.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'name',
|
|
59
|
+
message: 'Project name:',
|
|
60
|
+
default: name || 'my-servcraft-app',
|
|
61
|
+
validate: (input: string) => {
|
|
62
|
+
if (!/^[a-z0-9-_]+$/i.test(input)) {
|
|
63
|
+
return 'Project name can only contain letters, numbers, hyphens, and underscores';
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'language',
|
|
71
|
+
message: 'Select language:',
|
|
72
|
+
choices: [
|
|
73
|
+
{ name: 'TypeScript (Recommended)', value: 'typescript' },
|
|
74
|
+
{ name: 'JavaScript', value: 'javascript' },
|
|
75
|
+
],
|
|
76
|
+
default: 'typescript',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'list',
|
|
80
|
+
name: 'database',
|
|
81
|
+
message: 'Select database:',
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: 'PostgreSQL (Recommended for SQL)', value: 'postgresql' },
|
|
84
|
+
{ name: 'MySQL', value: 'mysql' },
|
|
85
|
+
{ name: 'SQLite (Development)', value: 'sqlite' },
|
|
86
|
+
{ name: 'MongoDB (NoSQL)', value: 'mongodb' },
|
|
87
|
+
{ name: 'None (Add later)', value: 'none' },
|
|
88
|
+
],
|
|
89
|
+
default: 'postgresql',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'list',
|
|
93
|
+
name: 'validator',
|
|
94
|
+
message: 'Select validation library:',
|
|
95
|
+
choices: [
|
|
96
|
+
{ name: 'Zod (Recommended - TypeScript-first)', value: 'zod' },
|
|
97
|
+
{ name: 'Joi (Battle-tested, feature-rich)', value: 'joi' },
|
|
98
|
+
{ name: 'Yup (Inspired by Joi, lighter)', value: 'yup' },
|
|
99
|
+
],
|
|
100
|
+
default: 'zod',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'checkbox',
|
|
104
|
+
name: 'features',
|
|
105
|
+
message: 'Select features to include:',
|
|
106
|
+
choices: [
|
|
107
|
+
{ name: 'Authentication (JWT)', value: 'auth', checked: true },
|
|
108
|
+
{ name: 'User Management', value: 'users', checked: true },
|
|
109
|
+
{ name: 'Email Service', value: 'email', checked: true },
|
|
110
|
+
{ name: 'Audit Logs', value: 'audit', checked: false },
|
|
111
|
+
{ name: 'File Upload', value: 'upload', checked: false },
|
|
112
|
+
{ name: 'Redis Cache', value: 'redis', checked: false },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// Auto-determine ORM based on database choice
|
|
118
|
+
const db = answers.database as InitOptions['database'];
|
|
119
|
+
options = {
|
|
120
|
+
...answers,
|
|
121
|
+
orm: db === 'mongodb' ? 'mongoose' : db === 'none' ? 'none' : 'prisma',
|
|
122
|
+
} as InitOptions;
|
|
140
123
|
}
|
|
141
124
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await writeFile(path.join(projectDir, '.env'), generateEnvExample(options));
|
|
145
|
-
|
|
146
|
-
// Generate .gitignore
|
|
147
|
-
await writeFile(path.join(projectDir, '.gitignore'), generateGitignore());
|
|
148
|
-
|
|
149
|
-
// Generate Docker files
|
|
150
|
-
await writeFile(path.join(projectDir, 'Dockerfile'), generateDockerfile(options));
|
|
151
|
-
await writeFile(path.join(projectDir, 'docker-compose.yml'), generateDockerCompose(options));
|
|
152
|
-
|
|
153
|
-
// Create directory structure
|
|
154
|
-
const ext = options.language === 'typescript' ? 'ts' : 'js';
|
|
155
|
-
const dirs = [
|
|
156
|
-
'src/core',
|
|
157
|
-
'src/config',
|
|
158
|
-
'src/modules',
|
|
159
|
-
'src/middleware',
|
|
160
|
-
'src/utils',
|
|
161
|
-
'src/types',
|
|
162
|
-
'tests/unit',
|
|
163
|
-
'tests/integration',
|
|
164
|
-
];
|
|
165
|
-
|
|
166
|
-
if (options.database !== 'none' && options.database !== 'mongodb') {
|
|
167
|
-
dirs.push('prisma');
|
|
168
|
-
}
|
|
125
|
+
const projectDir = path.resolve(process.cwd(), options.name);
|
|
126
|
+
const spinner = ora('Creating project...').start();
|
|
169
127
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
128
|
+
try {
|
|
129
|
+
// Check if directory exists
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(projectDir);
|
|
132
|
+
spinner.stop();
|
|
133
|
+
error(`Directory "${options.name}" already exists`);
|
|
134
|
+
return;
|
|
135
|
+
} catch {
|
|
136
|
+
// Directory doesn't exist, continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create project directory
|
|
140
|
+
await ensureDir(projectDir);
|
|
141
|
+
|
|
142
|
+
spinner.text = 'Generating project files...';
|
|
143
|
+
|
|
144
|
+
// Generate package.json
|
|
145
|
+
const packageJson = generatePackageJson(options);
|
|
146
|
+
await writeFile(
|
|
147
|
+
path.join(projectDir, 'package.json'),
|
|
148
|
+
JSON.stringify(packageJson, null, 2)
|
|
149
|
+
);
|
|
173
150
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
151
|
+
// Generate tsconfig or jsconfig
|
|
152
|
+
if (options.language === 'typescript') {
|
|
153
|
+
await writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
|
|
154
|
+
await writeFile(path.join(projectDir, 'tsup.config.ts'), generateTsupConfig());
|
|
155
|
+
} else {
|
|
156
|
+
await writeFile(path.join(projectDir, 'jsconfig.json'), generateJsConfig());
|
|
157
|
+
}
|
|
179
158
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
path.join(projectDir,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
path.join(projectDir, `src/core/logger.${ext}`),
|
|
187
|
-
generateLoggerFile(options)
|
|
188
|
-
);
|
|
159
|
+
// Generate .env files
|
|
160
|
+
await writeFile(path.join(projectDir, '.env.example'), generateEnvExample(options));
|
|
161
|
+
await writeFile(path.join(projectDir, '.env'), generateEnvExample(options));
|
|
162
|
+
|
|
163
|
+
// Generate .gitignore
|
|
164
|
+
await writeFile(path.join(projectDir, '.gitignore'), generateGitignore());
|
|
189
165
|
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
// Generate Docker files
|
|
167
|
+
await writeFile(path.join(projectDir, 'Dockerfile'), generateDockerfile(options));
|
|
192
168
|
await writeFile(
|
|
193
|
-
path.join(projectDir, '
|
|
194
|
-
|
|
169
|
+
path.join(projectDir, 'docker-compose.yml'),
|
|
170
|
+
generateDockerCompose(options)
|
|
195
171
|
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
spinner.succeed('Project files generated!');
|
|
199
172
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
173
|
+
// Create directory structure
|
|
174
|
+
const ext = options.language === 'typescript' ? 'ts' : 'js';
|
|
175
|
+
const dirs = [
|
|
176
|
+
'src/core',
|
|
177
|
+
'src/config',
|
|
178
|
+
'src/modules',
|
|
179
|
+
'src/middleware',
|
|
180
|
+
'src/utils',
|
|
181
|
+
'src/types',
|
|
182
|
+
'tests/unit',
|
|
183
|
+
'tests/integration',
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
if (options.orm === 'prisma') {
|
|
187
|
+
dirs.push('prisma');
|
|
188
|
+
}
|
|
189
|
+
if (options.orm === 'mongoose') {
|
|
190
|
+
dirs.push('src/database/models');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const dir of dirs) {
|
|
194
|
+
await ensureDir(path.join(projectDir, dir));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Generate main entry file
|
|
198
|
+
await writeFile(path.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
|
|
199
|
+
|
|
200
|
+
// Generate core files
|
|
201
|
+
await writeFile(
|
|
202
|
+
path.join(projectDir, `src/core/server.${ext}`),
|
|
203
|
+
generateServerFile(options)
|
|
204
|
+
);
|
|
205
|
+
await writeFile(
|
|
206
|
+
path.join(projectDir, `src/core/logger.${ext}`),
|
|
207
|
+
generateLoggerFile(options)
|
|
208
|
+
);
|
|
210
209
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
// Generate database files based on ORM choice
|
|
211
|
+
if (options.orm === 'prisma') {
|
|
212
|
+
await writeFile(
|
|
213
|
+
path.join(projectDir, 'prisma/schema.prisma'),
|
|
214
|
+
generatePrismaSchema(options)
|
|
215
|
+
);
|
|
216
|
+
} else if (options.orm === 'mongoose') {
|
|
217
|
+
await writeFile(
|
|
218
|
+
path.join(projectDir, `src/database/connection.${ext}`),
|
|
219
|
+
generateMongooseConnection(options)
|
|
220
|
+
);
|
|
221
|
+
await writeFile(
|
|
222
|
+
path.join(projectDir, `src/database/models/user.model.${ext}`),
|
|
223
|
+
generateMongooseUserModel(options)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
spinner.succeed('Project files generated!');
|
|
228
|
+
|
|
229
|
+
// Install dependencies
|
|
230
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
|
|
234
|
+
installSpinner.succeed('Dependencies installed!');
|
|
235
|
+
} catch {
|
|
236
|
+
installSpinner.warn('Failed to install dependencies automatically');
|
|
237
|
+
warn(' Run "npm install" manually in the project directory');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Print success message
|
|
241
|
+
console.log('\n' + chalk.green('✨ Project created successfully!'));
|
|
242
|
+
console.log('\n' + chalk.bold('📁 Project structure:'));
|
|
243
|
+
console.log(`
|
|
215
244
|
${options.name}/
|
|
216
245
|
├── src/
|
|
217
246
|
│ ├── core/ # Core server, logger
|
|
@@ -226,26 +255,26 @@ export const initCommand = new Command('init')
|
|
|
226
255
|
└── package.json
|
|
227
256
|
`);
|
|
228
257
|
|
|
229
|
-
|
|
230
|
-
|
|
258
|
+
console.log(chalk.bold('🚀 Get started:'));
|
|
259
|
+
console.log(`
|
|
231
260
|
${chalk.cyan(`cd ${options.name}`)}
|
|
232
261
|
${options.database !== 'none' ? chalk.cyan('npm run db:push # Setup database') : ''}
|
|
233
262
|
${chalk.cyan('npm run dev # Start development server')}
|
|
234
263
|
`);
|
|
235
264
|
|
|
236
|
-
|
|
237
|
-
|
|
265
|
+
console.log(chalk.bold('📚 Available commands:'));
|
|
266
|
+
console.log(`
|
|
238
267
|
${chalk.yellow('servcraft generate module <name>')} Generate a new module
|
|
239
268
|
${chalk.yellow('servcraft generate controller <name>')} Generate a controller
|
|
240
269
|
${chalk.yellow('servcraft generate service <name>')} Generate a service
|
|
241
270
|
${chalk.yellow('servcraft add auth')} Add authentication module
|
|
242
271
|
`);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
272
|
+
} catch (err) {
|
|
273
|
+
spinner.fail('Failed to create project');
|
|
274
|
+
error(err instanceof Error ? err.message : String(err));
|
|
275
|
+
}
|
|
247
276
|
}
|
|
248
|
-
|
|
277
|
+
);
|
|
249
278
|
|
|
250
279
|
function generatePackageJson(options: InitOptions): Record<string, unknown> {
|
|
251
280
|
const isTS = options.language === 'typescript';
|
|
@@ -301,7 +330,7 @@ function generatePackageJson(options: InitOptions): Record<string, unknown> {
|
|
|
301
330
|
(pkg.devDependencies as Record<string, string>)['@types/bcryptjs'] = '^2.4.6';
|
|
302
331
|
}
|
|
303
332
|
|
|
304
|
-
if (options.
|
|
333
|
+
if (options.orm === 'prisma') {
|
|
305
334
|
(pkg.dependencies as Record<string, string>)['@prisma/client'] = '^5.22.0';
|
|
306
335
|
(pkg.devDependencies as Record<string, string>).prisma = '^5.22.0';
|
|
307
336
|
(pkg.scripts as Record<string, string>)['db:generate'] = 'prisma generate';
|
|
@@ -310,8 +339,11 @@ function generatePackageJson(options: InitOptions): Record<string, unknown> {
|
|
|
310
339
|
(pkg.scripts as Record<string, string>)['db:studio'] = 'prisma studio';
|
|
311
340
|
}
|
|
312
341
|
|
|
313
|
-
if (options.
|
|
342
|
+
if (options.orm === 'mongoose') {
|
|
314
343
|
(pkg.dependencies as Record<string, string>).mongoose = '^8.8.4';
|
|
344
|
+
if (isTS) {
|
|
345
|
+
(pkg.devDependencies as Record<string, string>)['@types/mongoose'] = '^5.11.97';
|
|
346
|
+
}
|
|
315
347
|
}
|
|
316
348
|
|
|
317
349
|
if (options.features.includes('email')) {
|
|
@@ -330,38 +362,46 @@ function generatePackageJson(options: InitOptions): Record<string, unknown> {
|
|
|
330
362
|
}
|
|
331
363
|
|
|
332
364
|
function generateTsConfig(): string {
|
|
333
|
-
return JSON.stringify(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
365
|
+
return JSON.stringify(
|
|
366
|
+
{
|
|
367
|
+
compilerOptions: {
|
|
368
|
+
target: 'ES2022',
|
|
369
|
+
module: 'NodeNext',
|
|
370
|
+
moduleResolution: 'NodeNext',
|
|
371
|
+
lib: ['ES2022'],
|
|
372
|
+
outDir: './dist',
|
|
373
|
+
rootDir: './src',
|
|
374
|
+
strict: true,
|
|
375
|
+
esModuleInterop: true,
|
|
376
|
+
skipLibCheck: true,
|
|
377
|
+
forceConsistentCasingInFileNames: true,
|
|
378
|
+
resolveJsonModule: true,
|
|
379
|
+
declaration: true,
|
|
380
|
+
sourceMap: true,
|
|
381
|
+
},
|
|
382
|
+
include: ['src/**/*'],
|
|
383
|
+
exclude: ['node_modules', 'dist'],
|
|
348
384
|
},
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
385
|
+
null,
|
|
386
|
+
2
|
|
387
|
+
);
|
|
352
388
|
}
|
|
353
389
|
|
|
354
390
|
function generateJsConfig(): string {
|
|
355
|
-
return JSON.stringify(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
391
|
+
return JSON.stringify(
|
|
392
|
+
{
|
|
393
|
+
compilerOptions: {
|
|
394
|
+
module: 'NodeNext',
|
|
395
|
+
moduleResolution: 'NodeNext',
|
|
396
|
+
target: 'ES2022',
|
|
397
|
+
checkJs: true,
|
|
398
|
+
},
|
|
399
|
+
include: ['src/**/*'],
|
|
400
|
+
exclude: ['node_modules'],
|
|
361
401
|
},
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
402
|
+
null,
|
|
403
|
+
2
|
|
404
|
+
);
|
|
365
405
|
}
|
|
366
406
|
|
|
367
407
|
function generateTsupConfig(): string {
|
|
@@ -517,6 +557,23 @@ volumes:
|
|
|
517
557
|
|
|
518
558
|
volumes:
|
|
519
559
|
mysql-data:
|
|
560
|
+
`;
|
|
561
|
+
} else if (options.database === 'mongodb') {
|
|
562
|
+
compose += ` - MONGODB_URI=mongodb://mongodb:27017/mydb
|
|
563
|
+
depends_on:
|
|
564
|
+
- mongodb
|
|
565
|
+
|
|
566
|
+
mongodb:
|
|
567
|
+
image: mongo:7
|
|
568
|
+
environment:
|
|
569
|
+
MONGO_INITDB_DATABASE: mydb
|
|
570
|
+
ports:
|
|
571
|
+
- "27017:27017"
|
|
572
|
+
volumes:
|
|
573
|
+
- mongodb-data:/data/db
|
|
574
|
+
|
|
575
|
+
volumes:
|
|
576
|
+
mongodb-data:
|
|
520
577
|
`;
|
|
521
578
|
}
|
|
522
579
|
|
|
@@ -583,10 +640,14 @@ main();
|
|
|
583
640
|
function generateServerFile(options: InitOptions): string {
|
|
584
641
|
const isTS = options.language === 'typescript';
|
|
585
642
|
|
|
586
|
-
return `${
|
|
643
|
+
return `${
|
|
644
|
+
isTS
|
|
645
|
+
? `import Fastify from 'fastify';
|
|
587
646
|
import type { FastifyInstance } from 'fastify';
|
|
588
|
-
import { logger } from './logger.js';`
|
|
589
|
-
const
|
|
647
|
+
import { logger } from './logger.js';`
|
|
648
|
+
: `const Fastify = require('fastify');
|
|
649
|
+
const { logger } = require('./logger.js');`
|
|
650
|
+
}
|
|
590
651
|
|
|
591
652
|
${isTS ? 'export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }' : 'function createServer()'} {
|
|
592
653
|
const app = Fastify({ logger });
|
|
@@ -638,3 +699,112 @@ ${isTS ? 'export const logger: Logger' : 'const logger'} = pino({
|
|
|
638
699
|
${isTS ? '' : 'module.exports = { logger };'}
|
|
639
700
|
`;
|
|
640
701
|
}
|
|
702
|
+
|
|
703
|
+
function generateMongooseConnection(options: InitOptions): string {
|
|
704
|
+
const isTS = options.language === 'typescript';
|
|
705
|
+
|
|
706
|
+
return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
|
|
707
|
+
|
|
708
|
+
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
|
|
709
|
+
|
|
710
|
+
${isTS ? 'export async function connectDatabase(): Promise<typeof mongoose>' : 'async function connectDatabase()'} {
|
|
711
|
+
try {
|
|
712
|
+
const conn = await mongoose.connect(MONGODB_URI);
|
|
713
|
+
logger.info(\`MongoDB connected: \${conn.connection.host}\`);
|
|
714
|
+
return conn;
|
|
715
|
+
} catch (error) {
|
|
716
|
+
logger.error({ err: error }, 'MongoDB connection failed');
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
${isTS ? 'export async function disconnectDatabase(): Promise<void>' : 'async function disconnectDatabase()'} {
|
|
722
|
+
try {
|
|
723
|
+
await mongoose.disconnect();
|
|
724
|
+
logger.info('MongoDB disconnected');
|
|
725
|
+
} catch (error) {
|
|
726
|
+
logger.error({ err: error }, 'MongoDB disconnect failed');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
${isTS ? 'export { mongoose };' : 'module.exports = { connectDatabase, disconnectDatabase, mongoose };'}
|
|
731
|
+
`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function generateMongooseUserModel(options: InitOptions): string {
|
|
735
|
+
const isTS = options.language === 'typescript';
|
|
736
|
+
|
|
737
|
+
return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
|
|
738
|
+
|
|
739
|
+
${
|
|
740
|
+
isTS
|
|
741
|
+
? `export interface IUser extends Document {
|
|
742
|
+
email: string;
|
|
743
|
+
password: string;
|
|
744
|
+
name?: string;
|
|
745
|
+
role: 'user' | 'admin';
|
|
746
|
+
status: 'active' | 'inactive' | 'suspended';
|
|
747
|
+
emailVerified: boolean;
|
|
748
|
+
createdAt: Date;
|
|
749
|
+
updatedAt: Date;
|
|
750
|
+
comparePassword(candidatePassword: string): Promise<boolean>;
|
|
751
|
+
}`
|
|
752
|
+
: ''
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const userSchema = new Schema${isTS ? '<IUser>' : ''}({
|
|
756
|
+
email: {
|
|
757
|
+
type: String,
|
|
758
|
+
required: true,
|
|
759
|
+
unique: true,
|
|
760
|
+
lowercase: true,
|
|
761
|
+
trim: true,
|
|
762
|
+
},
|
|
763
|
+
password: {
|
|
764
|
+
type: String,
|
|
765
|
+
required: true,
|
|
766
|
+
minlength: 8,
|
|
767
|
+
},
|
|
768
|
+
name: {
|
|
769
|
+
type: String,
|
|
770
|
+
trim: true,
|
|
771
|
+
},
|
|
772
|
+
role: {
|
|
773
|
+
type: String,
|
|
774
|
+
enum: ['user', 'admin'],
|
|
775
|
+
default: 'user',
|
|
776
|
+
},
|
|
777
|
+
status: {
|
|
778
|
+
type: String,
|
|
779
|
+
enum: ['active', 'inactive', 'suspended'],
|
|
780
|
+
default: 'active',
|
|
781
|
+
},
|
|
782
|
+
emailVerified: {
|
|
783
|
+
type: Boolean,
|
|
784
|
+
default: false,
|
|
785
|
+
},
|
|
786
|
+
}, {
|
|
787
|
+
timestamps: true,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Hash password before saving
|
|
791
|
+
userSchema.pre('save', async function(next) {
|
|
792
|
+
if (!this.isModified('password')) return next();
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const salt = await bcrypt.genSalt(10);
|
|
796
|
+
this.password = await bcrypt.hash(this.password, salt);
|
|
797
|
+
next();
|
|
798
|
+
} catch (error${isTS ? ': any' : ''}) {
|
|
799
|
+
next(error);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// Compare password method
|
|
804
|
+
userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ': string' : ''})${isTS ? ': Promise<boolean>' : ''} {
|
|
805
|
+
return bcrypt.compare(candidatePassword, this.password);
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
|
|
809
|
+
`;
|
|
810
|
+
}
|