servcraft 0.1.0 → 0.1.3
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 +30 -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/LICENSE +21 -0
- package/README.md +1102 -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/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command5 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/cli/commands/init.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -73,159 +73,183 @@ function getModulesDir() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// src/cli/commands/init.ts
|
|
76
|
-
var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
|
|
77
|
-
|
|
76
|
+
var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
|
|
77
|
+
async (name, cmdOptions) => {
|
|
78
|
+
console.log(
|
|
79
|
+
chalk2.blue(`
|
|
78
80
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
79
81
|
\u2551 \u2551
|
|
80
82
|
\u2551 ${chalk2.bold("\u{1F680} Servcraft Project Generator")} \u2551
|
|
81
83
|
\u2551 \u2551
|
|
82
84
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
83
|
-
`)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
85
|
+
`)
|
|
86
|
+
);
|
|
87
|
+
let options;
|
|
88
|
+
if (cmdOptions?.yes) {
|
|
89
|
+
const db = cmdOptions.db || "postgresql";
|
|
90
|
+
options = {
|
|
91
|
+
name: name || "my-servcraft-app",
|
|
92
|
+
language: cmdOptions.javascript ? "javascript" : "typescript",
|
|
93
|
+
database: db,
|
|
94
|
+
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma",
|
|
95
|
+
validator: "zod",
|
|
96
|
+
features: ["auth", "users", "email"]
|
|
97
|
+
};
|
|
98
|
+
} else {
|
|
99
|
+
const answers = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: "input",
|
|
102
|
+
name: "name",
|
|
103
|
+
message: "Project name:",
|
|
104
|
+
default: name || "my-servcraft-app",
|
|
105
|
+
validate: (input) => {
|
|
106
|
+
if (!/^[a-z0-9-_]+$/i.test(input)) {
|
|
107
|
+
return "Project name can only contain letters, numbers, hyphens, and underscores";
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
103
110
|
}
|
|
104
|
-
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: "list",
|
|
114
|
+
name: "language",
|
|
115
|
+
message: "Select language:",
|
|
116
|
+
choices: [
|
|
117
|
+
{ name: "TypeScript (Recommended)", value: "typescript" },
|
|
118
|
+
{ name: "JavaScript", value: "javascript" }
|
|
119
|
+
],
|
|
120
|
+
default: "typescript"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: "list",
|
|
124
|
+
name: "database",
|
|
125
|
+
message: "Select database:",
|
|
126
|
+
choices: [
|
|
127
|
+
{ name: "PostgreSQL (Recommended for SQL)", value: "postgresql" },
|
|
128
|
+
{ name: "MySQL", value: "mysql" },
|
|
129
|
+
{ name: "SQLite (Development)", value: "sqlite" },
|
|
130
|
+
{ name: "MongoDB (NoSQL)", value: "mongodb" },
|
|
131
|
+
{ name: "None (Add later)", value: "none" }
|
|
132
|
+
],
|
|
133
|
+
default: "postgresql"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: "list",
|
|
137
|
+
name: "validator",
|
|
138
|
+
message: "Select validation library:",
|
|
139
|
+
choices: [
|
|
140
|
+
{ name: "Zod (Recommended - TypeScript-first)", value: "zod" },
|
|
141
|
+
{ name: "Joi (Battle-tested, feature-rich)", value: "joi" },
|
|
142
|
+
{ name: "Yup (Inspired by Joi, lighter)", value: "yup" }
|
|
143
|
+
],
|
|
144
|
+
default: "zod"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "checkbox",
|
|
148
|
+
name: "features",
|
|
149
|
+
message: "Select features to include:",
|
|
150
|
+
choices: [
|
|
151
|
+
{ name: "Authentication (JWT)", value: "auth", checked: true },
|
|
152
|
+
{ name: "User Management", value: "users", checked: true },
|
|
153
|
+
{ name: "Email Service", value: "email", checked: true },
|
|
154
|
+
{ name: "Audit Logs", value: "audit", checked: false },
|
|
155
|
+
{ name: "File Upload", value: "upload", checked: false },
|
|
156
|
+
{ name: "Redis Cache", value: "redis", checked: false }
|
|
157
|
+
]
|
|
105
158
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{ name: "TypeScript (Recommended)", value: "typescript" },
|
|
113
|
-
{ name: "JavaScript", value: "javascript" }
|
|
114
|
-
],
|
|
115
|
-
default: "typescript"
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
type: "list",
|
|
119
|
-
name: "database",
|
|
120
|
-
message: "Select database:",
|
|
121
|
-
choices: [
|
|
122
|
-
{ name: "PostgreSQL (Recommended)", value: "postgresql" },
|
|
123
|
-
{ name: "MySQL", value: "mysql" },
|
|
124
|
-
{ name: "SQLite (Development)", value: "sqlite" },
|
|
125
|
-
{ name: "MongoDB", value: "mongodb" },
|
|
126
|
-
{ name: "None (Add later)", value: "none" }
|
|
127
|
-
],
|
|
128
|
-
default: "postgresql"
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
type: "list",
|
|
132
|
-
name: "validator",
|
|
133
|
-
message: "Select validation library:",
|
|
134
|
-
choices: [
|
|
135
|
-
{ name: "Zod (Recommended - TypeScript-first)", value: "zod" },
|
|
136
|
-
{ name: "Joi (Battle-tested, feature-rich)", value: "joi" },
|
|
137
|
-
{ name: "Yup (Inspired by Joi, lighter)", value: "yup" }
|
|
138
|
-
],
|
|
139
|
-
default: "zod"
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
type: "checkbox",
|
|
143
|
-
name: "features",
|
|
144
|
-
message: "Select features to include:",
|
|
145
|
-
choices: [
|
|
146
|
-
{ name: "Authentication (JWT)", value: "auth", checked: true },
|
|
147
|
-
{ name: "User Management", value: "users", checked: true },
|
|
148
|
-
{ name: "Email Service", value: "email", checked: true },
|
|
149
|
-
{ name: "Audit Logs", value: "audit", checked: false },
|
|
150
|
-
{ name: "File Upload", value: "upload", checked: false },
|
|
151
|
-
{ name: "Redis Cache", value: "redis", checked: false }
|
|
152
|
-
]
|
|
153
|
-
}
|
|
154
|
-
]);
|
|
155
|
-
options = answers;
|
|
156
|
-
}
|
|
157
|
-
const projectDir = path2.resolve(process.cwd(), options.name);
|
|
158
|
-
const spinner = ora("Creating project...").start();
|
|
159
|
-
try {
|
|
160
|
-
try {
|
|
161
|
-
await fs2.access(projectDir);
|
|
162
|
-
spinner.stop();
|
|
163
|
-
error(`Directory "${options.name}" already exists`);
|
|
164
|
-
return;
|
|
165
|
-
} catch {
|
|
166
|
-
}
|
|
167
|
-
await ensureDir(projectDir);
|
|
168
|
-
spinner.text = "Generating project files...";
|
|
169
|
-
const packageJson = generatePackageJson(options);
|
|
170
|
-
await writeFile(path2.join(projectDir, "package.json"), JSON.stringify(packageJson, null, 2));
|
|
171
|
-
if (options.language === "typescript") {
|
|
172
|
-
await writeFile(path2.join(projectDir, "tsconfig.json"), generateTsConfig());
|
|
173
|
-
await writeFile(path2.join(projectDir, "tsup.config.ts"), generateTsupConfig());
|
|
174
|
-
} else {
|
|
175
|
-
await writeFile(path2.join(projectDir, "jsconfig.json"), generateJsConfig());
|
|
176
|
-
}
|
|
177
|
-
await writeFile(path2.join(projectDir, ".env.example"), generateEnvExample(options));
|
|
178
|
-
await writeFile(path2.join(projectDir, ".env"), generateEnvExample(options));
|
|
179
|
-
await writeFile(path2.join(projectDir, ".gitignore"), generateGitignore());
|
|
180
|
-
await writeFile(path2.join(projectDir, "Dockerfile"), generateDockerfile(options));
|
|
181
|
-
await writeFile(path2.join(projectDir, "docker-compose.yml"), generateDockerCompose(options));
|
|
182
|
-
const ext = options.language === "typescript" ? "ts" : "js";
|
|
183
|
-
const dirs = [
|
|
184
|
-
"src/core",
|
|
185
|
-
"src/config",
|
|
186
|
-
"src/modules",
|
|
187
|
-
"src/middleware",
|
|
188
|
-
"src/utils",
|
|
189
|
-
"src/types",
|
|
190
|
-
"tests/unit",
|
|
191
|
-
"tests/integration"
|
|
192
|
-
];
|
|
193
|
-
if (options.database !== "none" && options.database !== "mongodb") {
|
|
194
|
-
dirs.push("prisma");
|
|
195
|
-
}
|
|
196
|
-
for (const dir of dirs) {
|
|
197
|
-
await ensureDir(path2.join(projectDir, dir));
|
|
159
|
+
]);
|
|
160
|
+
const db = answers.database;
|
|
161
|
+
options = {
|
|
162
|
+
...answers,
|
|
163
|
+
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
|
|
164
|
+
};
|
|
198
165
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
166
|
+
const projectDir = path2.resolve(process.cwd(), options.name);
|
|
167
|
+
const spinner = ora("Creating project...").start();
|
|
168
|
+
try {
|
|
169
|
+
try {
|
|
170
|
+
await fs2.access(projectDir);
|
|
171
|
+
spinner.stop();
|
|
172
|
+
error(`Directory "${options.name}" already exists`);
|
|
173
|
+
return;
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
await ensureDir(projectDir);
|
|
177
|
+
spinner.text = "Generating project files...";
|
|
178
|
+
const packageJson = generatePackageJson(options);
|
|
212
179
|
await writeFile(
|
|
213
|
-
path2.join(projectDir, "
|
|
214
|
-
|
|
180
|
+
path2.join(projectDir, "package.json"),
|
|
181
|
+
JSON.stringify(packageJson, null, 2)
|
|
215
182
|
);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
183
|
+
if (options.language === "typescript") {
|
|
184
|
+
await writeFile(path2.join(projectDir, "tsconfig.json"), generateTsConfig());
|
|
185
|
+
await writeFile(path2.join(projectDir, "tsup.config.ts"), generateTsupConfig());
|
|
186
|
+
} else {
|
|
187
|
+
await writeFile(path2.join(projectDir, "jsconfig.json"), generateJsConfig());
|
|
188
|
+
}
|
|
189
|
+
await writeFile(path2.join(projectDir, ".env.example"), generateEnvExample(options));
|
|
190
|
+
await writeFile(path2.join(projectDir, ".env"), generateEnvExample(options));
|
|
191
|
+
await writeFile(path2.join(projectDir, ".gitignore"), generateGitignore());
|
|
192
|
+
await writeFile(path2.join(projectDir, "Dockerfile"), generateDockerfile(options));
|
|
193
|
+
await writeFile(
|
|
194
|
+
path2.join(projectDir, "docker-compose.yml"),
|
|
195
|
+
generateDockerCompose(options)
|
|
196
|
+
);
|
|
197
|
+
const ext = options.language === "typescript" ? "ts" : "js";
|
|
198
|
+
const dirs = [
|
|
199
|
+
"src/core",
|
|
200
|
+
"src/config",
|
|
201
|
+
"src/modules",
|
|
202
|
+
"src/middleware",
|
|
203
|
+
"src/utils",
|
|
204
|
+
"src/types",
|
|
205
|
+
"tests/unit",
|
|
206
|
+
"tests/integration"
|
|
207
|
+
];
|
|
208
|
+
if (options.orm === "prisma") {
|
|
209
|
+
dirs.push("prisma");
|
|
210
|
+
}
|
|
211
|
+
if (options.orm === "mongoose") {
|
|
212
|
+
dirs.push("src/database/models");
|
|
213
|
+
}
|
|
214
|
+
for (const dir of dirs) {
|
|
215
|
+
await ensureDir(path2.join(projectDir, dir));
|
|
216
|
+
}
|
|
217
|
+
await writeFile(path2.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
|
|
218
|
+
await writeFile(
|
|
219
|
+
path2.join(projectDir, `src/core/server.${ext}`),
|
|
220
|
+
generateServerFile(options)
|
|
221
|
+
);
|
|
222
|
+
await writeFile(
|
|
223
|
+
path2.join(projectDir, `src/core/logger.${ext}`),
|
|
224
|
+
generateLoggerFile(options)
|
|
225
|
+
);
|
|
226
|
+
if (options.orm === "prisma") {
|
|
227
|
+
await writeFile(
|
|
228
|
+
path2.join(projectDir, "prisma/schema.prisma"),
|
|
229
|
+
generatePrismaSchema(options)
|
|
230
|
+
);
|
|
231
|
+
} else if (options.orm === "mongoose") {
|
|
232
|
+
await writeFile(
|
|
233
|
+
path2.join(projectDir, `src/database/connection.${ext}`),
|
|
234
|
+
generateMongooseConnection(options)
|
|
235
|
+
);
|
|
236
|
+
await writeFile(
|
|
237
|
+
path2.join(projectDir, `src/database/models/user.model.${ext}`),
|
|
238
|
+
generateMongooseUserModel(options)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
spinner.succeed("Project files generated!");
|
|
242
|
+
const installSpinner = ora("Installing dependencies...").start();
|
|
243
|
+
try {
|
|
244
|
+
execSync("npm install", { cwd: projectDir, stdio: "pipe" });
|
|
245
|
+
installSpinner.succeed("Dependencies installed!");
|
|
246
|
+
} catch {
|
|
247
|
+
installSpinner.warn("Failed to install dependencies automatically");
|
|
248
|
+
warn(' Run "npm install" manually in the project directory');
|
|
249
|
+
}
|
|
250
|
+
console.log("\n" + chalk2.green("\u2728 Project created successfully!"));
|
|
251
|
+
console.log("\n" + chalk2.bold("\u{1F4C1} Project structure:"));
|
|
252
|
+
console.log(`
|
|
229
253
|
${options.name}/
|
|
230
254
|
\u251C\u2500\u2500 src/
|
|
231
255
|
\u2502 \u251C\u2500\u2500 core/ # Core server, logger
|
|
@@ -239,24 +263,25 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
239
263
|
\u251C\u2500\u2500 docker-compose.yml
|
|
240
264
|
\u2514\u2500\u2500 package.json
|
|
241
265
|
`);
|
|
242
|
-
|
|
243
|
-
|
|
266
|
+
console.log(chalk2.bold("\u{1F680} Get started:"));
|
|
267
|
+
console.log(`
|
|
244
268
|
${chalk2.cyan(`cd ${options.name}`)}
|
|
245
269
|
${options.database !== "none" ? chalk2.cyan("npm run db:push # Setup database") : ""}
|
|
246
270
|
${chalk2.cyan("npm run dev # Start development server")}
|
|
247
271
|
`);
|
|
248
|
-
|
|
249
|
-
|
|
272
|
+
console.log(chalk2.bold("\u{1F4DA} Available commands:"));
|
|
273
|
+
console.log(`
|
|
250
274
|
${chalk2.yellow("servcraft generate module <name>")} Generate a new module
|
|
251
275
|
${chalk2.yellow("servcraft generate controller <name>")} Generate a controller
|
|
252
276
|
${chalk2.yellow("servcraft generate service <name>")} Generate a service
|
|
253
277
|
${chalk2.yellow("servcraft add auth")} Add authentication module
|
|
254
278
|
`);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
279
|
+
} catch (err) {
|
|
280
|
+
spinner.fail("Failed to create project");
|
|
281
|
+
error(err instanceof Error ? err.message : String(err));
|
|
282
|
+
}
|
|
258
283
|
}
|
|
259
|
-
|
|
284
|
+
);
|
|
260
285
|
function generatePackageJson(options) {
|
|
261
286
|
const isTS = options.language === "typescript";
|
|
262
287
|
const pkg = {
|
|
@@ -306,7 +331,7 @@ function generatePackageJson(options) {
|
|
|
306
331
|
pkg.devDependencies["@types/node"] = "^22.10.1";
|
|
307
332
|
pkg.devDependencies["@types/bcryptjs"] = "^2.4.6";
|
|
308
333
|
}
|
|
309
|
-
if (options.
|
|
334
|
+
if (options.orm === "prisma") {
|
|
310
335
|
pkg.dependencies["@prisma/client"] = "^5.22.0";
|
|
311
336
|
pkg.devDependencies.prisma = "^5.22.0";
|
|
312
337
|
pkg.scripts["db:generate"] = "prisma generate";
|
|
@@ -314,8 +339,11 @@ function generatePackageJson(options) {
|
|
|
314
339
|
pkg.scripts["db:push"] = "prisma db push";
|
|
315
340
|
pkg.scripts["db:studio"] = "prisma studio";
|
|
316
341
|
}
|
|
317
|
-
if (options.
|
|
342
|
+
if (options.orm === "mongoose") {
|
|
318
343
|
pkg.dependencies.mongoose = "^8.8.4";
|
|
344
|
+
if (isTS) {
|
|
345
|
+
pkg.devDependencies["@types/mongoose"] = "^5.11.97";
|
|
346
|
+
}
|
|
319
347
|
}
|
|
320
348
|
if (options.features.includes("email")) {
|
|
321
349
|
pkg.dependencies.nodemailer = "^6.9.15";
|
|
@@ -330,37 +358,45 @@ function generatePackageJson(options) {
|
|
|
330
358
|
return pkg;
|
|
331
359
|
}
|
|
332
360
|
function generateTsConfig() {
|
|
333
|
-
return JSON.stringify(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
361
|
+
return JSON.stringify(
|
|
362
|
+
{
|
|
363
|
+
compilerOptions: {
|
|
364
|
+
target: "ES2022",
|
|
365
|
+
module: "NodeNext",
|
|
366
|
+
moduleResolution: "NodeNext",
|
|
367
|
+
lib: ["ES2022"],
|
|
368
|
+
outDir: "./dist",
|
|
369
|
+
rootDir: "./src",
|
|
370
|
+
strict: true,
|
|
371
|
+
esModuleInterop: true,
|
|
372
|
+
skipLibCheck: true,
|
|
373
|
+
forceConsistentCasingInFileNames: true,
|
|
374
|
+
resolveJsonModule: true,
|
|
375
|
+
declaration: true,
|
|
376
|
+
sourceMap: true
|
|
377
|
+
},
|
|
378
|
+
include: ["src/**/*"],
|
|
379
|
+
exclude: ["node_modules", "dist"]
|
|
348
380
|
},
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
381
|
+
null,
|
|
382
|
+
2
|
|
383
|
+
);
|
|
352
384
|
}
|
|
353
385
|
function generateJsConfig() {
|
|
354
|
-
return JSON.stringify(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
386
|
+
return JSON.stringify(
|
|
387
|
+
{
|
|
388
|
+
compilerOptions: {
|
|
389
|
+
module: "NodeNext",
|
|
390
|
+
moduleResolution: "NodeNext",
|
|
391
|
+
target: "ES2022",
|
|
392
|
+
checkJs: true
|
|
393
|
+
},
|
|
394
|
+
include: ["src/**/*"],
|
|
395
|
+
exclude: ["node_modules"]
|
|
360
396
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
397
|
+
null,
|
|
398
|
+
2
|
|
399
|
+
);
|
|
364
400
|
}
|
|
365
401
|
function generateTsupConfig() {
|
|
366
402
|
return `import { defineConfig } from 'tsup';
|
|
@@ -376,7 +412,7 @@ export default defineConfig({
|
|
|
376
412
|
`;
|
|
377
413
|
}
|
|
378
414
|
function generateEnvExample(options) {
|
|
379
|
-
let
|
|
415
|
+
let env = `# Server
|
|
380
416
|
NODE_ENV=development
|
|
381
417
|
PORT=3000
|
|
382
418
|
HOST=0.0.0.0
|
|
@@ -394,31 +430,31 @@ RATE_LIMIT_MAX=100
|
|
|
394
430
|
LOG_LEVEL=info
|
|
395
431
|
`;
|
|
396
432
|
if (options.database === "postgresql") {
|
|
397
|
-
|
|
433
|
+
env += `
|
|
398
434
|
# Database (PostgreSQL)
|
|
399
435
|
DATABASE_PROVIDER=postgresql
|
|
400
436
|
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
|
|
401
437
|
`;
|
|
402
438
|
} else if (options.database === "mysql") {
|
|
403
|
-
|
|
439
|
+
env += `
|
|
404
440
|
# Database (MySQL)
|
|
405
441
|
DATABASE_PROVIDER=mysql
|
|
406
442
|
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
|
|
407
443
|
`;
|
|
408
444
|
} else if (options.database === "sqlite") {
|
|
409
|
-
|
|
445
|
+
env += `
|
|
410
446
|
# Database (SQLite)
|
|
411
447
|
DATABASE_PROVIDER=sqlite
|
|
412
448
|
DATABASE_URL="file:./dev.db"
|
|
413
449
|
`;
|
|
414
450
|
} else if (options.database === "mongodb") {
|
|
415
|
-
|
|
451
|
+
env += `
|
|
416
452
|
# Database (MongoDB)
|
|
417
453
|
MONGODB_URI="mongodb://localhost:27017/mydb"
|
|
418
454
|
`;
|
|
419
455
|
}
|
|
420
456
|
if (options.features.includes("email")) {
|
|
421
|
-
|
|
457
|
+
env += `
|
|
422
458
|
# Email
|
|
423
459
|
SMTP_HOST=smtp.example.com
|
|
424
460
|
SMTP_PORT=587
|
|
@@ -428,12 +464,12 @@ SMTP_FROM="App <noreply@example.com>"
|
|
|
428
464
|
`;
|
|
429
465
|
}
|
|
430
466
|
if (options.features.includes("redis")) {
|
|
431
|
-
|
|
467
|
+
env += `
|
|
432
468
|
# Redis
|
|
433
469
|
REDIS_URL=redis://localhost:6379
|
|
434
470
|
`;
|
|
435
471
|
}
|
|
436
|
-
return
|
|
472
|
+
return env;
|
|
437
473
|
}
|
|
438
474
|
function generateGitignore() {
|
|
439
475
|
return `node_modules/
|
|
@@ -505,6 +541,23 @@ volumes:
|
|
|
505
541
|
|
|
506
542
|
volumes:
|
|
507
543
|
mysql-data:
|
|
544
|
+
`;
|
|
545
|
+
} else if (options.database === "mongodb") {
|
|
546
|
+
compose += ` - MONGODB_URI=mongodb://mongodb:27017/mydb
|
|
547
|
+
depends_on:
|
|
548
|
+
- mongodb
|
|
549
|
+
|
|
550
|
+
mongodb:
|
|
551
|
+
image: mongo:7
|
|
552
|
+
environment:
|
|
553
|
+
MONGO_INITDB_DATABASE: mydb
|
|
554
|
+
ports:
|
|
555
|
+
- "27017:27017"
|
|
556
|
+
volumes:
|
|
557
|
+
- mongodb-data:/data/db
|
|
558
|
+
|
|
559
|
+
volumes:
|
|
560
|
+
mongodb-data:
|
|
508
561
|
`;
|
|
509
562
|
}
|
|
510
563
|
if (options.features.includes("redis")) {
|
|
@@ -616,11 +669,112 @@ ${isTS ? "export const logger: Logger" : "const logger"} = pino({
|
|
|
616
669
|
${isTS ? "" : "module.exports = { logger };"}
|
|
617
670
|
`;
|
|
618
671
|
}
|
|
672
|
+
function generateMongooseConnection(options) {
|
|
673
|
+
const isTS = options.language === "typescript";
|
|
674
|
+
return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
|
|
675
|
+
|
|
676
|
+
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
|
|
677
|
+
|
|
678
|
+
${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "async function connectDatabase()"} {
|
|
679
|
+
try {
|
|
680
|
+
const conn = await mongoose.connect(MONGODB_URI);
|
|
681
|
+
logger.info(\`MongoDB connected: \${conn.connection.host}\`);
|
|
682
|
+
return conn;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
logger.error({ err: error }, 'MongoDB connection failed');
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
${isTS ? "export async function disconnectDatabase(): Promise<void>" : "async function disconnectDatabase()"} {
|
|
690
|
+
try {
|
|
691
|
+
await mongoose.disconnect();
|
|
692
|
+
logger.info('MongoDB disconnected');
|
|
693
|
+
} catch (error) {
|
|
694
|
+
logger.error({ err: error }, 'MongoDB disconnect failed');
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
${isTS ? "export { mongoose };" : "module.exports = { connectDatabase, disconnectDatabase, mongoose };"}
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
function generateMongooseUserModel(options) {
|
|
702
|
+
const isTS = options.language === "typescript";
|
|
703
|
+
return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
|
|
704
|
+
|
|
705
|
+
${isTS ? `export interface IUser extends Document {
|
|
706
|
+
email: string;
|
|
707
|
+
password: string;
|
|
708
|
+
name?: string;
|
|
709
|
+
role: 'user' | 'admin';
|
|
710
|
+
status: 'active' | 'inactive' | 'suspended';
|
|
711
|
+
emailVerified: boolean;
|
|
712
|
+
createdAt: Date;
|
|
713
|
+
updatedAt: Date;
|
|
714
|
+
comparePassword(candidatePassword: string): Promise<boolean>;
|
|
715
|
+
}` : ""}
|
|
716
|
+
|
|
717
|
+
const userSchema = new Schema${isTS ? "<IUser>" : ""}({
|
|
718
|
+
email: {
|
|
719
|
+
type: String,
|
|
720
|
+
required: true,
|
|
721
|
+
unique: true,
|
|
722
|
+
lowercase: true,
|
|
723
|
+
trim: true,
|
|
724
|
+
},
|
|
725
|
+
password: {
|
|
726
|
+
type: String,
|
|
727
|
+
required: true,
|
|
728
|
+
minlength: 8,
|
|
729
|
+
},
|
|
730
|
+
name: {
|
|
731
|
+
type: String,
|
|
732
|
+
trim: true,
|
|
733
|
+
},
|
|
734
|
+
role: {
|
|
735
|
+
type: String,
|
|
736
|
+
enum: ['user', 'admin'],
|
|
737
|
+
default: 'user',
|
|
738
|
+
},
|
|
739
|
+
status: {
|
|
740
|
+
type: String,
|
|
741
|
+
enum: ['active', 'inactive', 'suspended'],
|
|
742
|
+
default: 'active',
|
|
743
|
+
},
|
|
744
|
+
emailVerified: {
|
|
745
|
+
type: Boolean,
|
|
746
|
+
default: false,
|
|
747
|
+
},
|
|
748
|
+
}, {
|
|
749
|
+
timestamps: true,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Hash password before saving
|
|
753
|
+
userSchema.pre('save', async function(next) {
|
|
754
|
+
if (!this.isModified('password')) return next();
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
const salt = await bcrypt.genSalt(10);
|
|
758
|
+
this.password = await bcrypt.hash(this.password, salt);
|
|
759
|
+
next();
|
|
760
|
+
} catch (error${isTS ? ": any" : ""}) {
|
|
761
|
+
next(error);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Compare password method
|
|
766
|
+
userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ": string" : ""})${isTS ? ": Promise<boolean>" : ""} {
|
|
767
|
+
return bcrypt.compare(candidatePassword, this.password);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
|
|
771
|
+
`;
|
|
772
|
+
}
|
|
619
773
|
|
|
620
774
|
// src/cli/commands/generate.ts
|
|
621
775
|
import { Command as Command2 } from "commander";
|
|
622
|
-
import
|
|
623
|
-
import
|
|
776
|
+
import path3 from "path";
|
|
777
|
+
import ora2 from "ora";
|
|
624
778
|
import inquirer2 from "inquirer";
|
|
625
779
|
|
|
626
780
|
// src/cli/utils/field-parser.ts
|
|
@@ -1058,23 +1212,11 @@ export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
|
|
|
1058
1212
|
}
|
|
1059
1213
|
|
|
1060
1214
|
// src/cli/templates/routes.ts
|
|
1061
|
-
function routesTemplate(name, pascalName, camelName, pluralName
|
|
1062
|
-
const serializedFields = JSON.stringify(fields, null, 2);
|
|
1215
|
+
function routesTemplate(name, pascalName, camelName, pluralName) {
|
|
1063
1216
|
return `import type { FastifyInstance } from 'fastify';
|
|
1064
1217
|
import type { ${pascalName}Controller } from './${name}.controller.js';
|
|
1065
1218
|
import type { AuthService } from '../auth/auth.service.js';
|
|
1066
1219
|
import { createAuthMiddleware, createRoleMiddleware } from '../auth/auth.middleware.js';
|
|
1067
|
-
import { generateRouteSchema } from '../swagger/schema-builder.js';
|
|
1068
|
-
import type { FieldDefinition } from '../cli/utils/field-parser.js';
|
|
1069
|
-
|
|
1070
|
-
const ${camelName}Fields: FieldDefinition[] = ${serializedFields};
|
|
1071
|
-
const ${camelName}Schemas = {
|
|
1072
|
-
list: generateRouteSchema('${pascalName}', ${camelName}Fields, 'list'),
|
|
1073
|
-
get: generateRouteSchema('${pascalName}', ${camelName}Fields, 'get'),
|
|
1074
|
-
create: generateRouteSchema('${pascalName}', ${camelName}Fields, 'create'),
|
|
1075
|
-
update: generateRouteSchema('${pascalName}', ${camelName}Fields, 'update'),
|
|
1076
|
-
delete: generateRouteSchema('${pascalName}', ${camelName}Fields, 'delete'),
|
|
1077
|
-
};
|
|
1078
1220
|
|
|
1079
1221
|
export function register${pascalName}Routes(
|
|
1080
1222
|
app: FastifyInstance,
|
|
@@ -1090,31 +1232,31 @@ export function register${pascalName}Routes(
|
|
|
1090
1232
|
// Protected routes
|
|
1091
1233
|
app.get(
|
|
1092
1234
|
'/${pluralName}',
|
|
1093
|
-
{ preHandler: [authenticate]
|
|
1235
|
+
{ preHandler: [authenticate] },
|
|
1094
1236
|
controller.list.bind(controller)
|
|
1095
1237
|
);
|
|
1096
1238
|
|
|
1097
1239
|
app.get(
|
|
1098
1240
|
'/${pluralName}/:id',
|
|
1099
|
-
{ preHandler: [authenticate]
|
|
1241
|
+
{ preHandler: [authenticate] },
|
|
1100
1242
|
controller.getById.bind(controller)
|
|
1101
1243
|
);
|
|
1102
1244
|
|
|
1103
1245
|
app.post(
|
|
1104
1246
|
'/${pluralName}',
|
|
1105
|
-
{ preHandler: [authenticate]
|
|
1247
|
+
{ preHandler: [authenticate] },
|
|
1106
1248
|
controller.create.bind(controller)
|
|
1107
1249
|
);
|
|
1108
1250
|
|
|
1109
1251
|
app.patch(
|
|
1110
1252
|
'/${pluralName}/:id',
|
|
1111
|
-
{ preHandler: [authenticate]
|
|
1253
|
+
{ preHandler: [authenticate] },
|
|
1112
1254
|
controller.update.bind(controller)
|
|
1113
1255
|
);
|
|
1114
1256
|
|
|
1115
1257
|
app.delete(
|
|
1116
1258
|
'/${pluralName}/:id',
|
|
1117
|
-
{ preHandler: [authenticate, isAdmin]
|
|
1259
|
+
{ preHandler: [authenticate, isAdmin] },
|
|
1118
1260
|
controller.delete.bind(controller)
|
|
1119
1261
|
);
|
|
1120
1262
|
}
|
|
@@ -1465,1735 +1607,74 @@ ${indexLines.join("\n")}
|
|
|
1465
1607
|
`;
|
|
1466
1608
|
}
|
|
1467
1609
|
|
|
1468
|
-
// src/cli/
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
var defaultConfig = {
|
|
1479
|
-
level: process.env.LOG_LEVEL || "info",
|
|
1480
|
-
pretty: process.env.NODE_ENV !== "production",
|
|
1481
|
-
name: "servcraft"
|
|
1482
|
-
};
|
|
1483
|
-
function createLogger(config2 = {}) {
|
|
1484
|
-
const mergedConfig = { ...defaultConfig, ...config2 };
|
|
1485
|
-
const transport = mergedConfig.pretty ? {
|
|
1486
|
-
target: "pino-pretty",
|
|
1487
|
-
options: {
|
|
1488
|
-
colorize: true,
|
|
1489
|
-
translateTime: "SYS:standard",
|
|
1490
|
-
ignore: "pid,hostname"
|
|
1491
|
-
}
|
|
1492
|
-
} : void 0;
|
|
1493
|
-
return pino({
|
|
1494
|
-
name: mergedConfig.name,
|
|
1495
|
-
level: mergedConfig.level,
|
|
1496
|
-
transport,
|
|
1497
|
-
formatters: {
|
|
1498
|
-
level: (label) => ({ level: label })
|
|
1499
|
-
},
|
|
1500
|
-
timestamp: pino.stdTimeFunctions.isoTime
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
var logger = createLogger();
|
|
1504
|
-
|
|
1505
|
-
// src/core/server.ts
|
|
1506
|
-
var defaultConfig2 = {
|
|
1507
|
-
port: parseInt(process.env.PORT || "3000", 10),
|
|
1508
|
-
host: process.env.HOST || "0.0.0.0",
|
|
1509
|
-
trustProxy: true,
|
|
1510
|
-
bodyLimit: 1048576,
|
|
1511
|
-
// 1MB
|
|
1512
|
-
requestTimeout: 3e4
|
|
1513
|
-
// 30s
|
|
1514
|
-
};
|
|
1515
|
-
var Server = class {
|
|
1516
|
-
app;
|
|
1517
|
-
config;
|
|
1518
|
-
logger;
|
|
1519
|
-
isShuttingDown = false;
|
|
1520
|
-
constructor(config2 = {}) {
|
|
1521
|
-
this.config = { ...defaultConfig2, ...config2 };
|
|
1522
|
-
this.logger = this.config.logger || logger;
|
|
1523
|
-
const fastifyOptions = {
|
|
1524
|
-
logger: this.logger,
|
|
1525
|
-
trustProxy: this.config.trustProxy,
|
|
1526
|
-
bodyLimit: this.config.bodyLimit,
|
|
1527
|
-
requestTimeout: this.config.requestTimeout
|
|
1528
|
-
};
|
|
1529
|
-
this.app = Fastify(fastifyOptions);
|
|
1530
|
-
this.setupHealthCheck();
|
|
1531
|
-
this.setupGracefulShutdown();
|
|
1532
|
-
}
|
|
1533
|
-
get instance() {
|
|
1534
|
-
return this.app;
|
|
1535
|
-
}
|
|
1536
|
-
setupHealthCheck() {
|
|
1537
|
-
this.app.get("/health", async (_request, reply) => {
|
|
1538
|
-
const healthcheck = {
|
|
1539
|
-
status: "ok",
|
|
1540
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1541
|
-
uptime: process.uptime(),
|
|
1542
|
-
memory: process.memoryUsage(),
|
|
1543
|
-
version: process.env.npm_package_version || "0.1.0"
|
|
1544
|
-
};
|
|
1545
|
-
return reply.status(200).send(healthcheck);
|
|
1546
|
-
});
|
|
1547
|
-
this.app.get("/ready", async (_request, reply) => {
|
|
1548
|
-
if (this.isShuttingDown) {
|
|
1549
|
-
return reply.status(503).send({ status: "shutting_down" });
|
|
1550
|
-
}
|
|
1551
|
-
return reply.status(200).send({ status: "ready" });
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
setupGracefulShutdown() {
|
|
1555
|
-
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
1556
|
-
signals.forEach((signal) => {
|
|
1557
|
-
process.on(signal, async () => {
|
|
1558
|
-
this.logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
1559
|
-
await this.shutdown();
|
|
1560
|
-
});
|
|
1561
|
-
});
|
|
1562
|
-
process.on("uncaughtException", async (error2) => {
|
|
1563
|
-
this.logger.error({ err: error2 }, "Uncaught exception");
|
|
1564
|
-
await this.shutdown(1);
|
|
1565
|
-
});
|
|
1566
|
-
process.on("unhandledRejection", async (reason) => {
|
|
1567
|
-
this.logger.error({ err: reason }, "Unhandled rejection");
|
|
1568
|
-
await this.shutdown(1);
|
|
1569
|
-
});
|
|
1610
|
+
// src/cli/commands/generate.ts
|
|
1611
|
+
var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
1612
|
+
generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
1613
|
+
"Generate a complete module with controller, service, repository, types, schemas, and routes"
|
|
1614
|
+
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").action(async (name, fieldsArgs, options) => {
|
|
1615
|
+
let fields = [];
|
|
1616
|
+
if (options.interactive) {
|
|
1617
|
+
fields = await promptForFields();
|
|
1618
|
+
} else if (fieldsArgs.length > 0) {
|
|
1619
|
+
fields = parseFields(fieldsArgs.join(" "));
|
|
1570
1620
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1621
|
+
const spinner = ora2("Generating module...").start();
|
|
1622
|
+
try {
|
|
1623
|
+
const kebabName = toKebabCase(name);
|
|
1624
|
+
const pascalName = toPascalCase(name);
|
|
1625
|
+
const camelName = toCamelCase(name);
|
|
1626
|
+
const pluralName = pluralize(kebabName);
|
|
1627
|
+
const tableName = pluralize(kebabName.replace(/-/g, "_"));
|
|
1628
|
+
const validatorType = options.validator || "zod";
|
|
1629
|
+
const moduleDir = path3.join(getModulesDir(), kebabName);
|
|
1630
|
+
if (await fileExists(moduleDir)) {
|
|
1631
|
+
spinner.stop();
|
|
1632
|
+
error(`Module "${kebabName}" already exists`);
|
|
1573
1633
|
return;
|
|
1574
1634
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1635
|
+
const hasFields = fields.length > 0;
|
|
1636
|
+
const files = [
|
|
1637
|
+
{
|
|
1638
|
+
name: `${kebabName}.types.ts`,
|
|
1639
|
+
content: hasFields ? dynamicTypesTemplate(kebabName, pascalName, fields) : typesTemplate(kebabName, pascalName)
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
name: `${kebabName}.schemas.ts`,
|
|
1643
|
+
content: hasFields ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType) : schemasTemplate(kebabName, pascalName, camelName)
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
name: `${kebabName}.service.ts`,
|
|
1647
|
+
content: serviceTemplate(kebabName, pascalName, camelName)
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
name: `${kebabName}.controller.ts`,
|
|
1651
|
+
content: controllerTemplate(kebabName, pascalName, camelName)
|
|
1652
|
+
},
|
|
1653
|
+
{ name: "index.ts", content: moduleIndexTemplate(kebabName, pascalName, camelName) }
|
|
1654
|
+
];
|
|
1655
|
+
if (options.repository !== false) {
|
|
1656
|
+
files.push({
|
|
1657
|
+
name: `${kebabName}.repository.ts`,
|
|
1658
|
+
content: repositoryTemplate(kebabName, pascalName, camelName, pluralName)
|
|
1659
|
+
});
|
|
1590
1660
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
port: this.config.port,
|
|
1596
|
-
host: this.config.host
|
|
1661
|
+
if (options.routes !== false) {
|
|
1662
|
+
files.push({
|
|
1663
|
+
name: `${kebabName}.routes.ts`,
|
|
1664
|
+
content: routesTemplate(kebabName, pascalName, camelName, pluralName)
|
|
1597
1665
|
});
|
|
1598
|
-
this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
|
|
1599
|
-
} catch (error2) {
|
|
1600
|
-
this.logger.error({ err: error2 }, "Failed to start server");
|
|
1601
|
-
throw error2;
|
|
1602
1666
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
function createServer(config2 = {}) {
|
|
1606
|
-
return new Server(config2);
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// src/utils/errors.ts
|
|
1610
|
-
var AppError = class _AppError extends Error {
|
|
1611
|
-
statusCode;
|
|
1612
|
-
isOperational;
|
|
1613
|
-
errors;
|
|
1614
|
-
constructor(message, statusCode = 500, isOperational = true, errors) {
|
|
1615
|
-
super(message);
|
|
1616
|
-
this.statusCode = statusCode;
|
|
1617
|
-
this.isOperational = isOperational;
|
|
1618
|
-
this.errors = errors;
|
|
1619
|
-
Object.setPrototypeOf(this, _AppError.prototype);
|
|
1620
|
-
Error.captureStackTrace(this, this.constructor);
|
|
1621
|
-
}
|
|
1622
|
-
};
|
|
1623
|
-
var NotFoundError = class extends AppError {
|
|
1624
|
-
constructor(resource = "Resource") {
|
|
1625
|
-
super(`${resource} not found`, 404);
|
|
1626
|
-
}
|
|
1627
|
-
};
|
|
1628
|
-
var UnauthorizedError = class extends AppError {
|
|
1629
|
-
constructor(message = "Unauthorized") {
|
|
1630
|
-
super(message, 401);
|
|
1631
|
-
}
|
|
1632
|
-
};
|
|
1633
|
-
var ForbiddenError = class extends AppError {
|
|
1634
|
-
constructor(message = "Forbidden") {
|
|
1635
|
-
super(message, 403);
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
var BadRequestError = class extends AppError {
|
|
1639
|
-
constructor(message = "Bad request", errors) {
|
|
1640
|
-
super(message, 400, true, errors);
|
|
1641
|
-
}
|
|
1642
|
-
};
|
|
1643
|
-
var ConflictError = class extends AppError {
|
|
1644
|
-
constructor(message = "Resource already exists") {
|
|
1645
|
-
super(message, 409);
|
|
1646
|
-
}
|
|
1647
|
-
};
|
|
1648
|
-
var ValidationError = class extends AppError {
|
|
1649
|
-
constructor(errors) {
|
|
1650
|
-
super("Validation failed", 422, true, errors);
|
|
1651
|
-
}
|
|
1652
|
-
};
|
|
1653
|
-
function isAppError(error2) {
|
|
1654
|
-
return error2 instanceof AppError;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// src/config/env.ts
|
|
1658
|
-
import { z } from "zod";
|
|
1659
|
-
import dotenv from "dotenv";
|
|
1660
|
-
dotenv.config();
|
|
1661
|
-
var envSchema = z.object({
|
|
1662
|
-
// Server
|
|
1663
|
-
NODE_ENV: z.enum(["development", "staging", "production", "test"]).default("development"),
|
|
1664
|
-
PORT: z.string().transform(Number).default("3000"),
|
|
1665
|
-
HOST: z.string().default("0.0.0.0"),
|
|
1666
|
-
// Database
|
|
1667
|
-
DATABASE_URL: z.string().optional(),
|
|
1668
|
-
// JWT
|
|
1669
|
-
JWT_SECRET: z.string().min(32).optional(),
|
|
1670
|
-
JWT_ACCESS_EXPIRES_IN: z.string().default("15m"),
|
|
1671
|
-
JWT_REFRESH_EXPIRES_IN: z.string().default("7d"),
|
|
1672
|
-
// Security
|
|
1673
|
-
CORS_ORIGIN: z.string().default("*"),
|
|
1674
|
-
RATE_LIMIT_MAX: z.string().transform(Number).default("100"),
|
|
1675
|
-
RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default("60000"),
|
|
1676
|
-
// Email
|
|
1677
|
-
SMTP_HOST: z.string().optional(),
|
|
1678
|
-
SMTP_PORT: z.string().transform(Number).optional(),
|
|
1679
|
-
SMTP_USER: z.string().optional(),
|
|
1680
|
-
SMTP_PASS: z.string().optional(),
|
|
1681
|
-
SMTP_FROM: z.string().optional(),
|
|
1682
|
-
// Redis (optional)
|
|
1683
|
-
REDIS_URL: z.string().optional(),
|
|
1684
|
-
// Swagger/OpenAPI
|
|
1685
|
-
SWAGGER_ENABLED: z.union([z.literal("true"), z.literal("false")]).default("true").transform((val) => val === "true"),
|
|
1686
|
-
SWAGGER_ROUTE: z.string().default("/docs"),
|
|
1687
|
-
SWAGGER_TITLE: z.string().default("Servcraft API"),
|
|
1688
|
-
SWAGGER_DESCRIPTION: z.string().default("API documentation"),
|
|
1689
|
-
SWAGGER_VERSION: z.string().default("1.0.0"),
|
|
1690
|
-
// Logging
|
|
1691
|
-
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
|
|
1692
|
-
});
|
|
1693
|
-
function validateEnv() {
|
|
1694
|
-
const parsed = envSchema.safeParse(process.env);
|
|
1695
|
-
if (!parsed.success) {
|
|
1696
|
-
logger.error({ errors: parsed.error.flatten().fieldErrors }, "Invalid environment variables");
|
|
1697
|
-
throw new Error("Invalid environment variables");
|
|
1698
|
-
}
|
|
1699
|
-
return parsed.data;
|
|
1700
|
-
}
|
|
1701
|
-
var env = validateEnv();
|
|
1702
|
-
function isProduction() {
|
|
1703
|
-
return env.NODE_ENV === "production";
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// src/config/index.ts
|
|
1707
|
-
function parseCorsOrigin(origin) {
|
|
1708
|
-
if (origin === "*") return "*";
|
|
1709
|
-
if (origin.includes(",")) {
|
|
1710
|
-
return origin.split(",").map((o) => o.trim());
|
|
1711
|
-
}
|
|
1712
|
-
return origin;
|
|
1713
|
-
}
|
|
1714
|
-
function createConfig() {
|
|
1715
|
-
return {
|
|
1716
|
-
env,
|
|
1717
|
-
server: {
|
|
1718
|
-
port: env.PORT,
|
|
1719
|
-
host: env.HOST
|
|
1720
|
-
},
|
|
1721
|
-
jwt: {
|
|
1722
|
-
secret: env.JWT_SECRET || "change-me-in-production-please-32chars",
|
|
1723
|
-
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
1724
|
-
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN
|
|
1725
|
-
},
|
|
1726
|
-
security: {
|
|
1727
|
-
corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
|
|
1728
|
-
rateLimit: {
|
|
1729
|
-
max: env.RATE_LIMIT_MAX,
|
|
1730
|
-
windowMs: env.RATE_LIMIT_WINDOW_MS
|
|
1731
|
-
}
|
|
1732
|
-
},
|
|
1733
|
-
email: {
|
|
1734
|
-
host: env.SMTP_HOST,
|
|
1735
|
-
port: env.SMTP_PORT,
|
|
1736
|
-
user: env.SMTP_USER,
|
|
1737
|
-
pass: env.SMTP_PASS,
|
|
1738
|
-
from: env.SMTP_FROM
|
|
1739
|
-
},
|
|
1740
|
-
database: {
|
|
1741
|
-
url: env.DATABASE_URL
|
|
1742
|
-
},
|
|
1743
|
-
redis: {
|
|
1744
|
-
url: env.REDIS_URL
|
|
1745
|
-
},
|
|
1746
|
-
swagger: {
|
|
1747
|
-
enabled: env.SWAGGER_ENABLED,
|
|
1748
|
-
route: env.SWAGGER_ROUTE,
|
|
1749
|
-
title: env.SWAGGER_TITLE,
|
|
1750
|
-
description: env.SWAGGER_DESCRIPTION,
|
|
1751
|
-
version: env.SWAGGER_VERSION
|
|
1667
|
+
for (const file of files) {
|
|
1668
|
+
await writeFile(path3.join(moduleDir, file.name), file.content);
|
|
1752
1669
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
logger.error(
|
|
1762
|
-
{
|
|
1763
|
-
err: error2,
|
|
1764
|
-
requestId: request.id,
|
|
1765
|
-
method: request.method,
|
|
1766
|
-
url: request.url
|
|
1767
|
-
},
|
|
1768
|
-
"Request error"
|
|
1769
|
-
);
|
|
1770
|
-
if (isAppError(error2)) {
|
|
1771
|
-
return reply.status(error2.statusCode).send({
|
|
1772
|
-
success: false,
|
|
1773
|
-
message: error2.message,
|
|
1774
|
-
errors: error2.errors,
|
|
1775
|
-
...isProduction() ? {} : { stack: error2.stack }
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
if ("validation" in error2 && error2.validation) {
|
|
1779
|
-
const errors = {};
|
|
1780
|
-
for (const err of error2.validation) {
|
|
1781
|
-
const field = err.instancePath?.replace("/", "") || "body";
|
|
1782
|
-
if (!errors[field]) {
|
|
1783
|
-
errors[field] = [];
|
|
1784
|
-
}
|
|
1785
|
-
errors[field].push(err.message || "Invalid value");
|
|
1786
|
-
}
|
|
1787
|
-
return reply.status(400).send({
|
|
1788
|
-
success: false,
|
|
1789
|
-
message: "Validation failed",
|
|
1790
|
-
errors
|
|
1791
|
-
});
|
|
1792
|
-
}
|
|
1793
|
-
if ("statusCode" in error2 && typeof error2.statusCode === "number") {
|
|
1794
|
-
return reply.status(error2.statusCode).send({
|
|
1795
|
-
success: false,
|
|
1796
|
-
message: error2.message,
|
|
1797
|
-
...isProduction() ? {} : { stack: error2.stack }
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
return reply.status(500).send({
|
|
1801
|
-
success: false,
|
|
1802
|
-
message: isProduction() ? "Internal server error" : error2.message,
|
|
1803
|
-
...isProduction() ? {} : { stack: error2.stack }
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
);
|
|
1807
|
-
app.setNotFoundHandler((request, reply) => {
|
|
1808
|
-
return reply.status(404).send({
|
|
1809
|
-
success: false,
|
|
1810
|
-
message: `Route ${request.method} ${request.url} not found`
|
|
1811
|
-
});
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
// src/middleware/security.ts
|
|
1816
|
-
import helmet from "@fastify/helmet";
|
|
1817
|
-
import cors from "@fastify/cors";
|
|
1818
|
-
import rateLimit from "@fastify/rate-limit";
|
|
1819
|
-
var defaultOptions = {
|
|
1820
|
-
helmet: true,
|
|
1821
|
-
cors: true,
|
|
1822
|
-
rateLimit: true
|
|
1823
|
-
};
|
|
1824
|
-
async function registerSecurity(app, options = {}) {
|
|
1825
|
-
const opts = { ...defaultOptions, ...options };
|
|
1826
|
-
if (opts.helmet) {
|
|
1827
|
-
await app.register(helmet, {
|
|
1828
|
-
contentSecurityPolicy: {
|
|
1829
|
-
directives: {
|
|
1830
|
-
defaultSrc: ["'self'"],
|
|
1831
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
1832
|
-
scriptSrc: ["'self'"],
|
|
1833
|
-
imgSrc: ["'self'", "data:", "https:"]
|
|
1834
|
-
}
|
|
1835
|
-
},
|
|
1836
|
-
crossOriginEmbedderPolicy: false
|
|
1837
|
-
});
|
|
1838
|
-
logger.debug("Helmet security headers enabled");
|
|
1839
|
-
}
|
|
1840
|
-
if (opts.cors) {
|
|
1841
|
-
await app.register(cors, {
|
|
1842
|
-
origin: config.security.corsOrigin,
|
|
1843
|
-
credentials: true,
|
|
1844
|
-
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
1845
|
-
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
|
1846
|
-
exposedHeaders: ["X-Total-Count", "X-Page", "X-Limit"],
|
|
1847
|
-
maxAge: 86400
|
|
1848
|
-
// 24 hours
|
|
1849
|
-
});
|
|
1850
|
-
logger.debug({ origin: config.security.corsOrigin }, "CORS enabled");
|
|
1851
|
-
}
|
|
1852
|
-
if (opts.rateLimit) {
|
|
1853
|
-
await app.register(rateLimit, {
|
|
1854
|
-
max: config.security.rateLimit.max,
|
|
1855
|
-
timeWindow: config.security.rateLimit.windowMs,
|
|
1856
|
-
errorResponseBuilder: (_request, context) => ({
|
|
1857
|
-
success: false,
|
|
1858
|
-
message: "Too many requests, please try again later",
|
|
1859
|
-
retryAfter: context.after
|
|
1860
|
-
}),
|
|
1861
|
-
keyGenerator: (request) => {
|
|
1862
|
-
return request.headers["x-forwarded-for"]?.toString().split(",")[0] || request.ip || "unknown";
|
|
1863
|
-
}
|
|
1864
|
-
});
|
|
1865
|
-
logger.debug(
|
|
1866
|
-
{
|
|
1867
|
-
max: config.security.rateLimit.max,
|
|
1868
|
-
windowMs: config.security.rateLimit.windowMs
|
|
1869
|
-
},
|
|
1870
|
-
"Rate limiting enabled"
|
|
1871
|
-
);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
// src/modules/swagger/swagger.service.ts
|
|
1876
|
-
import swagger from "@fastify/swagger";
|
|
1877
|
-
import swaggerUi from "@fastify/swagger-ui";
|
|
1878
|
-
var defaultConfig3 = {
|
|
1879
|
-
enabled: true,
|
|
1880
|
-
route: "/docs",
|
|
1881
|
-
title: "Servcraft API",
|
|
1882
|
-
description: "API documentation generated by Servcraft",
|
|
1883
|
-
version: "1.0.0",
|
|
1884
|
-
tags: [
|
|
1885
|
-
{ name: "Auth", description: "Authentication endpoints" },
|
|
1886
|
-
{ name: "Users", description: "User management endpoints" },
|
|
1887
|
-
{ name: "Health", description: "Health check endpoints" }
|
|
1888
|
-
]
|
|
1889
|
-
};
|
|
1890
|
-
async function registerSwagger(app, customConfig) {
|
|
1891
|
-
const swaggerConfig = { ...defaultConfig3, ...customConfig };
|
|
1892
|
-
if (swaggerConfig.enabled === false) {
|
|
1893
|
-
logger.info("Swagger documentation disabled");
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
await app.register(swagger, {
|
|
1897
|
-
openapi: {
|
|
1898
|
-
openapi: "3.0.3",
|
|
1899
|
-
info: {
|
|
1900
|
-
title: swaggerConfig.title,
|
|
1901
|
-
description: swaggerConfig.description,
|
|
1902
|
-
version: swaggerConfig.version,
|
|
1903
|
-
contact: swaggerConfig.contact,
|
|
1904
|
-
license: swaggerConfig.license
|
|
1905
|
-
},
|
|
1906
|
-
servers: swaggerConfig.servers || [
|
|
1907
|
-
{
|
|
1908
|
-
url: `http://localhost:${config.server.port}`,
|
|
1909
|
-
description: "Development server"
|
|
1910
|
-
}
|
|
1911
|
-
],
|
|
1912
|
-
tags: swaggerConfig.tags,
|
|
1913
|
-
components: {
|
|
1914
|
-
securitySchemes: {
|
|
1915
|
-
bearerAuth: {
|
|
1916
|
-
type: "http",
|
|
1917
|
-
scheme: "bearer",
|
|
1918
|
-
bearerFormat: "JWT",
|
|
1919
|
-
description: "Enter your JWT token"
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
});
|
|
1925
|
-
await app.register(swaggerUi, {
|
|
1926
|
-
routePrefix: swaggerConfig.route || "/docs",
|
|
1927
|
-
uiConfig: {
|
|
1928
|
-
docExpansion: "list",
|
|
1929
|
-
deepLinking: true,
|
|
1930
|
-
displayRequestDuration: true,
|
|
1931
|
-
filter: true,
|
|
1932
|
-
showExtensions: true,
|
|
1933
|
-
showCommonExtensions: true
|
|
1934
|
-
},
|
|
1935
|
-
staticCSP: true,
|
|
1936
|
-
transformStaticCSP: (header) => header
|
|
1937
|
-
});
|
|
1938
|
-
logger.info("Swagger documentation registered at /docs");
|
|
1939
|
-
}
|
|
1940
|
-
var commonResponses = {
|
|
1941
|
-
success: {
|
|
1942
|
-
type: "object",
|
|
1943
|
-
properties: {
|
|
1944
|
-
success: { type: "boolean", example: true },
|
|
1945
|
-
data: { type: "object" }
|
|
1946
|
-
}
|
|
1947
|
-
},
|
|
1948
|
-
error: {
|
|
1949
|
-
type: "object",
|
|
1950
|
-
properties: {
|
|
1951
|
-
success: { type: "boolean", example: false },
|
|
1952
|
-
message: { type: "string" },
|
|
1953
|
-
errors: {
|
|
1954
|
-
type: "object",
|
|
1955
|
-
additionalProperties: {
|
|
1956
|
-
type: "array",
|
|
1957
|
-
items: { type: "string" }
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
},
|
|
1962
|
-
unauthorized: {
|
|
1963
|
-
type: "object",
|
|
1964
|
-
properties: {
|
|
1965
|
-
success: { type: "boolean", example: false },
|
|
1966
|
-
message: { type: "string", example: "Unauthorized" }
|
|
1967
|
-
}
|
|
1968
|
-
},
|
|
1969
|
-
notFound: {
|
|
1970
|
-
type: "object",
|
|
1971
|
-
properties: {
|
|
1972
|
-
success: { type: "boolean", example: false },
|
|
1973
|
-
message: { type: "string", example: "Resource not found" }
|
|
1974
|
-
}
|
|
1975
|
-
},
|
|
1976
|
-
paginated: {
|
|
1977
|
-
type: "object",
|
|
1978
|
-
properties: {
|
|
1979
|
-
success: { type: "boolean", example: true },
|
|
1980
|
-
data: {
|
|
1981
|
-
type: "object",
|
|
1982
|
-
properties: {
|
|
1983
|
-
data: { type: "array", items: { type: "object" } },
|
|
1984
|
-
meta: {
|
|
1985
|
-
type: "object",
|
|
1986
|
-
properties: {
|
|
1987
|
-
total: { type: "number" },
|
|
1988
|
-
page: { type: "number" },
|
|
1989
|
-
limit: { type: "number" },
|
|
1990
|
-
totalPages: { type: "number" },
|
|
1991
|
-
hasNextPage: { type: "boolean" },
|
|
1992
|
-
hasPrevPage: { type: "boolean" }
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
};
|
|
2000
|
-
var paginationQuery = {
|
|
2001
|
-
type: "object",
|
|
2002
|
-
properties: {
|
|
2003
|
-
page: { type: "integer", minimum: 1, default: 1, description: "Page number" },
|
|
2004
|
-
limit: { type: "integer", minimum: 1, maximum: 100, default: 20, description: "Items per page" },
|
|
2005
|
-
sortBy: { type: "string", description: "Field to sort by" },
|
|
2006
|
-
sortOrder: { type: "string", enum: ["asc", "desc"], default: "asc", description: "Sort order" },
|
|
2007
|
-
search: { type: "string", description: "Search query" }
|
|
2008
|
-
}
|
|
2009
|
-
};
|
|
2010
|
-
var idParam = {
|
|
2011
|
-
type: "object",
|
|
2012
|
-
properties: {
|
|
2013
|
-
id: { type: "string", format: "uuid", description: "Resource ID" }
|
|
2014
|
-
},
|
|
2015
|
-
required: ["id"]
|
|
2016
|
-
};
|
|
2017
|
-
|
|
2018
|
-
// src/modules/auth/index.ts
|
|
2019
|
-
import jwt from "@fastify/jwt";
|
|
2020
|
-
import cookie from "@fastify/cookie";
|
|
2021
|
-
|
|
2022
|
-
// src/modules/auth/auth.service.ts
|
|
2023
|
-
import bcrypt from "bcryptjs";
|
|
2024
|
-
var tokenBlacklist = /* @__PURE__ */ new Set();
|
|
2025
|
-
var AuthService = class {
|
|
2026
|
-
app;
|
|
2027
|
-
SALT_ROUNDS = 12;
|
|
2028
|
-
constructor(app) {
|
|
2029
|
-
this.app = app;
|
|
2030
|
-
}
|
|
2031
|
-
async hashPassword(password) {
|
|
2032
|
-
return bcrypt.hash(password, this.SALT_ROUNDS);
|
|
2033
|
-
}
|
|
2034
|
-
async verifyPassword(password, hash) {
|
|
2035
|
-
return bcrypt.compare(password, hash);
|
|
2036
|
-
}
|
|
2037
|
-
generateTokenPair(user) {
|
|
2038
|
-
const accessPayload = {
|
|
2039
|
-
sub: user.id,
|
|
2040
|
-
email: user.email,
|
|
2041
|
-
role: user.role,
|
|
2042
|
-
type: "access"
|
|
2043
|
-
};
|
|
2044
|
-
const refreshPayload = {
|
|
2045
|
-
sub: user.id,
|
|
2046
|
-
email: user.email,
|
|
2047
|
-
role: user.role,
|
|
2048
|
-
type: "refresh"
|
|
2049
|
-
};
|
|
2050
|
-
const accessToken = this.app.jwt.sign(accessPayload, {
|
|
2051
|
-
expiresIn: config.jwt.accessExpiresIn
|
|
2052
|
-
});
|
|
2053
|
-
const refreshToken = this.app.jwt.sign(refreshPayload, {
|
|
2054
|
-
expiresIn: config.jwt.refreshExpiresIn
|
|
2055
|
-
});
|
|
2056
|
-
const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
|
|
2057
|
-
return { accessToken, refreshToken, expiresIn };
|
|
2058
|
-
}
|
|
2059
|
-
parseExpiration(expiration) {
|
|
2060
|
-
const match = expiration.match(/^(\d+)([smhd])$/);
|
|
2061
|
-
if (!match) return 900;
|
|
2062
|
-
const value = parseInt(match[1] || "0", 10);
|
|
2063
|
-
const unit = match[2];
|
|
2064
|
-
switch (unit) {
|
|
2065
|
-
case "s":
|
|
2066
|
-
return value;
|
|
2067
|
-
case "m":
|
|
2068
|
-
return value * 60;
|
|
2069
|
-
case "h":
|
|
2070
|
-
return value * 3600;
|
|
2071
|
-
case "d":
|
|
2072
|
-
return value * 86400;
|
|
2073
|
-
default:
|
|
2074
|
-
return 900;
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
async verifyAccessToken(token) {
|
|
2078
|
-
try {
|
|
2079
|
-
if (this.isTokenBlacklisted(token)) {
|
|
2080
|
-
throw new UnauthorizedError("Token has been revoked");
|
|
2081
|
-
}
|
|
2082
|
-
const payload = this.app.jwt.verify(token);
|
|
2083
|
-
if (payload.type !== "access") {
|
|
2084
|
-
throw new UnauthorizedError("Invalid token type");
|
|
2085
|
-
}
|
|
2086
|
-
return payload;
|
|
2087
|
-
} catch (error2) {
|
|
2088
|
-
if (error2 instanceof UnauthorizedError) throw error2;
|
|
2089
|
-
logger.debug({ err: error2 }, "Token verification failed");
|
|
2090
|
-
throw new UnauthorizedError("Invalid or expired token");
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
async verifyRefreshToken(token) {
|
|
2094
|
-
try {
|
|
2095
|
-
if (this.isTokenBlacklisted(token)) {
|
|
2096
|
-
throw new UnauthorizedError("Token has been revoked");
|
|
2097
|
-
}
|
|
2098
|
-
const payload = this.app.jwt.verify(token);
|
|
2099
|
-
if (payload.type !== "refresh") {
|
|
2100
|
-
throw new UnauthorizedError("Invalid token type");
|
|
2101
|
-
}
|
|
2102
|
-
return payload;
|
|
2103
|
-
} catch (error2) {
|
|
2104
|
-
if (error2 instanceof UnauthorizedError) throw error2;
|
|
2105
|
-
logger.debug({ err: error2 }, "Refresh token verification failed");
|
|
2106
|
-
throw new UnauthorizedError("Invalid or expired refresh token");
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
blacklistToken(token) {
|
|
2110
|
-
tokenBlacklist.add(token);
|
|
2111
|
-
logger.debug("Token blacklisted");
|
|
2112
|
-
}
|
|
2113
|
-
isTokenBlacklisted(token) {
|
|
2114
|
-
return tokenBlacklist.has(token);
|
|
2115
|
-
}
|
|
2116
|
-
// Clear expired tokens from blacklist periodically
|
|
2117
|
-
cleanupBlacklist() {
|
|
2118
|
-
tokenBlacklist.clear();
|
|
2119
|
-
logger.debug("Token blacklist cleared");
|
|
2120
|
-
}
|
|
2121
|
-
};
|
|
2122
|
-
function createAuthService(app) {
|
|
2123
|
-
return new AuthService(app);
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
// src/modules/auth/schemas.ts
|
|
2127
|
-
import { z as z2 } from "zod";
|
|
2128
|
-
var loginSchema = z2.object({
|
|
2129
|
-
email: z2.string().email("Invalid email address"),
|
|
2130
|
-
password: z2.string().min(1, "Password is required")
|
|
2131
|
-
});
|
|
2132
|
-
var registerSchema = z2.object({
|
|
2133
|
-
email: z2.string().email("Invalid email address"),
|
|
2134
|
-
password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
|
|
2135
|
-
name: z2.string().min(2, "Name must be at least 2 characters").optional()
|
|
2136
|
-
});
|
|
2137
|
-
var refreshTokenSchema = z2.object({
|
|
2138
|
-
refreshToken: z2.string().min(1, "Refresh token is required")
|
|
2139
|
-
});
|
|
2140
|
-
var passwordResetRequestSchema = z2.object({
|
|
2141
|
-
email: z2.string().email("Invalid email address")
|
|
2142
|
-
});
|
|
2143
|
-
var passwordResetConfirmSchema = z2.object({
|
|
2144
|
-
token: z2.string().min(1, "Token is required"),
|
|
2145
|
-
password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
|
|
2146
|
-
});
|
|
2147
|
-
var changePasswordSchema = z2.object({
|
|
2148
|
-
currentPassword: z2.string().min(1, "Current password is required"),
|
|
2149
|
-
newPassword: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
|
|
2150
|
-
});
|
|
2151
|
-
|
|
2152
|
-
// src/utils/response.ts
|
|
2153
|
-
function success3(reply, data, statusCode = 200) {
|
|
2154
|
-
const response = {
|
|
2155
|
-
success: true,
|
|
2156
|
-
data
|
|
2157
|
-
};
|
|
2158
|
-
return reply.status(statusCode).send(response);
|
|
2159
|
-
}
|
|
2160
|
-
function created(reply, data) {
|
|
2161
|
-
return success3(reply, data, 201);
|
|
2162
|
-
}
|
|
2163
|
-
function noContent(reply) {
|
|
2164
|
-
return reply.status(204).send();
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
// src/modules/validation/validator.ts
|
|
2168
|
-
import { z as z3 } from "zod";
|
|
2169
|
-
function validateBody(schema, data) {
|
|
2170
|
-
const result = schema.safeParse(data);
|
|
2171
|
-
if (!result.success) {
|
|
2172
|
-
throw new ValidationError(formatZodErrors(result.error));
|
|
2173
|
-
}
|
|
2174
|
-
return result.data;
|
|
2175
|
-
}
|
|
2176
|
-
function validateQuery(schema, data) {
|
|
2177
|
-
const result = schema.safeParse(data);
|
|
2178
|
-
if (!result.success) {
|
|
2179
|
-
throw new ValidationError(formatZodErrors(result.error));
|
|
2180
|
-
}
|
|
2181
|
-
return result.data;
|
|
2182
|
-
}
|
|
2183
|
-
function formatZodErrors(error2) {
|
|
2184
|
-
const errors = {};
|
|
2185
|
-
for (const issue of error2.issues) {
|
|
2186
|
-
const path6 = issue.path.join(".") || "root";
|
|
2187
|
-
if (!errors[path6]) {
|
|
2188
|
-
errors[path6] = [];
|
|
2189
|
-
}
|
|
2190
|
-
errors[path6].push(issue.message);
|
|
2191
|
-
}
|
|
2192
|
-
return errors;
|
|
2193
|
-
}
|
|
2194
|
-
var idParamSchema = z3.object({
|
|
2195
|
-
id: z3.string().uuid("Invalid ID format")
|
|
2196
|
-
});
|
|
2197
|
-
var paginationSchema = z3.object({
|
|
2198
|
-
page: z3.string().transform(Number).optional().default("1"),
|
|
2199
|
-
limit: z3.string().transform(Number).optional().default("20"),
|
|
2200
|
-
sortBy: z3.string().optional(),
|
|
2201
|
-
sortOrder: z3.enum(["asc", "desc"]).optional().default("asc")
|
|
2202
|
-
});
|
|
2203
|
-
var searchSchema = z3.object({
|
|
2204
|
-
q: z3.string().min(1, "Search query is required").optional(),
|
|
2205
|
-
search: z3.string().min(1).optional()
|
|
2206
|
-
});
|
|
2207
|
-
var emailSchema = z3.string().email("Invalid email address");
|
|
2208
|
-
var passwordSchema = z3.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number").regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
|
|
2209
|
-
var urlSchema = z3.string().url("Invalid URL format");
|
|
2210
|
-
var phoneSchema = z3.string().regex(
|
|
2211
|
-
/^\+?[1-9]\d{1,14}$/,
|
|
2212
|
-
"Invalid phone number format"
|
|
2213
|
-
);
|
|
2214
|
-
var dateSchema = z3.coerce.date();
|
|
2215
|
-
var futureDateSchema = z3.coerce.date().refine(
|
|
2216
|
-
(date) => date > /* @__PURE__ */ new Date(),
|
|
2217
|
-
"Date must be in the future"
|
|
2218
|
-
);
|
|
2219
|
-
var pastDateSchema = z3.coerce.date().refine(
|
|
2220
|
-
(date) => date < /* @__PURE__ */ new Date(),
|
|
2221
|
-
"Date must be in the past"
|
|
2222
|
-
);
|
|
2223
|
-
|
|
2224
|
-
// src/modules/auth/auth.controller.ts
|
|
2225
|
-
var AuthController = class {
|
|
2226
|
-
constructor(authService, userService) {
|
|
2227
|
-
this.authService = authService;
|
|
2228
|
-
this.userService = userService;
|
|
2229
|
-
}
|
|
2230
|
-
async register(request, reply) {
|
|
2231
|
-
const data = validateBody(registerSchema, request.body);
|
|
2232
|
-
const existingUser = await this.userService.findByEmail(data.email);
|
|
2233
|
-
if (existingUser) {
|
|
2234
|
-
throw new BadRequestError("Email already registered");
|
|
2235
|
-
}
|
|
2236
|
-
const hashedPassword = await this.authService.hashPassword(data.password);
|
|
2237
|
-
const user = await this.userService.create({
|
|
2238
|
-
email: data.email,
|
|
2239
|
-
password: hashedPassword,
|
|
2240
|
-
name: data.name
|
|
2241
|
-
});
|
|
2242
|
-
const tokens = this.authService.generateTokenPair({
|
|
2243
|
-
id: user.id,
|
|
2244
|
-
email: user.email,
|
|
2245
|
-
role: user.role
|
|
2246
|
-
});
|
|
2247
|
-
created(reply, {
|
|
2248
|
-
user: {
|
|
2249
|
-
id: user.id,
|
|
2250
|
-
email: user.email,
|
|
2251
|
-
name: user.name,
|
|
2252
|
-
role: user.role
|
|
2253
|
-
},
|
|
2254
|
-
...tokens
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
async login(request, reply) {
|
|
2258
|
-
const data = validateBody(loginSchema, request.body);
|
|
2259
|
-
const user = await this.userService.findByEmail(data.email);
|
|
2260
|
-
if (!user) {
|
|
2261
|
-
throw new UnauthorizedError("Invalid credentials");
|
|
2262
|
-
}
|
|
2263
|
-
if (user.status !== "active") {
|
|
2264
|
-
throw new UnauthorizedError("Account is not active");
|
|
2265
|
-
}
|
|
2266
|
-
const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
|
|
2267
|
-
if (!isValidPassword) {
|
|
2268
|
-
throw new UnauthorizedError("Invalid credentials");
|
|
2269
|
-
}
|
|
2270
|
-
await this.userService.updateLastLogin(user.id);
|
|
2271
|
-
const tokens = this.authService.generateTokenPair({
|
|
2272
|
-
id: user.id,
|
|
2273
|
-
email: user.email,
|
|
2274
|
-
role: user.role
|
|
2275
|
-
});
|
|
2276
|
-
success3(reply, {
|
|
2277
|
-
user: {
|
|
2278
|
-
id: user.id,
|
|
2279
|
-
email: user.email,
|
|
2280
|
-
name: user.name,
|
|
2281
|
-
role: user.role
|
|
2282
|
-
},
|
|
2283
|
-
...tokens
|
|
2284
|
-
});
|
|
2285
|
-
}
|
|
2286
|
-
async refresh(request, reply) {
|
|
2287
|
-
const data = validateBody(refreshTokenSchema, request.body);
|
|
2288
|
-
const payload = await this.authService.verifyRefreshToken(data.refreshToken);
|
|
2289
|
-
const user = await this.userService.findById(payload.sub);
|
|
2290
|
-
if (!user || user.status !== "active") {
|
|
2291
|
-
throw new UnauthorizedError("User not found or inactive");
|
|
2292
|
-
}
|
|
2293
|
-
this.authService.blacklistToken(data.refreshToken);
|
|
2294
|
-
const tokens = this.authService.generateTokenPair({
|
|
2295
|
-
id: user.id,
|
|
2296
|
-
email: user.email,
|
|
2297
|
-
role: user.role
|
|
2298
|
-
});
|
|
2299
|
-
success3(reply, tokens);
|
|
2300
|
-
}
|
|
2301
|
-
async logout(request, reply) {
|
|
2302
|
-
const authHeader = request.headers.authorization;
|
|
2303
|
-
if (authHeader?.startsWith("Bearer ")) {
|
|
2304
|
-
const token = authHeader.substring(7);
|
|
2305
|
-
this.authService.blacklistToken(token);
|
|
2306
|
-
}
|
|
2307
|
-
success3(reply, { message: "Logged out successfully" });
|
|
2308
|
-
}
|
|
2309
|
-
async me(request, reply) {
|
|
2310
|
-
const authRequest = request;
|
|
2311
|
-
const user = await this.userService.findById(authRequest.user.id);
|
|
2312
|
-
if (!user) {
|
|
2313
|
-
throw new UnauthorizedError("User not found");
|
|
2314
|
-
}
|
|
2315
|
-
success3(reply, {
|
|
2316
|
-
id: user.id,
|
|
2317
|
-
email: user.email,
|
|
2318
|
-
name: user.name,
|
|
2319
|
-
role: user.role,
|
|
2320
|
-
status: user.status,
|
|
2321
|
-
createdAt: user.createdAt
|
|
2322
|
-
});
|
|
2323
|
-
}
|
|
2324
|
-
async changePassword(request, reply) {
|
|
2325
|
-
const authRequest = request;
|
|
2326
|
-
const data = validateBody(changePasswordSchema, request.body);
|
|
2327
|
-
const user = await this.userService.findById(authRequest.user.id);
|
|
2328
|
-
if (!user) {
|
|
2329
|
-
throw new UnauthorizedError("User not found");
|
|
2330
|
-
}
|
|
2331
|
-
const isValidPassword = await this.authService.verifyPassword(
|
|
2332
|
-
data.currentPassword,
|
|
2333
|
-
user.password
|
|
2334
|
-
);
|
|
2335
|
-
if (!isValidPassword) {
|
|
2336
|
-
throw new BadRequestError("Current password is incorrect");
|
|
2337
|
-
}
|
|
2338
|
-
const hashedPassword = await this.authService.hashPassword(data.newPassword);
|
|
2339
|
-
await this.userService.updatePassword(user.id, hashedPassword);
|
|
2340
|
-
success3(reply, { message: "Password changed successfully" });
|
|
2341
|
-
}
|
|
2342
|
-
};
|
|
2343
|
-
function createAuthController(authService, userService) {
|
|
2344
|
-
return new AuthController(authService, userService);
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
// src/modules/auth/auth.middleware.ts
|
|
2348
|
-
function createAuthMiddleware(authService) {
|
|
2349
|
-
return async function authenticate(request, reply) {
|
|
2350
|
-
const authHeader = request.headers.authorization;
|
|
2351
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
2352
|
-
throw new UnauthorizedError("Missing or invalid authorization header");
|
|
2353
|
-
}
|
|
2354
|
-
const token = authHeader.substring(7);
|
|
2355
|
-
const payload = await authService.verifyAccessToken(token);
|
|
2356
|
-
request.user = {
|
|
2357
|
-
id: payload.sub,
|
|
2358
|
-
email: payload.email,
|
|
2359
|
-
role: payload.role
|
|
2360
|
-
};
|
|
2361
|
-
};
|
|
2362
|
-
}
|
|
2363
|
-
function createRoleMiddleware(allowedRoles) {
|
|
2364
|
-
return async function authorize(request, _reply) {
|
|
2365
|
-
const user = request.user;
|
|
2366
|
-
if (!user) {
|
|
2367
|
-
throw new UnauthorizedError("Authentication required");
|
|
2368
|
-
}
|
|
2369
|
-
if (!allowedRoles.includes(user.role)) {
|
|
2370
|
-
throw new ForbiddenError("Insufficient permissions");
|
|
2371
|
-
}
|
|
2372
|
-
};
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
// src/modules/auth/auth.routes.ts
|
|
2376
|
-
var credentialsBody = {
|
|
2377
|
-
type: "object",
|
|
2378
|
-
required: ["email", "password"],
|
|
2379
|
-
properties: {
|
|
2380
|
-
email: { type: "string", format: "email" },
|
|
2381
|
-
password: { type: "string", minLength: 8 }
|
|
2382
|
-
}
|
|
2383
|
-
};
|
|
2384
|
-
var changePasswordBody = {
|
|
2385
|
-
type: "object",
|
|
2386
|
-
required: ["currentPassword", "newPassword"],
|
|
2387
|
-
properties: {
|
|
2388
|
-
currentPassword: { type: "string", minLength: 8 },
|
|
2389
|
-
newPassword: { type: "string", minLength: 8 }
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
function registerAuthRoutes(app, controller, authService) {
|
|
2393
|
-
const authenticate = createAuthMiddleware(authService);
|
|
2394
|
-
app.post("/auth/register", {
|
|
2395
|
-
schema: {
|
|
2396
|
-
tags: ["Auth"],
|
|
2397
|
-
summary: "Register a new user",
|
|
2398
|
-
body: credentialsBody,
|
|
2399
|
-
response: {
|
|
2400
|
-
201: commonResponses.success,
|
|
2401
|
-
400: commonResponses.error,
|
|
2402
|
-
409: commonResponses.error
|
|
2403
|
-
}
|
|
2404
|
-
},
|
|
2405
|
-
handler: controller.register.bind(controller)
|
|
2406
|
-
});
|
|
2407
|
-
app.post("/auth/login", {
|
|
2408
|
-
schema: {
|
|
2409
|
-
tags: ["Auth"],
|
|
2410
|
-
summary: "Login and obtain tokens",
|
|
2411
|
-
body: credentialsBody,
|
|
2412
|
-
response: {
|
|
2413
|
-
200: commonResponses.success,
|
|
2414
|
-
400: commonResponses.error,
|
|
2415
|
-
401: commonResponses.unauthorized
|
|
2416
|
-
}
|
|
2417
|
-
},
|
|
2418
|
-
handler: controller.login.bind(controller)
|
|
2419
|
-
});
|
|
2420
|
-
app.post("/auth/refresh", {
|
|
2421
|
-
schema: {
|
|
2422
|
-
tags: ["Auth"],
|
|
2423
|
-
summary: "Refresh access token",
|
|
2424
|
-
body: {
|
|
2425
|
-
type: "object",
|
|
2426
|
-
required: ["refreshToken"],
|
|
2427
|
-
properties: {
|
|
2428
|
-
refreshToken: { type: "string" }
|
|
2429
|
-
}
|
|
2430
|
-
},
|
|
2431
|
-
response: {
|
|
2432
|
-
200: commonResponses.success,
|
|
2433
|
-
401: commonResponses.unauthorized
|
|
2434
|
-
}
|
|
2435
|
-
},
|
|
2436
|
-
handler: controller.refresh.bind(controller)
|
|
2437
|
-
});
|
|
2438
|
-
app.post("/auth/logout", {
|
|
2439
|
-
preHandler: [authenticate],
|
|
2440
|
-
schema: {
|
|
2441
|
-
tags: ["Auth"],
|
|
2442
|
-
summary: "Logout current user",
|
|
2443
|
-
security: [{ bearerAuth: [] }],
|
|
2444
|
-
response: {
|
|
2445
|
-
200: commonResponses.success,
|
|
2446
|
-
401: commonResponses.unauthorized
|
|
2447
|
-
}
|
|
2448
|
-
},
|
|
2449
|
-
handler: controller.logout.bind(controller)
|
|
2450
|
-
});
|
|
2451
|
-
app.get("/auth/me", {
|
|
2452
|
-
preHandler: [authenticate],
|
|
2453
|
-
schema: {
|
|
2454
|
-
tags: ["Auth"],
|
|
2455
|
-
summary: "Get current user profile",
|
|
2456
|
-
security: [{ bearerAuth: [] }],
|
|
2457
|
-
response: {
|
|
2458
|
-
200: commonResponses.success,
|
|
2459
|
-
401: commonResponses.unauthorized
|
|
2460
|
-
}
|
|
2461
|
-
},
|
|
2462
|
-
handler: controller.me.bind(controller)
|
|
2463
|
-
});
|
|
2464
|
-
app.post("/auth/change-password", {
|
|
2465
|
-
preHandler: [authenticate],
|
|
2466
|
-
schema: {
|
|
2467
|
-
tags: ["Auth"],
|
|
2468
|
-
summary: "Change current user password",
|
|
2469
|
-
security: [{ bearerAuth: [] }],
|
|
2470
|
-
body: changePasswordBody,
|
|
2471
|
-
response: {
|
|
2472
|
-
200: commonResponses.success,
|
|
2473
|
-
400: commonResponses.error,
|
|
2474
|
-
401: commonResponses.unauthorized
|
|
2475
|
-
}
|
|
2476
|
-
},
|
|
2477
|
-
handler: controller.changePassword.bind(controller)
|
|
2478
|
-
});
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
// src/modules/user/user.repository.ts
|
|
2482
|
-
import { randomUUID } from "crypto";
|
|
2483
|
-
|
|
2484
|
-
// src/utils/pagination.ts
|
|
2485
|
-
var DEFAULT_PAGE = 1;
|
|
2486
|
-
var DEFAULT_LIMIT = 20;
|
|
2487
|
-
var MAX_LIMIT = 100;
|
|
2488
|
-
function parsePaginationParams(query) {
|
|
2489
|
-
const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
|
|
2490
|
-
const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10)));
|
|
2491
|
-
const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
|
|
2492
|
-
const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
|
|
2493
|
-
return { page, limit, sortBy, sortOrder };
|
|
2494
|
-
}
|
|
2495
|
-
function createPaginatedResult(data, total, params) {
|
|
2496
|
-
const totalPages = Math.ceil(total / params.limit);
|
|
2497
|
-
return {
|
|
2498
|
-
data,
|
|
2499
|
-
meta: {
|
|
2500
|
-
total,
|
|
2501
|
-
page: params.page,
|
|
2502
|
-
limit: params.limit,
|
|
2503
|
-
totalPages,
|
|
2504
|
-
hasNextPage: params.page < totalPages,
|
|
2505
|
-
hasPrevPage: params.page > 1
|
|
2506
|
-
}
|
|
2507
|
-
};
|
|
2508
|
-
}
|
|
2509
|
-
function getSkip(params) {
|
|
2510
|
-
return (params.page - 1) * params.limit;
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
// src/modules/user/user.repository.ts
|
|
2514
|
-
var users = /* @__PURE__ */ new Map();
|
|
2515
|
-
var UserRepository = class {
|
|
2516
|
-
async findById(id) {
|
|
2517
|
-
return users.get(id) || null;
|
|
2518
|
-
}
|
|
2519
|
-
async findByEmail(email) {
|
|
2520
|
-
for (const user of users.values()) {
|
|
2521
|
-
if (user.email.toLowerCase() === email.toLowerCase()) {
|
|
2522
|
-
return user;
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
return null;
|
|
2526
|
-
}
|
|
2527
|
-
async findMany(params, filters) {
|
|
2528
|
-
let filteredUsers = Array.from(users.values());
|
|
2529
|
-
if (filters) {
|
|
2530
|
-
if (filters.status) {
|
|
2531
|
-
filteredUsers = filteredUsers.filter((u) => u.status === filters.status);
|
|
2532
|
-
}
|
|
2533
|
-
if (filters.role) {
|
|
2534
|
-
filteredUsers = filteredUsers.filter((u) => u.role === filters.role);
|
|
2535
|
-
}
|
|
2536
|
-
if (filters.emailVerified !== void 0) {
|
|
2537
|
-
filteredUsers = filteredUsers.filter((u) => u.emailVerified === filters.emailVerified);
|
|
2538
|
-
}
|
|
2539
|
-
if (filters.search) {
|
|
2540
|
-
const search = filters.search.toLowerCase();
|
|
2541
|
-
filteredUsers = filteredUsers.filter(
|
|
2542
|
-
(u) => u.email.toLowerCase().includes(search) || u.name?.toLowerCase().includes(search)
|
|
2543
|
-
);
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
if (params.sortBy) {
|
|
2547
|
-
const sortKey = params.sortBy;
|
|
2548
|
-
filteredUsers.sort((a, b) => {
|
|
2549
|
-
const aVal = a[sortKey];
|
|
2550
|
-
const bVal = b[sortKey];
|
|
2551
|
-
if (aVal === void 0 || bVal === void 0) return 0;
|
|
2552
|
-
if (aVal < bVal) return params.sortOrder === "desc" ? 1 : -1;
|
|
2553
|
-
if (aVal > bVal) return params.sortOrder === "desc" ? -1 : 1;
|
|
2554
|
-
return 0;
|
|
2555
|
-
});
|
|
2556
|
-
}
|
|
2557
|
-
const total = filteredUsers.length;
|
|
2558
|
-
const skip = getSkip(params);
|
|
2559
|
-
const data = filteredUsers.slice(skip, skip + params.limit);
|
|
2560
|
-
return createPaginatedResult(data, total, params);
|
|
2561
|
-
}
|
|
2562
|
-
async create(data) {
|
|
2563
|
-
const now = /* @__PURE__ */ new Date();
|
|
2564
|
-
const user = {
|
|
2565
|
-
id: randomUUID(),
|
|
2566
|
-
email: data.email,
|
|
2567
|
-
password: data.password,
|
|
2568
|
-
name: data.name,
|
|
2569
|
-
role: data.role || "user",
|
|
2570
|
-
status: "active",
|
|
2571
|
-
emailVerified: false,
|
|
2572
|
-
createdAt: now,
|
|
2573
|
-
updatedAt: now
|
|
2574
|
-
};
|
|
2575
|
-
users.set(user.id, user);
|
|
2576
|
-
return user;
|
|
2577
|
-
}
|
|
2578
|
-
async update(id, data) {
|
|
2579
|
-
const user = users.get(id);
|
|
2580
|
-
if (!user) return null;
|
|
2581
|
-
const updatedUser = {
|
|
2582
|
-
...user,
|
|
2583
|
-
...data,
|
|
2584
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2585
|
-
};
|
|
2586
|
-
users.set(id, updatedUser);
|
|
2587
|
-
return updatedUser;
|
|
2588
|
-
}
|
|
2589
|
-
async updatePassword(id, password) {
|
|
2590
|
-
const user = users.get(id);
|
|
2591
|
-
if (!user) return null;
|
|
2592
|
-
const updatedUser = {
|
|
2593
|
-
...user,
|
|
2594
|
-
password,
|
|
2595
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2596
|
-
};
|
|
2597
|
-
users.set(id, updatedUser);
|
|
2598
|
-
return updatedUser;
|
|
2599
|
-
}
|
|
2600
|
-
async updateLastLogin(id) {
|
|
2601
|
-
const user = users.get(id);
|
|
2602
|
-
if (!user) return null;
|
|
2603
|
-
const updatedUser = {
|
|
2604
|
-
...user,
|
|
2605
|
-
lastLoginAt: /* @__PURE__ */ new Date(),
|
|
2606
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2607
|
-
};
|
|
2608
|
-
users.set(id, updatedUser);
|
|
2609
|
-
return updatedUser;
|
|
2610
|
-
}
|
|
2611
|
-
async delete(id) {
|
|
2612
|
-
return users.delete(id);
|
|
2613
|
-
}
|
|
2614
|
-
async count(filters) {
|
|
2615
|
-
let count = 0;
|
|
2616
|
-
for (const user of users.values()) {
|
|
2617
|
-
if (filters) {
|
|
2618
|
-
if (filters.status && user.status !== filters.status) continue;
|
|
2619
|
-
if (filters.role && user.role !== filters.role) continue;
|
|
2620
|
-
if (filters.emailVerified !== void 0 && user.emailVerified !== filters.emailVerified)
|
|
2621
|
-
continue;
|
|
2622
|
-
}
|
|
2623
|
-
count++;
|
|
2624
|
-
}
|
|
2625
|
-
return count;
|
|
2626
|
-
}
|
|
2627
|
-
// Helper to clear all users (for testing)
|
|
2628
|
-
async clear() {
|
|
2629
|
-
users.clear();
|
|
2630
|
-
}
|
|
2631
|
-
};
|
|
2632
|
-
function createUserRepository() {
|
|
2633
|
-
return new UserRepository();
|
|
2634
|
-
}
|
|
2635
|
-
|
|
2636
|
-
// src/modules/user/types.ts
|
|
2637
|
-
var DEFAULT_ROLE_PERMISSIONS = {
|
|
2638
|
-
user: ["profile:read", "profile:update"],
|
|
2639
|
-
moderator: [
|
|
2640
|
-
"profile:read",
|
|
2641
|
-
"profile:update",
|
|
2642
|
-
"users:read",
|
|
2643
|
-
"content:read",
|
|
2644
|
-
"content:update",
|
|
2645
|
-
"content:delete"
|
|
2646
|
-
],
|
|
2647
|
-
admin: [
|
|
2648
|
-
"profile:read",
|
|
2649
|
-
"profile:update",
|
|
2650
|
-
"users:read",
|
|
2651
|
-
"users:update",
|
|
2652
|
-
"users:delete",
|
|
2653
|
-
"content:manage",
|
|
2654
|
-
"settings:read"
|
|
2655
|
-
],
|
|
2656
|
-
super_admin: ["*:manage"]
|
|
2657
|
-
// All permissions
|
|
2658
|
-
};
|
|
2659
|
-
|
|
2660
|
-
// src/modules/user/user.service.ts
|
|
2661
|
-
var UserService = class {
|
|
2662
|
-
constructor(repository) {
|
|
2663
|
-
this.repository = repository;
|
|
2664
|
-
}
|
|
2665
|
-
async findById(id) {
|
|
2666
|
-
return this.repository.findById(id);
|
|
2667
|
-
}
|
|
2668
|
-
async findByEmail(email) {
|
|
2669
|
-
return this.repository.findByEmail(email);
|
|
2670
|
-
}
|
|
2671
|
-
async findMany(params, filters) {
|
|
2672
|
-
const result = await this.repository.findMany(params, filters);
|
|
2673
|
-
return {
|
|
2674
|
-
...result,
|
|
2675
|
-
data: result.data.map(({ password, ...user }) => user)
|
|
2676
|
-
};
|
|
2677
|
-
}
|
|
2678
|
-
async create(data) {
|
|
2679
|
-
const existing = await this.repository.findByEmail(data.email);
|
|
2680
|
-
if (existing) {
|
|
2681
|
-
throw new ConflictError("User with this email already exists");
|
|
2682
|
-
}
|
|
2683
|
-
const user = await this.repository.create(data);
|
|
2684
|
-
logger.info({ userId: user.id, email: user.email }, "User created");
|
|
2685
|
-
return user;
|
|
2686
|
-
}
|
|
2687
|
-
async update(id, data) {
|
|
2688
|
-
const user = await this.repository.findById(id);
|
|
2689
|
-
if (!user) {
|
|
2690
|
-
throw new NotFoundError("User");
|
|
2691
|
-
}
|
|
2692
|
-
if (data.email && data.email !== user.email) {
|
|
2693
|
-
const existing = await this.repository.findByEmail(data.email);
|
|
2694
|
-
if (existing) {
|
|
2695
|
-
throw new ConflictError("Email already in use");
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
const updatedUser = await this.repository.update(id, data);
|
|
2699
|
-
if (!updatedUser) {
|
|
2700
|
-
throw new NotFoundError("User");
|
|
2701
|
-
}
|
|
2702
|
-
logger.info({ userId: id }, "User updated");
|
|
2703
|
-
return updatedUser;
|
|
2704
|
-
}
|
|
2705
|
-
async updatePassword(id, hashedPassword) {
|
|
2706
|
-
const user = await this.repository.updatePassword(id, hashedPassword);
|
|
2707
|
-
if (!user) {
|
|
2708
|
-
throw new NotFoundError("User");
|
|
2709
|
-
}
|
|
2710
|
-
logger.info({ userId: id }, "User password updated");
|
|
2711
|
-
return user;
|
|
2712
|
-
}
|
|
2713
|
-
async updateLastLogin(id) {
|
|
2714
|
-
const user = await this.repository.updateLastLogin(id);
|
|
2715
|
-
if (!user) {
|
|
2716
|
-
throw new NotFoundError("User");
|
|
2717
|
-
}
|
|
2718
|
-
return user;
|
|
2719
|
-
}
|
|
2720
|
-
async delete(id) {
|
|
2721
|
-
const user = await this.repository.findById(id);
|
|
2722
|
-
if (!user) {
|
|
2723
|
-
throw new NotFoundError("User");
|
|
2724
|
-
}
|
|
2725
|
-
await this.repository.delete(id);
|
|
2726
|
-
logger.info({ userId: id }, "User deleted");
|
|
2727
|
-
}
|
|
2728
|
-
async suspend(id) {
|
|
2729
|
-
return this.update(id, { status: "suspended" });
|
|
2730
|
-
}
|
|
2731
|
-
async ban(id) {
|
|
2732
|
-
return this.update(id, { status: "banned" });
|
|
2733
|
-
}
|
|
2734
|
-
async activate(id) {
|
|
2735
|
-
return this.update(id, { status: "active" });
|
|
2736
|
-
}
|
|
2737
|
-
async verifyEmail(id) {
|
|
2738
|
-
return this.update(id, { emailVerified: true });
|
|
2739
|
-
}
|
|
2740
|
-
async changeRole(id, role) {
|
|
2741
|
-
return this.update(id, { role });
|
|
2742
|
-
}
|
|
2743
|
-
// RBAC helpers
|
|
2744
|
-
hasPermission(role, permission) {
|
|
2745
|
-
const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
|
|
2746
|
-
if (permissions.includes("*:manage")) {
|
|
2747
|
-
return true;
|
|
2748
|
-
}
|
|
2749
|
-
if (permissions.includes(permission)) {
|
|
2750
|
-
return true;
|
|
2751
|
-
}
|
|
2752
|
-
const [resource, action] = permission.split(":");
|
|
2753
|
-
const managePermission = `${resource}:manage`;
|
|
2754
|
-
if (permissions.includes(managePermission)) {
|
|
2755
|
-
return true;
|
|
2756
|
-
}
|
|
2757
|
-
return false;
|
|
2758
|
-
}
|
|
2759
|
-
getPermissions(role) {
|
|
2760
|
-
return DEFAULT_ROLE_PERMISSIONS[role] || [];
|
|
2761
|
-
}
|
|
2762
|
-
};
|
|
2763
|
-
function createUserService(repository) {
|
|
2764
|
-
return new UserService(repository || createUserRepository());
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
// src/modules/auth/index.ts
|
|
2768
|
-
async function registerAuthModule(app) {
|
|
2769
|
-
await app.register(jwt, {
|
|
2770
|
-
secret: config.jwt.secret,
|
|
2771
|
-
sign: {
|
|
2772
|
-
algorithm: "HS256"
|
|
2773
|
-
}
|
|
2774
|
-
});
|
|
2775
|
-
await app.register(cookie, {
|
|
2776
|
-
secret: config.jwt.secret,
|
|
2777
|
-
hook: "onRequest"
|
|
2778
|
-
});
|
|
2779
|
-
const authService = createAuthService(app);
|
|
2780
|
-
const userService = createUserService();
|
|
2781
|
-
const authController = createAuthController(authService, userService);
|
|
2782
|
-
registerAuthRoutes(app, authController, authService);
|
|
2783
|
-
logger.info("Auth module registered");
|
|
2784
|
-
return authService;
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
// src/modules/user/schemas.ts
|
|
2788
|
-
import { z as z4 } from "zod";
|
|
2789
|
-
var userStatusEnum = z4.enum(["active", "inactive", "suspended", "banned"]);
|
|
2790
|
-
var userRoleEnum = z4.enum(["user", "admin", "moderator", "super_admin"]);
|
|
2791
|
-
var createUserSchema = z4.object({
|
|
2792
|
-
email: z4.string().email("Invalid email address"),
|
|
2793
|
-
password: z4.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
|
|
2794
|
-
name: z4.string().min(2, "Name must be at least 2 characters").optional(),
|
|
2795
|
-
role: userRoleEnum.optional().default("user")
|
|
2796
|
-
});
|
|
2797
|
-
var updateUserSchema = z4.object({
|
|
2798
|
-
email: z4.string().email("Invalid email address").optional(),
|
|
2799
|
-
name: z4.string().min(2, "Name must be at least 2 characters").optional(),
|
|
2800
|
-
role: userRoleEnum.optional(),
|
|
2801
|
-
status: userStatusEnum.optional(),
|
|
2802
|
-
emailVerified: z4.boolean().optional(),
|
|
2803
|
-
metadata: z4.record(z4.unknown()).optional()
|
|
2804
|
-
});
|
|
2805
|
-
var updateProfileSchema = z4.object({
|
|
2806
|
-
name: z4.string().min(2, "Name must be at least 2 characters").optional(),
|
|
2807
|
-
metadata: z4.record(z4.unknown()).optional()
|
|
2808
|
-
});
|
|
2809
|
-
var userQuerySchema = z4.object({
|
|
2810
|
-
page: z4.string().transform(Number).optional(),
|
|
2811
|
-
limit: z4.string().transform(Number).optional(),
|
|
2812
|
-
sortBy: z4.string().optional(),
|
|
2813
|
-
sortOrder: z4.enum(["asc", "desc"]).optional(),
|
|
2814
|
-
status: userStatusEnum.optional(),
|
|
2815
|
-
role: userRoleEnum.optional(),
|
|
2816
|
-
search: z4.string().optional(),
|
|
2817
|
-
emailVerified: z4.string().transform((val) => val === "true").optional()
|
|
2818
|
-
});
|
|
2819
|
-
|
|
2820
|
-
// src/modules/user/user.controller.ts
|
|
2821
|
-
var UserController = class {
|
|
2822
|
-
constructor(userService) {
|
|
2823
|
-
this.userService = userService;
|
|
2824
|
-
}
|
|
2825
|
-
async list(request, reply) {
|
|
2826
|
-
const query = validateQuery(userQuerySchema, request.query);
|
|
2827
|
-
const pagination = parsePaginationParams(query);
|
|
2828
|
-
const filters = {
|
|
2829
|
-
status: query.status,
|
|
2830
|
-
role: query.role,
|
|
2831
|
-
search: query.search,
|
|
2832
|
-
emailVerified: query.emailVerified
|
|
2833
|
-
};
|
|
2834
|
-
const result = await this.userService.findMany(pagination, filters);
|
|
2835
|
-
success3(reply, result);
|
|
2836
|
-
}
|
|
2837
|
-
async getById(request, reply) {
|
|
2838
|
-
const user = await this.userService.findById(request.params.id);
|
|
2839
|
-
if (!user) {
|
|
2840
|
-
return reply.status(404).send({
|
|
2841
|
-
success: false,
|
|
2842
|
-
message: "User not found"
|
|
2843
|
-
});
|
|
2844
|
-
}
|
|
2845
|
-
const { password, ...userData } = user;
|
|
2846
|
-
success3(reply, userData);
|
|
2847
|
-
}
|
|
2848
|
-
async update(request, reply) {
|
|
2849
|
-
const data = validateBody(updateUserSchema, request.body);
|
|
2850
|
-
const user = await this.userService.update(request.params.id, data);
|
|
2851
|
-
const { password, ...userData } = user;
|
|
2852
|
-
success3(reply, userData);
|
|
2853
|
-
}
|
|
2854
|
-
async delete(request, reply) {
|
|
2855
|
-
const authRequest = request;
|
|
2856
|
-
if (authRequest.user.id === request.params.id) {
|
|
2857
|
-
throw new ForbiddenError("Cannot delete your own account");
|
|
2858
|
-
}
|
|
2859
|
-
await this.userService.delete(request.params.id);
|
|
2860
|
-
noContent(reply);
|
|
2861
|
-
}
|
|
2862
|
-
async suspend(request, reply) {
|
|
2863
|
-
const authRequest = request;
|
|
2864
|
-
if (authRequest.user.id === request.params.id) {
|
|
2865
|
-
throw new ForbiddenError("Cannot suspend your own account");
|
|
2866
|
-
}
|
|
2867
|
-
const user = await this.userService.suspend(request.params.id);
|
|
2868
|
-
const { password, ...userData } = user;
|
|
2869
|
-
success3(reply, userData);
|
|
2870
|
-
}
|
|
2871
|
-
async ban(request, reply) {
|
|
2872
|
-
const authRequest = request;
|
|
2873
|
-
if (authRequest.user.id === request.params.id) {
|
|
2874
|
-
throw new ForbiddenError("Cannot ban your own account");
|
|
2875
|
-
}
|
|
2876
|
-
const user = await this.userService.ban(request.params.id);
|
|
2877
|
-
const { password, ...userData } = user;
|
|
2878
|
-
success3(reply, userData);
|
|
2879
|
-
}
|
|
2880
|
-
async activate(request, reply) {
|
|
2881
|
-
const user = await this.userService.activate(request.params.id);
|
|
2882
|
-
const { password, ...userData } = user;
|
|
2883
|
-
success3(reply, userData);
|
|
2884
|
-
}
|
|
2885
|
-
// Profile routes (for authenticated user)
|
|
2886
|
-
async getProfile(request, reply) {
|
|
2887
|
-
const authRequest = request;
|
|
2888
|
-
const user = await this.userService.findById(authRequest.user.id);
|
|
2889
|
-
if (!user) {
|
|
2890
|
-
return reply.status(404).send({
|
|
2891
|
-
success: false,
|
|
2892
|
-
message: "User not found"
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
const { password, ...userData } = user;
|
|
2896
|
-
success3(reply, userData);
|
|
2897
|
-
}
|
|
2898
|
-
async updateProfile(request, reply) {
|
|
2899
|
-
const authRequest = request;
|
|
2900
|
-
const data = validateBody(updateProfileSchema, request.body);
|
|
2901
|
-
const user = await this.userService.update(authRequest.user.id, data);
|
|
2902
|
-
const { password, ...userData } = user;
|
|
2903
|
-
success3(reply, userData);
|
|
2904
|
-
}
|
|
2905
|
-
};
|
|
2906
|
-
function createUserController(userService) {
|
|
2907
|
-
return new UserController(userService);
|
|
2908
|
-
}
|
|
2909
|
-
|
|
2910
|
-
// src/modules/user/user.routes.ts
|
|
2911
|
-
var userTag = "Users";
|
|
2912
|
-
var userResponse = {
|
|
2913
|
-
type: "object",
|
|
2914
|
-
properties: {
|
|
2915
|
-
success: { type: "boolean", example: true },
|
|
2916
|
-
data: { type: "object" }
|
|
2917
|
-
}
|
|
2918
|
-
};
|
|
2919
|
-
function registerUserRoutes(app, controller, authService) {
|
|
2920
|
-
const authenticate = createAuthMiddleware(authService);
|
|
2921
|
-
const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
|
|
2922
|
-
const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
|
|
2923
|
-
app.get(
|
|
2924
|
-
"/profile",
|
|
2925
|
-
{
|
|
2926
|
-
preHandler: [authenticate],
|
|
2927
|
-
schema: {
|
|
2928
|
-
tags: [userTag],
|
|
2929
|
-
summary: "Get current user profile",
|
|
2930
|
-
security: [{ bearerAuth: [] }],
|
|
2931
|
-
response: {
|
|
2932
|
-
200: userResponse,
|
|
2933
|
-
401: commonResponses.unauthorized
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
},
|
|
2937
|
-
controller.getProfile.bind(controller)
|
|
2938
|
-
);
|
|
2939
|
-
app.patch(
|
|
2940
|
-
"/profile",
|
|
2941
|
-
{
|
|
2942
|
-
preHandler: [authenticate],
|
|
2943
|
-
schema: {
|
|
2944
|
-
tags: [userTag],
|
|
2945
|
-
summary: "Update current user profile",
|
|
2946
|
-
security: [{ bearerAuth: [] }],
|
|
2947
|
-
body: { type: "object" },
|
|
2948
|
-
response: {
|
|
2949
|
-
200: userResponse,
|
|
2950
|
-
401: commonResponses.unauthorized,
|
|
2951
|
-
400: commonResponses.error
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
},
|
|
2955
|
-
controller.updateProfile.bind(controller)
|
|
2956
|
-
);
|
|
2957
|
-
app.get(
|
|
2958
|
-
"/users",
|
|
2959
|
-
{
|
|
2960
|
-
preHandler: [authenticate, isModerator],
|
|
2961
|
-
schema: {
|
|
2962
|
-
tags: [userTag],
|
|
2963
|
-
summary: "List users",
|
|
2964
|
-
security: [{ bearerAuth: [] }],
|
|
2965
|
-
querystring: {
|
|
2966
|
-
...paginationQuery,
|
|
2967
|
-
properties: {
|
|
2968
|
-
...paginationQuery.properties,
|
|
2969
|
-
status: { type: "string", enum: ["active", "inactive", "suspended", "banned"] },
|
|
2970
|
-
role: { type: "string", enum: ["user", "admin", "moderator", "super_admin"] },
|
|
2971
|
-
search: { type: "string" },
|
|
2972
|
-
emailVerified: { type: "boolean" }
|
|
2973
|
-
}
|
|
2974
|
-
},
|
|
2975
|
-
response: {
|
|
2976
|
-
200: commonResponses.paginated,
|
|
2977
|
-
401: commonResponses.unauthorized
|
|
2978
|
-
}
|
|
2979
|
-
}
|
|
2980
|
-
},
|
|
2981
|
-
controller.list.bind(controller)
|
|
2982
|
-
);
|
|
2983
|
-
app.get(
|
|
2984
|
-
"/users/:id",
|
|
2985
|
-
{
|
|
2986
|
-
preHandler: [authenticate, isModerator],
|
|
2987
|
-
schema: {
|
|
2988
|
-
tags: [userTag],
|
|
2989
|
-
summary: "Get user by id",
|
|
2990
|
-
security: [{ bearerAuth: [] }],
|
|
2991
|
-
params: idParam,
|
|
2992
|
-
response: {
|
|
2993
|
-
200: userResponse,
|
|
2994
|
-
401: commonResponses.unauthorized,
|
|
2995
|
-
404: commonResponses.notFound
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
},
|
|
2999
|
-
controller.getById.bind(controller)
|
|
3000
|
-
);
|
|
3001
|
-
app.patch(
|
|
3002
|
-
"/users/:id",
|
|
3003
|
-
{
|
|
3004
|
-
preHandler: [authenticate, isAdmin],
|
|
3005
|
-
schema: {
|
|
3006
|
-
tags: [userTag],
|
|
3007
|
-
summary: "Update user",
|
|
3008
|
-
security: [{ bearerAuth: [] }],
|
|
3009
|
-
params: idParam,
|
|
3010
|
-
body: { type: "object" },
|
|
3011
|
-
response: {
|
|
3012
|
-
200: userResponse,
|
|
3013
|
-
401: commonResponses.unauthorized,
|
|
3014
|
-
404: commonResponses.notFound
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
},
|
|
3018
|
-
controller.update.bind(controller)
|
|
3019
|
-
);
|
|
3020
|
-
app.delete(
|
|
3021
|
-
"/users/:id",
|
|
3022
|
-
{
|
|
3023
|
-
preHandler: [authenticate, isAdmin],
|
|
3024
|
-
schema: {
|
|
3025
|
-
tags: [userTag],
|
|
3026
|
-
summary: "Delete user",
|
|
3027
|
-
security: [{ bearerAuth: [] }],
|
|
3028
|
-
params: idParam,
|
|
3029
|
-
response: {
|
|
3030
|
-
204: { description: "User deleted" },
|
|
3031
|
-
401: commonResponses.unauthorized,
|
|
3032
|
-
404: commonResponses.notFound
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
},
|
|
3036
|
-
controller.delete.bind(controller)
|
|
3037
|
-
);
|
|
3038
|
-
app.post(
|
|
3039
|
-
"/users/:id/suspend",
|
|
3040
|
-
{
|
|
3041
|
-
preHandler: [authenticate, isAdmin],
|
|
3042
|
-
schema: {
|
|
3043
|
-
tags: [userTag],
|
|
3044
|
-
summary: "Suspend user",
|
|
3045
|
-
security: [{ bearerAuth: [] }],
|
|
3046
|
-
params: idParam,
|
|
3047
|
-
response: {
|
|
3048
|
-
200: userResponse,
|
|
3049
|
-
401: commonResponses.unauthorized,
|
|
3050
|
-
404: commonResponses.notFound
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
},
|
|
3054
|
-
controller.suspend.bind(controller)
|
|
3055
|
-
);
|
|
3056
|
-
app.post(
|
|
3057
|
-
"/users/:id/ban",
|
|
3058
|
-
{
|
|
3059
|
-
preHandler: [authenticate, isAdmin],
|
|
3060
|
-
schema: {
|
|
3061
|
-
tags: [userTag],
|
|
3062
|
-
summary: "Ban user",
|
|
3063
|
-
security: [{ bearerAuth: [] }],
|
|
3064
|
-
params: idParam,
|
|
3065
|
-
response: {
|
|
3066
|
-
200: userResponse,
|
|
3067
|
-
401: commonResponses.unauthorized,
|
|
3068
|
-
404: commonResponses.notFound
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
},
|
|
3072
|
-
controller.ban.bind(controller)
|
|
3073
|
-
);
|
|
3074
|
-
app.post(
|
|
3075
|
-
"/users/:id/activate",
|
|
3076
|
-
{
|
|
3077
|
-
preHandler: [authenticate, isAdmin],
|
|
3078
|
-
schema: {
|
|
3079
|
-
tags: [userTag],
|
|
3080
|
-
summary: "Activate user",
|
|
3081
|
-
security: [{ bearerAuth: [] }],
|
|
3082
|
-
params: idParam,
|
|
3083
|
-
response: {
|
|
3084
|
-
200: userResponse,
|
|
3085
|
-
401: commonResponses.unauthorized,
|
|
3086
|
-
404: commonResponses.notFound
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
},
|
|
3090
|
-
controller.activate.bind(controller)
|
|
3091
|
-
);
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
// src/modules/user/index.ts
|
|
3095
|
-
async function registerUserModule(app, authService) {
|
|
3096
|
-
const repository = createUserRepository();
|
|
3097
|
-
const userService = createUserService(repository);
|
|
3098
|
-
const userController = createUserController(userService);
|
|
3099
|
-
registerUserRoutes(app, userController, authService);
|
|
3100
|
-
logger.info("User module registered");
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
// src/cli/utils/docs-generator.ts
|
|
3104
|
-
async function generateDocs(outputPath = "openapi.json", silent = false) {
|
|
3105
|
-
const spinner = silent ? null : ora2("Generating OpenAPI documentation...").start();
|
|
3106
|
-
try {
|
|
3107
|
-
const server = createServer({
|
|
3108
|
-
port: config.server.port,
|
|
3109
|
-
host: config.server.host
|
|
3110
|
-
});
|
|
3111
|
-
const app = server.instance;
|
|
3112
|
-
registerErrorHandler(app);
|
|
3113
|
-
await registerSecurity(app);
|
|
3114
|
-
await registerSwagger(app, {
|
|
3115
|
-
enabled: true,
|
|
3116
|
-
route: config.swagger.route,
|
|
3117
|
-
title: config.swagger.title,
|
|
3118
|
-
description: config.swagger.description,
|
|
3119
|
-
version: config.swagger.version
|
|
3120
|
-
});
|
|
3121
|
-
const authService = await registerAuthModule(app);
|
|
3122
|
-
await registerUserModule(app, authService);
|
|
3123
|
-
await app.ready();
|
|
3124
|
-
const spec = app.swagger();
|
|
3125
|
-
const absoluteOutput = path3.resolve(outputPath);
|
|
3126
|
-
await fs3.mkdir(path3.dirname(absoluteOutput), { recursive: true });
|
|
3127
|
-
await fs3.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
|
|
3128
|
-
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
3129
|
-
await app.close();
|
|
3130
|
-
return absoluteOutput;
|
|
3131
|
-
} catch (error2) {
|
|
3132
|
-
spinner?.fail("Failed to generate OpenAPI documentation");
|
|
3133
|
-
throw error2;
|
|
3134
|
-
}
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
// src/cli/commands/generate.ts
|
|
3138
|
-
var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
3139
|
-
generateCommand.command("module <name> [fields...]").alias("m").description("Generate a complete module with controller, service, repository, types, schemas, and routes").option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").action(async (name, fieldsArgs, options) => {
|
|
3140
|
-
let fields = [];
|
|
3141
|
-
if (options.interactive) {
|
|
3142
|
-
fields = await promptForFields();
|
|
3143
|
-
} else if (fieldsArgs.length > 0) {
|
|
3144
|
-
fields = parseFields(fieldsArgs.join(" "));
|
|
3145
|
-
}
|
|
3146
|
-
const spinner = ora3("Generating module...").start();
|
|
3147
|
-
try {
|
|
3148
|
-
const kebabName = toKebabCase(name);
|
|
3149
|
-
const pascalName = toPascalCase(name);
|
|
3150
|
-
const camelName = toCamelCase(name);
|
|
3151
|
-
const pluralName = pluralize(kebabName);
|
|
3152
|
-
const tableName = pluralize(kebabName.replace(/-/g, "_"));
|
|
3153
|
-
const validatorType = options.validator || "zod";
|
|
3154
|
-
const moduleDir = path4.join(getModulesDir(), kebabName);
|
|
3155
|
-
if (await fileExists(moduleDir)) {
|
|
3156
|
-
spinner.stop();
|
|
3157
|
-
error(`Module "${kebabName}" already exists`);
|
|
3158
|
-
return;
|
|
3159
|
-
}
|
|
3160
|
-
const hasFields = fields.length > 0;
|
|
3161
|
-
const files = [
|
|
3162
|
-
{
|
|
3163
|
-
name: `${kebabName}.types.ts`,
|
|
3164
|
-
content: hasFields ? dynamicTypesTemplate(kebabName, pascalName, fields) : typesTemplate(kebabName, pascalName)
|
|
3165
|
-
},
|
|
3166
|
-
{
|
|
3167
|
-
name: `${kebabName}.schemas.ts`,
|
|
3168
|
-
content: hasFields ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType) : schemasTemplate(kebabName, pascalName, camelName)
|
|
3169
|
-
},
|
|
3170
|
-
{ name: `${kebabName}.service.ts`, content: serviceTemplate(kebabName, pascalName, camelName) },
|
|
3171
|
-
{ name: `${kebabName}.controller.ts`, content: controllerTemplate(kebabName, pascalName, camelName) },
|
|
3172
|
-
{ name: "index.ts", content: moduleIndexTemplate(kebabName, pascalName, camelName) }
|
|
3173
|
-
];
|
|
3174
|
-
if (options.repository !== false) {
|
|
3175
|
-
files.push({
|
|
3176
|
-
name: `${kebabName}.repository.ts`,
|
|
3177
|
-
content: repositoryTemplate(kebabName, pascalName, camelName, pluralName)
|
|
3178
|
-
});
|
|
3179
|
-
}
|
|
3180
|
-
if (options.routes !== false) {
|
|
3181
|
-
files.push({
|
|
3182
|
-
name: `${kebabName}.routes.ts`,
|
|
3183
|
-
content: routesTemplate(kebabName, pascalName, camelName, pluralName, fields)
|
|
3184
|
-
});
|
|
3185
|
-
}
|
|
3186
|
-
for (const file of files) {
|
|
3187
|
-
await writeFile(path4.join(moduleDir, file.name), file.content);
|
|
3188
|
-
}
|
|
3189
|
-
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
3190
|
-
if (options.prisma || hasFields) {
|
|
3191
|
-
console.log("\n" + "\u2500".repeat(50));
|
|
3192
|
-
info("Prisma model suggestion:");
|
|
3193
|
-
if (hasFields) {
|
|
3194
|
-
console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
|
|
3195
|
-
} else {
|
|
3196
|
-
console.log(prismaModelTemplate(kebabName, pascalName, tableName));
|
|
1670
|
+
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
1671
|
+
if (options.prisma || hasFields) {
|
|
1672
|
+
console.log("\n" + "\u2500".repeat(50));
|
|
1673
|
+
info("Prisma model suggestion:");
|
|
1674
|
+
if (hasFields) {
|
|
1675
|
+
console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
|
|
1676
|
+
} else {
|
|
1677
|
+
console.log(prismaModelTemplate(kebabName, pascalName, tableName));
|
|
3197
1678
|
}
|
|
3198
1679
|
}
|
|
3199
1680
|
if (hasFields) {
|
|
@@ -3222,31 +1703,20 @@ generateCommand.command("module <name> [fields...]").alias("m").description("Gen
|
|
|
3222
1703
|
info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
|
|
3223
1704
|
info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
|
|
3224
1705
|
}
|
|
3225
|
-
const { generateDocsNow } = await inquirer2.prompt([
|
|
3226
|
-
{
|
|
3227
|
-
type: "confirm",
|
|
3228
|
-
name: "generateDocsNow",
|
|
3229
|
-
message: "Generate Swagger/OpenAPI documentation now?",
|
|
3230
|
-
default: true
|
|
3231
|
-
}
|
|
3232
|
-
]);
|
|
3233
|
-
if (generateDocsNow) {
|
|
3234
|
-
await generateDocs("openapi.json", true);
|
|
3235
|
-
}
|
|
3236
1706
|
} catch (err) {
|
|
3237
1707
|
spinner.fail("Failed to generate module");
|
|
3238
1708
|
error(err instanceof Error ? err.message : String(err));
|
|
3239
1709
|
}
|
|
3240
1710
|
});
|
|
3241
1711
|
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
3242
|
-
const spinner =
|
|
1712
|
+
const spinner = ora2("Generating controller...").start();
|
|
3243
1713
|
try {
|
|
3244
1714
|
const kebabName = toKebabCase(name);
|
|
3245
1715
|
const pascalName = toPascalCase(name);
|
|
3246
1716
|
const camelName = toCamelCase(name);
|
|
3247
1717
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
3248
|
-
const moduleDir =
|
|
3249
|
-
const filePath =
|
|
1718
|
+
const moduleDir = path3.join(getModulesDir(), moduleName);
|
|
1719
|
+
const filePath = path3.join(moduleDir, `${kebabName}.controller.ts`);
|
|
3250
1720
|
if (await fileExists(filePath)) {
|
|
3251
1721
|
spinner.stop();
|
|
3252
1722
|
error(`Controller "${kebabName}" already exists`);
|
|
@@ -3261,14 +1731,14 @@ generateCommand.command("controller <name>").alias("c").description("Generate a
|
|
|
3261
1731
|
}
|
|
3262
1732
|
});
|
|
3263
1733
|
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
3264
|
-
const spinner =
|
|
1734
|
+
const spinner = ora2("Generating service...").start();
|
|
3265
1735
|
try {
|
|
3266
1736
|
const kebabName = toKebabCase(name);
|
|
3267
1737
|
const pascalName = toPascalCase(name);
|
|
3268
1738
|
const camelName = toCamelCase(name);
|
|
3269
1739
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
3270
|
-
const moduleDir =
|
|
3271
|
-
const filePath =
|
|
1740
|
+
const moduleDir = path3.join(getModulesDir(), moduleName);
|
|
1741
|
+
const filePath = path3.join(moduleDir, `${kebabName}.service.ts`);
|
|
3272
1742
|
if (await fileExists(filePath)) {
|
|
3273
1743
|
spinner.stop();
|
|
3274
1744
|
error(`Service "${kebabName}" already exists`);
|
|
@@ -3283,15 +1753,15 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
|
|
|
3283
1753
|
}
|
|
3284
1754
|
});
|
|
3285
1755
|
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
3286
|
-
const spinner =
|
|
1756
|
+
const spinner = ora2("Generating repository...").start();
|
|
3287
1757
|
try {
|
|
3288
1758
|
const kebabName = toKebabCase(name);
|
|
3289
1759
|
const pascalName = toPascalCase(name);
|
|
3290
1760
|
const camelName = toCamelCase(name);
|
|
3291
1761
|
const pluralName = pluralize(kebabName);
|
|
3292
1762
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
3293
|
-
const moduleDir =
|
|
3294
|
-
const filePath =
|
|
1763
|
+
const moduleDir = path3.join(getModulesDir(), moduleName);
|
|
1764
|
+
const filePath = path3.join(moduleDir, `${kebabName}.repository.ts`);
|
|
3295
1765
|
if (await fileExists(filePath)) {
|
|
3296
1766
|
spinner.stop();
|
|
3297
1767
|
error(`Repository "${kebabName}" already exists`);
|
|
@@ -3306,157 +1776,1261 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
3306
1776
|
}
|
|
3307
1777
|
});
|
|
3308
1778
|
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
3309
|
-
const spinner =
|
|
1779
|
+
const spinner = ora2("Generating types...").start();
|
|
3310
1780
|
try {
|
|
3311
1781
|
const kebabName = toKebabCase(name);
|
|
3312
1782
|
const pascalName = toPascalCase(name);
|
|
3313
1783
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
3314
|
-
const moduleDir =
|
|
3315
|
-
const filePath =
|
|
1784
|
+
const moduleDir = path3.join(getModulesDir(), moduleName);
|
|
1785
|
+
const filePath = path3.join(moduleDir, `${kebabName}.types.ts`);
|
|
3316
1786
|
if (await fileExists(filePath)) {
|
|
3317
1787
|
spinner.stop();
|
|
3318
1788
|
error(`Types file "${kebabName}.types.ts" already exists`);
|
|
3319
1789
|
return;
|
|
3320
1790
|
}
|
|
3321
|
-
await writeFile(filePath, typesTemplate(kebabName, pascalName));
|
|
3322
|
-
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
3323
|
-
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
3324
|
-
} catch (err) {
|
|
3325
|
-
spinner.fail("Failed to generate types");
|
|
3326
|
-
error(err instanceof Error ? err.message : String(err));
|
|
1791
|
+
await writeFile(filePath, typesTemplate(kebabName, pascalName));
|
|
1792
|
+
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
1793
|
+
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
spinner.fail("Failed to generate types");
|
|
1796
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
1800
|
+
const spinner = ora2("Generating schemas...").start();
|
|
1801
|
+
try {
|
|
1802
|
+
const kebabName = toKebabCase(name);
|
|
1803
|
+
const pascalName = toPascalCase(name);
|
|
1804
|
+
const camelName = toCamelCase(name);
|
|
1805
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1806
|
+
const moduleDir = path3.join(getModulesDir(), moduleName);
|
|
1807
|
+
const filePath = path3.join(moduleDir, `${kebabName}.schemas.ts`);
|
|
1808
|
+
if (await fileExists(filePath)) {
|
|
1809
|
+
spinner.stop();
|
|
1810
|
+
error(`Schemas file "${kebabName}.schemas.ts" already exists`);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
|
|
1814
|
+
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
1815
|
+
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
spinner.fail("Failed to generate schemas");
|
|
1818
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
1822
|
+
const spinner = ora2("Generating routes...").start();
|
|
1823
|
+
try {
|
|
1824
|
+
const kebabName = toKebabCase(name);
|
|
1825
|
+
const pascalName = toPascalCase(name);
|
|
1826
|
+
const camelName = toCamelCase(name);
|
|
1827
|
+
const pluralName = pluralize(kebabName);
|
|
1828
|
+
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1829
|
+
const moduleDir = path3.join(getModulesDir(), moduleName);
|
|
1830
|
+
const filePath = path3.join(moduleDir, `${kebabName}.routes.ts`);
|
|
1831
|
+
if (await fileExists(filePath)) {
|
|
1832
|
+
spinner.stop();
|
|
1833
|
+
error(`Routes file "${kebabName}.routes.ts" already exists`);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
|
|
1837
|
+
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
1838
|
+
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
spinner.fail("Failed to generate routes");
|
|
1841
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
async function promptForFields() {
|
|
1845
|
+
const fields = [];
|
|
1846
|
+
console.log("\n\u{1F4DD} Define your model fields (press Enter with empty name to finish)\n");
|
|
1847
|
+
const fieldTypes = [
|
|
1848
|
+
"string",
|
|
1849
|
+
"number",
|
|
1850
|
+
"boolean",
|
|
1851
|
+
"date",
|
|
1852
|
+
"datetime",
|
|
1853
|
+
"text",
|
|
1854
|
+
"email",
|
|
1855
|
+
"url",
|
|
1856
|
+
"uuid",
|
|
1857
|
+
"int",
|
|
1858
|
+
"float",
|
|
1859
|
+
"decimal",
|
|
1860
|
+
"json"
|
|
1861
|
+
];
|
|
1862
|
+
let addMore = true;
|
|
1863
|
+
while (addMore) {
|
|
1864
|
+
const answers = await inquirer2.prompt([
|
|
1865
|
+
{
|
|
1866
|
+
type: "input",
|
|
1867
|
+
name: "name",
|
|
1868
|
+
message: "Field name (empty to finish):"
|
|
1869
|
+
}
|
|
1870
|
+
]);
|
|
1871
|
+
if (!answers.name) {
|
|
1872
|
+
addMore = false;
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const fieldDetails = await inquirer2.prompt([
|
|
1876
|
+
{
|
|
1877
|
+
type: "list",
|
|
1878
|
+
name: "type",
|
|
1879
|
+
message: `Type for "${answers.name}":`,
|
|
1880
|
+
choices: fieldTypes,
|
|
1881
|
+
default: "string"
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
type: "confirm",
|
|
1885
|
+
name: "isOptional",
|
|
1886
|
+
message: "Is optional?",
|
|
1887
|
+
default: false
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
type: "confirm",
|
|
1891
|
+
name: "isUnique",
|
|
1892
|
+
message: "Is unique?",
|
|
1893
|
+
default: false
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
type: "confirm",
|
|
1897
|
+
name: "isArray",
|
|
1898
|
+
message: "Is array?",
|
|
1899
|
+
default: false
|
|
1900
|
+
}
|
|
1901
|
+
]);
|
|
1902
|
+
fields.push({
|
|
1903
|
+
name: answers.name,
|
|
1904
|
+
type: fieldDetails.type,
|
|
1905
|
+
isOptional: fieldDetails.isOptional,
|
|
1906
|
+
isUnique: fieldDetails.isUnique,
|
|
1907
|
+
isArray: fieldDetails.isArray
|
|
1908
|
+
});
|
|
1909
|
+
console.log(` \u2713 Added: ${answers.name}: ${fieldDetails.type}
|
|
1910
|
+
`);
|
|
1911
|
+
}
|
|
1912
|
+
return fields;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/cli/commands/add-module.ts
|
|
1916
|
+
import { Command as Command3 } from "commander";
|
|
1917
|
+
import path6 from "path";
|
|
1918
|
+
import ora3 from "ora";
|
|
1919
|
+
import chalk4 from "chalk";
|
|
1920
|
+
import * as fs5 from "fs/promises";
|
|
1921
|
+
|
|
1922
|
+
// src/cli/utils/env-manager.ts
|
|
1923
|
+
import * as fs3 from "fs/promises";
|
|
1924
|
+
import * as path4 from "path";
|
|
1925
|
+
import { existsSync } from "fs";
|
|
1926
|
+
var EnvManager = class {
|
|
1927
|
+
envPath;
|
|
1928
|
+
envExamplePath;
|
|
1929
|
+
constructor(projectRoot) {
|
|
1930
|
+
this.envPath = path4.join(projectRoot, ".env");
|
|
1931
|
+
this.envExamplePath = path4.join(projectRoot, ".env.example");
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Add environment variables to .env file
|
|
1935
|
+
*/
|
|
1936
|
+
async addVariables(sections) {
|
|
1937
|
+
const added = [];
|
|
1938
|
+
const skipped = [];
|
|
1939
|
+
let created = false;
|
|
1940
|
+
let envContent = "";
|
|
1941
|
+
if (existsSync(this.envPath)) {
|
|
1942
|
+
envContent = await fs3.readFile(this.envPath, "utf-8");
|
|
1943
|
+
} else {
|
|
1944
|
+
created = true;
|
|
1945
|
+
}
|
|
1946
|
+
const existingKeys = this.parseExistingKeys(envContent);
|
|
1947
|
+
let newContent = envContent;
|
|
1948
|
+
if (newContent && !newContent.endsWith("\n\n")) {
|
|
1949
|
+
newContent += "\n\n";
|
|
1950
|
+
}
|
|
1951
|
+
for (const section of sections) {
|
|
1952
|
+
newContent += `# ${section.title}
|
|
1953
|
+
`;
|
|
1954
|
+
for (const variable of section.variables) {
|
|
1955
|
+
if (existingKeys.has(variable.key)) {
|
|
1956
|
+
skipped.push(variable.key);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
if (variable.comment) {
|
|
1960
|
+
newContent += `# ${variable.comment}
|
|
1961
|
+
`;
|
|
1962
|
+
}
|
|
1963
|
+
const value = variable.value || "";
|
|
1964
|
+
const prefix = variable.required ? "" : "# ";
|
|
1965
|
+
newContent += `${prefix}${variable.key}=${value}
|
|
1966
|
+
`;
|
|
1967
|
+
added.push(variable.key);
|
|
1968
|
+
}
|
|
1969
|
+
newContent += "\n";
|
|
1970
|
+
}
|
|
1971
|
+
await fs3.writeFile(this.envPath, newContent, "utf-8");
|
|
1972
|
+
if (existsSync(this.envExamplePath)) {
|
|
1973
|
+
await this.updateEnvExample(sections);
|
|
1974
|
+
}
|
|
1975
|
+
return { added, skipped, created };
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Update .env.example file
|
|
1979
|
+
*/
|
|
1980
|
+
async updateEnvExample(sections) {
|
|
1981
|
+
let exampleContent = "";
|
|
1982
|
+
if (existsSync(this.envExamplePath)) {
|
|
1983
|
+
exampleContent = await fs3.readFile(this.envExamplePath, "utf-8");
|
|
1984
|
+
}
|
|
1985
|
+
const existingKeys = this.parseExistingKeys(exampleContent);
|
|
1986
|
+
let newContent = exampleContent;
|
|
1987
|
+
if (newContent && !newContent.endsWith("\n\n")) {
|
|
1988
|
+
newContent += "\n\n";
|
|
1989
|
+
}
|
|
1990
|
+
for (const section of sections) {
|
|
1991
|
+
newContent += `# ${section.title}
|
|
1992
|
+
`;
|
|
1993
|
+
for (const variable of section.variables) {
|
|
1994
|
+
if (existingKeys.has(variable.key)) {
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
if (variable.comment) {
|
|
1998
|
+
newContent += `# ${variable.comment}
|
|
1999
|
+
`;
|
|
2000
|
+
}
|
|
2001
|
+
const placeholder = this.getPlaceholder(variable.key);
|
|
2002
|
+
newContent += `${variable.key}=${placeholder}
|
|
2003
|
+
`;
|
|
2004
|
+
}
|
|
2005
|
+
newContent += "\n";
|
|
2006
|
+
}
|
|
2007
|
+
await fs3.writeFile(this.envExamplePath, newContent, "utf-8");
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Parse existing environment variable keys
|
|
2011
|
+
*/
|
|
2012
|
+
parseExistingKeys(content) {
|
|
2013
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2014
|
+
const lines = content.split("\n");
|
|
2015
|
+
for (const line of lines) {
|
|
2016
|
+
const trimmed = line.trim();
|
|
2017
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
const match = trimmed.match(/^#?\s*([A-Z_][A-Z0-9_]*)\s*=/);
|
|
2021
|
+
if (match && match[1]) {
|
|
2022
|
+
keys.add(match[1]);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return keys;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Get placeholder value for .env.example
|
|
2029
|
+
*/
|
|
2030
|
+
getPlaceholder(key) {
|
|
2031
|
+
if (key.includes("SECRET") || key.includes("KEY") || key.includes("PASSWORD")) {
|
|
2032
|
+
return "your-secret-key-here";
|
|
2033
|
+
}
|
|
2034
|
+
if (key.includes("HOST")) {
|
|
2035
|
+
return "localhost";
|
|
2036
|
+
}
|
|
2037
|
+
if (key.includes("PORT")) {
|
|
2038
|
+
return "3000";
|
|
2039
|
+
}
|
|
2040
|
+
if (key.includes("URL")) {
|
|
2041
|
+
return "http://localhost:3000";
|
|
2042
|
+
}
|
|
2043
|
+
if (key.includes("EMAIL")) {
|
|
2044
|
+
return "user@example.com";
|
|
2045
|
+
}
|
|
2046
|
+
if (key.includes("REDIS")) {
|
|
2047
|
+
return "redis://localhost:6379";
|
|
2048
|
+
}
|
|
2049
|
+
if (key.includes("DATABASE")) {
|
|
2050
|
+
return "postgresql://user:pass@localhost:5432/db";
|
|
2051
|
+
}
|
|
2052
|
+
if (key.includes("NODE")) {
|
|
2053
|
+
return "http://localhost:9200";
|
|
2054
|
+
}
|
|
2055
|
+
return "";
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Get environment variables for a specific module
|
|
2059
|
+
*/
|
|
2060
|
+
static getModuleEnvVariables(moduleName) {
|
|
2061
|
+
const moduleEnvMap = {
|
|
2062
|
+
"rate-limit": [
|
|
2063
|
+
{
|
|
2064
|
+
title: "Rate Limiting Configuration",
|
|
2065
|
+
variables: [
|
|
2066
|
+
{
|
|
2067
|
+
key: "RATE_LIMIT_ENABLED",
|
|
2068
|
+
value: "true",
|
|
2069
|
+
comment: "Enable rate limiting",
|
|
2070
|
+
required: true
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
key: "RATE_LIMIT_REDIS_URL",
|
|
2074
|
+
value: "redis://localhost:6379",
|
|
2075
|
+
comment: "Redis URL for distributed rate limiting (optional, uses in-memory if not set)",
|
|
2076
|
+
required: false
|
|
2077
|
+
}
|
|
2078
|
+
]
|
|
2079
|
+
}
|
|
2080
|
+
],
|
|
2081
|
+
webhook: [
|
|
2082
|
+
{
|
|
2083
|
+
title: "Webhook Configuration",
|
|
2084
|
+
variables: [
|
|
2085
|
+
{
|
|
2086
|
+
key: "WEBHOOK_TIMEOUT",
|
|
2087
|
+
value: "10000",
|
|
2088
|
+
comment: "Webhook request timeout in milliseconds",
|
|
2089
|
+
required: true
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
key: "WEBHOOK_MAX_RETRIES",
|
|
2093
|
+
value: "5",
|
|
2094
|
+
comment: "Maximum number of retry attempts",
|
|
2095
|
+
required: true
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
key: "WEBHOOK_SIGNATURE_ENABLED",
|
|
2099
|
+
value: "true",
|
|
2100
|
+
comment: "Enable HMAC signature verification",
|
|
2101
|
+
required: true
|
|
2102
|
+
}
|
|
2103
|
+
]
|
|
2104
|
+
}
|
|
2105
|
+
],
|
|
2106
|
+
queue: [
|
|
2107
|
+
{
|
|
2108
|
+
title: "Queue/Jobs Configuration",
|
|
2109
|
+
variables: [
|
|
2110
|
+
{
|
|
2111
|
+
key: "REDIS_HOST",
|
|
2112
|
+
value: "localhost",
|
|
2113
|
+
comment: "Redis host for Bull queue",
|
|
2114
|
+
required: true
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
key: "REDIS_PORT",
|
|
2118
|
+
value: "6379",
|
|
2119
|
+
comment: "Redis port",
|
|
2120
|
+
required: true
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
key: "REDIS_PASSWORD",
|
|
2124
|
+
value: "",
|
|
2125
|
+
comment: "Redis password (optional)",
|
|
2126
|
+
required: false
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
key: "QUEUE_METRICS_ENABLED",
|
|
2130
|
+
value: "true",
|
|
2131
|
+
comment: "Enable queue metrics collection",
|
|
2132
|
+
required: true
|
|
2133
|
+
}
|
|
2134
|
+
]
|
|
2135
|
+
}
|
|
2136
|
+
],
|
|
2137
|
+
websocket: [
|
|
2138
|
+
{
|
|
2139
|
+
title: "WebSocket Configuration",
|
|
2140
|
+
variables: [
|
|
2141
|
+
{
|
|
2142
|
+
key: "WEBSOCKET_PORT",
|
|
2143
|
+
value: "3001",
|
|
2144
|
+
comment: "WebSocket server port",
|
|
2145
|
+
required: true
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
key: "WEBSOCKET_CORS_ORIGIN",
|
|
2149
|
+
value: "http://localhost:3000",
|
|
2150
|
+
comment: "CORS origin for WebSocket",
|
|
2151
|
+
required: true
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
key: "WEBSOCKET_REDIS_URL",
|
|
2155
|
+
value: "redis://localhost:6379",
|
|
2156
|
+
comment: "Redis URL for Socket.io adapter (optional, for multi-instance)",
|
|
2157
|
+
required: false
|
|
2158
|
+
}
|
|
2159
|
+
]
|
|
2160
|
+
}
|
|
2161
|
+
],
|
|
2162
|
+
search: [
|
|
2163
|
+
{
|
|
2164
|
+
title: "Search Configuration (Elasticsearch)",
|
|
2165
|
+
variables: [
|
|
2166
|
+
{
|
|
2167
|
+
key: "SEARCH_ENGINE",
|
|
2168
|
+
value: "memory",
|
|
2169
|
+
comment: "Search engine: elasticsearch, meilisearch, or memory",
|
|
2170
|
+
required: true
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
key: "ELASTICSEARCH_NODE",
|
|
2174
|
+
value: "http://localhost:9200",
|
|
2175
|
+
comment: "Elasticsearch node URL",
|
|
2176
|
+
required: false
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
key: "ELASTICSEARCH_USERNAME",
|
|
2180
|
+
value: "",
|
|
2181
|
+
comment: "Elasticsearch username (optional)",
|
|
2182
|
+
required: false
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
key: "ELASTICSEARCH_PASSWORD",
|
|
2186
|
+
value: "",
|
|
2187
|
+
comment: "Elasticsearch password (optional)",
|
|
2188
|
+
required: false
|
|
2189
|
+
}
|
|
2190
|
+
]
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
title: "Search Configuration (Meilisearch)",
|
|
2194
|
+
variables: [
|
|
2195
|
+
{
|
|
2196
|
+
key: "MEILISEARCH_HOST",
|
|
2197
|
+
value: "http://localhost:7700",
|
|
2198
|
+
comment: "Meilisearch host URL",
|
|
2199
|
+
required: false
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
key: "MEILISEARCH_API_KEY",
|
|
2203
|
+
value: "",
|
|
2204
|
+
comment: "Meilisearch API key (optional)",
|
|
2205
|
+
required: false
|
|
2206
|
+
}
|
|
2207
|
+
]
|
|
2208
|
+
}
|
|
2209
|
+
],
|
|
2210
|
+
i18n: [
|
|
2211
|
+
{
|
|
2212
|
+
title: "i18n/Localization Configuration",
|
|
2213
|
+
variables: [
|
|
2214
|
+
{
|
|
2215
|
+
key: "DEFAULT_LOCALE",
|
|
2216
|
+
value: "en",
|
|
2217
|
+
comment: "Default locale/language",
|
|
2218
|
+
required: true
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
key: "SUPPORTED_LOCALES",
|
|
2222
|
+
value: "en,fr,es,de,ar,zh,ja",
|
|
2223
|
+
comment: "Comma-separated list of supported locales",
|
|
2224
|
+
required: true
|
|
2225
|
+
},
|
|
2226
|
+
{
|
|
2227
|
+
key: "TRANSLATIONS_DIR",
|
|
2228
|
+
value: "./locales",
|
|
2229
|
+
comment: "Directory for translation files",
|
|
2230
|
+
required: true
|
|
2231
|
+
},
|
|
2232
|
+
{
|
|
2233
|
+
key: "I18N_CACHE_ENABLED",
|
|
2234
|
+
value: "true",
|
|
2235
|
+
comment: "Enable translation caching",
|
|
2236
|
+
required: true
|
|
2237
|
+
}
|
|
2238
|
+
]
|
|
2239
|
+
}
|
|
2240
|
+
],
|
|
2241
|
+
cache: [
|
|
2242
|
+
{
|
|
2243
|
+
title: "Cache Configuration",
|
|
2244
|
+
variables: [
|
|
2245
|
+
{
|
|
2246
|
+
key: "CACHE_PROVIDER",
|
|
2247
|
+
value: "redis",
|
|
2248
|
+
comment: "Cache provider: redis or memory",
|
|
2249
|
+
required: true
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
key: "CACHE_REDIS_URL",
|
|
2253
|
+
value: "redis://localhost:6379",
|
|
2254
|
+
comment: "Redis URL for cache",
|
|
2255
|
+
required: false
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
key: "CACHE_TTL",
|
|
2259
|
+
value: "3600",
|
|
2260
|
+
comment: "Default cache TTL in seconds",
|
|
2261
|
+
required: true
|
|
2262
|
+
}
|
|
2263
|
+
]
|
|
2264
|
+
}
|
|
2265
|
+
],
|
|
2266
|
+
mfa: [
|
|
2267
|
+
{
|
|
2268
|
+
title: "MFA/TOTP Configuration",
|
|
2269
|
+
variables: [
|
|
2270
|
+
{
|
|
2271
|
+
key: "MFA_ISSUER",
|
|
2272
|
+
value: "MyApp",
|
|
2273
|
+
comment: "MFA issuer name shown in authenticator apps",
|
|
2274
|
+
required: true
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
key: "MFA_ALGORITHM",
|
|
2278
|
+
value: "SHA1",
|
|
2279
|
+
comment: "TOTP algorithm: SHA1, SHA256, or SHA512",
|
|
2280
|
+
required: true
|
|
2281
|
+
}
|
|
2282
|
+
]
|
|
2283
|
+
}
|
|
2284
|
+
],
|
|
2285
|
+
oauth: [
|
|
2286
|
+
{
|
|
2287
|
+
title: "OAuth Configuration",
|
|
2288
|
+
variables: [
|
|
2289
|
+
{
|
|
2290
|
+
key: "OAUTH_GOOGLE_CLIENT_ID",
|
|
2291
|
+
value: "",
|
|
2292
|
+
comment: "Google OAuth client ID",
|
|
2293
|
+
required: false
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
key: "OAUTH_GOOGLE_CLIENT_SECRET",
|
|
2297
|
+
value: "",
|
|
2298
|
+
comment: "Google OAuth client secret",
|
|
2299
|
+
required: false
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
key: "OAUTH_GITHUB_CLIENT_ID",
|
|
2303
|
+
value: "",
|
|
2304
|
+
comment: "GitHub OAuth client ID",
|
|
2305
|
+
required: false
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
key: "OAUTH_GITHUB_CLIENT_SECRET",
|
|
2309
|
+
value: "",
|
|
2310
|
+
comment: "GitHub OAuth client secret",
|
|
2311
|
+
required: false
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
key: "OAUTH_REDIRECT_URL",
|
|
2315
|
+
value: "http://localhost:3000/auth/callback",
|
|
2316
|
+
comment: "OAuth callback URL",
|
|
2317
|
+
required: true
|
|
2318
|
+
}
|
|
2319
|
+
]
|
|
2320
|
+
}
|
|
2321
|
+
],
|
|
2322
|
+
payment: [
|
|
2323
|
+
{
|
|
2324
|
+
title: "Payment Configuration",
|
|
2325
|
+
variables: [
|
|
2326
|
+
{
|
|
2327
|
+
key: "STRIPE_SECRET_KEY",
|
|
2328
|
+
value: "",
|
|
2329
|
+
comment: "Stripe secret key",
|
|
2330
|
+
required: false
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
key: "STRIPE_PUBLISHABLE_KEY",
|
|
2334
|
+
value: "",
|
|
2335
|
+
comment: "Stripe publishable key",
|
|
2336
|
+
required: false
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
key: "PAYPAL_CLIENT_ID",
|
|
2340
|
+
value: "",
|
|
2341
|
+
comment: "PayPal client ID",
|
|
2342
|
+
required: false
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
key: "PAYPAL_CLIENT_SECRET",
|
|
2346
|
+
value: "",
|
|
2347
|
+
comment: "PayPal client secret",
|
|
2348
|
+
required: false
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
key: "PAYPAL_MODE",
|
|
2352
|
+
value: "sandbox",
|
|
2353
|
+
comment: "PayPal mode: sandbox or live",
|
|
2354
|
+
required: false
|
|
2355
|
+
}
|
|
2356
|
+
]
|
|
2357
|
+
}
|
|
2358
|
+
],
|
|
2359
|
+
"feature-flag": [
|
|
2360
|
+
{
|
|
2361
|
+
title: "Feature Flags Configuration",
|
|
2362
|
+
variables: [
|
|
2363
|
+
{
|
|
2364
|
+
key: "FEATURE_FLAGS_ENABLED",
|
|
2365
|
+
value: "true",
|
|
2366
|
+
comment: "Enable feature flags",
|
|
2367
|
+
required: true
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
key: "FEATURE_FLAGS_ENVIRONMENT",
|
|
2371
|
+
value: "development",
|
|
2372
|
+
comment: "Feature flags environment: development, staging, production, test",
|
|
2373
|
+
required: true
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
key: "FEATURE_FLAGS_ANALYTICS",
|
|
2377
|
+
value: "true",
|
|
2378
|
+
comment: "Enable feature flag analytics",
|
|
2379
|
+
required: true
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
key: "FEATURE_FLAGS_CACHE_TTL",
|
|
2383
|
+
value: "300",
|
|
2384
|
+
comment: "Cache TTL in seconds",
|
|
2385
|
+
required: true
|
|
2386
|
+
}
|
|
2387
|
+
]
|
|
2388
|
+
}
|
|
2389
|
+
],
|
|
2390
|
+
upload: [
|
|
2391
|
+
{
|
|
2392
|
+
title: "File Upload Configuration",
|
|
2393
|
+
variables: [
|
|
2394
|
+
{
|
|
2395
|
+
key: "UPLOAD_PROVIDER",
|
|
2396
|
+
value: "local",
|
|
2397
|
+
comment: "Upload provider: local, s3, cloudinary",
|
|
2398
|
+
required: true
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
key: "UPLOAD_MAX_SIZE",
|
|
2402
|
+
value: "10485760",
|
|
2403
|
+
comment: "Max upload size in bytes (10MB)",
|
|
2404
|
+
required: true
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
key: "UPLOAD_DIR",
|
|
2408
|
+
value: "./uploads",
|
|
2409
|
+
comment: "Local upload directory",
|
|
2410
|
+
required: true
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
key: "AWS_ACCESS_KEY_ID",
|
|
2414
|
+
value: "",
|
|
2415
|
+
comment: "AWS access key for S3",
|
|
2416
|
+
required: false
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
key: "AWS_SECRET_ACCESS_KEY",
|
|
2420
|
+
value: "",
|
|
2421
|
+
comment: "AWS secret key for S3",
|
|
2422
|
+
required: false
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
key: "AWS_S3_BUCKET",
|
|
2426
|
+
value: "",
|
|
2427
|
+
comment: "S3 bucket name",
|
|
2428
|
+
required: false
|
|
2429
|
+
}
|
|
2430
|
+
]
|
|
2431
|
+
}
|
|
2432
|
+
],
|
|
2433
|
+
notification: [
|
|
2434
|
+
{
|
|
2435
|
+
title: "Notification Configuration",
|
|
2436
|
+
variables: [
|
|
2437
|
+
{
|
|
2438
|
+
key: "NOTIFICATION_EMAIL_ENABLED",
|
|
2439
|
+
value: "true",
|
|
2440
|
+
comment: "Enable email notifications",
|
|
2441
|
+
required: true
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
key: "NOTIFICATION_SMS_ENABLED",
|
|
2445
|
+
value: "false",
|
|
2446
|
+
comment: "Enable SMS notifications",
|
|
2447
|
+
required: false
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
key: "NOTIFICATION_PUSH_ENABLED",
|
|
2451
|
+
value: "false",
|
|
2452
|
+
comment: "Enable push notifications",
|
|
2453
|
+
required: false
|
|
2454
|
+
},
|
|
2455
|
+
{
|
|
2456
|
+
key: "TWILIO_ACCOUNT_SID",
|
|
2457
|
+
value: "",
|
|
2458
|
+
comment: "Twilio account SID for SMS",
|
|
2459
|
+
required: false
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
key: "TWILIO_AUTH_TOKEN",
|
|
2463
|
+
value: "",
|
|
2464
|
+
comment: "Twilio auth token",
|
|
2465
|
+
required: false
|
|
2466
|
+
}
|
|
2467
|
+
]
|
|
2468
|
+
}
|
|
2469
|
+
],
|
|
2470
|
+
analytics: [
|
|
2471
|
+
{
|
|
2472
|
+
title: "Analytics/Metrics Configuration",
|
|
2473
|
+
variables: [
|
|
2474
|
+
{
|
|
2475
|
+
key: "ANALYTICS_ENABLED",
|
|
2476
|
+
value: "true",
|
|
2477
|
+
comment: "Enable analytics and metrics collection",
|
|
2478
|
+
required: true
|
|
2479
|
+
},
|
|
2480
|
+
{
|
|
2481
|
+
key: "ANALYTICS_PREFIX",
|
|
2482
|
+
value: "app",
|
|
2483
|
+
comment: "Metrics prefix",
|
|
2484
|
+
required: true
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
key: "PROMETHEUS_ENABLED",
|
|
2488
|
+
value: "true",
|
|
2489
|
+
comment: "Enable Prometheus metrics endpoint",
|
|
2490
|
+
required: true
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
key: "METRICS_FLUSH_INTERVAL",
|
|
2494
|
+
value: "60000",
|
|
2495
|
+
comment: "Metrics flush interval in milliseconds",
|
|
2496
|
+
required: true
|
|
2497
|
+
}
|
|
2498
|
+
]
|
|
2499
|
+
}
|
|
2500
|
+
],
|
|
2501
|
+
"media-processing": [
|
|
2502
|
+
{
|
|
2503
|
+
title: "Media Processing Configuration",
|
|
2504
|
+
variables: [
|
|
2505
|
+
{
|
|
2506
|
+
key: "FFMPEG_PATH",
|
|
2507
|
+
value: "ffmpeg",
|
|
2508
|
+
comment: "Path to FFmpeg binary",
|
|
2509
|
+
required: true
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
key: "FFPROBE_PATH",
|
|
2513
|
+
value: "ffprobe",
|
|
2514
|
+
comment: "Path to FFprobe binary",
|
|
2515
|
+
required: true
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
key: "MEDIA_TEMP_DIR",
|
|
2519
|
+
value: "./temp/media",
|
|
2520
|
+
comment: "Temporary directory for media processing",
|
|
2521
|
+
required: true
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
key: "MEDIA_MAX_CONCURRENT",
|
|
2525
|
+
value: "3",
|
|
2526
|
+
comment: "Maximum concurrent processing jobs",
|
|
2527
|
+
required: true
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
key: "MEDIA_GPU_ACCELERATION",
|
|
2531
|
+
value: "false",
|
|
2532
|
+
comment: "Enable GPU acceleration (requires NVIDIA GPU)",
|
|
2533
|
+
required: false
|
|
2534
|
+
}
|
|
2535
|
+
]
|
|
2536
|
+
}
|
|
2537
|
+
],
|
|
2538
|
+
"api-versioning": [
|
|
2539
|
+
{
|
|
2540
|
+
title: "API Versioning Configuration",
|
|
2541
|
+
variables: [
|
|
2542
|
+
{
|
|
2543
|
+
key: "API_VERSION_STRATEGY",
|
|
2544
|
+
value: "url",
|
|
2545
|
+
comment: "Versioning strategy: url, header, query, accept-header",
|
|
2546
|
+
required: true
|
|
2547
|
+
},
|
|
2548
|
+
{
|
|
2549
|
+
key: "API_DEFAULT_VERSION",
|
|
2550
|
+
value: "v1",
|
|
2551
|
+
comment: "Default API version",
|
|
2552
|
+
required: true
|
|
2553
|
+
},
|
|
2554
|
+
{
|
|
2555
|
+
key: "API_VERSION_HEADER",
|
|
2556
|
+
value: "X-API-Version",
|
|
2557
|
+
comment: "Header name for version (if strategy is header)",
|
|
2558
|
+
required: false
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
key: "API_VERSION_STRICT",
|
|
2562
|
+
value: "true",
|
|
2563
|
+
comment: "Strict mode - reject unknown versions",
|
|
2564
|
+
required: true
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
key: "API_DEPRECATION_WARNINGS",
|
|
2568
|
+
value: "true",
|
|
2569
|
+
comment: "Show deprecation warnings in headers",
|
|
2570
|
+
required: true
|
|
2571
|
+
}
|
|
2572
|
+
]
|
|
2573
|
+
}
|
|
2574
|
+
]
|
|
2575
|
+
};
|
|
2576
|
+
return moduleEnvMap[moduleName] || [];
|
|
2577
|
+
}
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// src/cli/utils/template-manager.ts
|
|
2581
|
+
import * as fs4 from "fs/promises";
|
|
2582
|
+
import * as path5 from "path";
|
|
2583
|
+
import { createHash } from "crypto";
|
|
2584
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2585
|
+
var TemplateManager = class {
|
|
2586
|
+
templatesDir;
|
|
2587
|
+
manifestsDir;
|
|
2588
|
+
constructor(projectRoot) {
|
|
2589
|
+
this.templatesDir = path5.join(projectRoot, ".servcraft", "templates");
|
|
2590
|
+
this.manifestsDir = path5.join(projectRoot, ".servcraft", "manifests");
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Initialize template system
|
|
2594
|
+
*/
|
|
2595
|
+
async initialize() {
|
|
2596
|
+
await fs4.mkdir(this.templatesDir, { recursive: true });
|
|
2597
|
+
await fs4.mkdir(this.manifestsDir, { recursive: true });
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Save module template
|
|
2601
|
+
*/
|
|
2602
|
+
async saveTemplate(moduleName, files) {
|
|
2603
|
+
await this.initialize();
|
|
2604
|
+
const moduleTemplateDir = path5.join(this.templatesDir, moduleName);
|
|
2605
|
+
await fs4.mkdir(moduleTemplateDir, { recursive: true });
|
|
2606
|
+
for (const [fileName, content] of Object.entries(files)) {
|
|
2607
|
+
const filePath = path5.join(moduleTemplateDir, fileName);
|
|
2608
|
+
await fs4.writeFile(filePath, content, "utf-8");
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Get template content
|
|
2613
|
+
*/
|
|
2614
|
+
async getTemplate(moduleName, fileName) {
|
|
2615
|
+
try {
|
|
2616
|
+
const filePath = path5.join(this.templatesDir, moduleName, fileName);
|
|
2617
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
2618
|
+
} catch {
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Save module manifest
|
|
2624
|
+
*/
|
|
2625
|
+
async saveManifest(moduleName, files) {
|
|
2626
|
+
await this.initialize();
|
|
2627
|
+
const fileHashes = {};
|
|
2628
|
+
for (const [fileName, content] of Object.entries(files)) {
|
|
2629
|
+
fileHashes[fileName] = {
|
|
2630
|
+
hash: this.hashContent(content),
|
|
2631
|
+
path: `src/modules/${moduleName}/${fileName}`
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
const manifest = {
|
|
2635
|
+
name: moduleName,
|
|
2636
|
+
version: "1.0.0",
|
|
2637
|
+
files: fileHashes,
|
|
2638
|
+
installedAt: /* @__PURE__ */ new Date(),
|
|
2639
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2640
|
+
};
|
|
2641
|
+
const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
|
|
2642
|
+
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Get module manifest
|
|
2646
|
+
*/
|
|
2647
|
+
async getManifest(moduleName) {
|
|
2648
|
+
try {
|
|
2649
|
+
const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
|
|
2650
|
+
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
2651
|
+
return JSON.parse(content);
|
|
2652
|
+
} catch {
|
|
2653
|
+
return null;
|
|
2654
|
+
}
|
|
3327
2655
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
const
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
const
|
|
3337
|
-
const
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
2656
|
+
/**
|
|
2657
|
+
* Check if file has been modified by user
|
|
2658
|
+
*/
|
|
2659
|
+
async isFileModified(moduleName, fileName, currentContent) {
|
|
2660
|
+
const manifest = await this.getManifest(moduleName);
|
|
2661
|
+
if (!manifest || !manifest.files[fileName]) {
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
const originalHash = manifest.files[fileName].hash;
|
|
2665
|
+
const currentHash = this.hashContent(currentContent);
|
|
2666
|
+
return originalHash !== currentHash;
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Get all modified files in a module
|
|
2670
|
+
*/
|
|
2671
|
+
async getModifiedFiles(moduleName, moduleDir) {
|
|
2672
|
+
const manifest = await this.getManifest(moduleName);
|
|
2673
|
+
if (!manifest) {
|
|
2674
|
+
return [];
|
|
2675
|
+
}
|
|
2676
|
+
const results = [];
|
|
2677
|
+
for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
|
|
2678
|
+
const filePath = path5.join(moduleDir, fileName);
|
|
2679
|
+
if (!existsSync2(filePath)) {
|
|
2680
|
+
results.push({
|
|
2681
|
+
fileName,
|
|
2682
|
+
isModified: true,
|
|
2683
|
+
originalHash: fileInfo.hash,
|
|
2684
|
+
currentHash: ""
|
|
2685
|
+
});
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
const currentContent = await fs4.readFile(filePath, "utf-8");
|
|
2689
|
+
const currentHash = this.hashContent(currentContent);
|
|
2690
|
+
results.push({
|
|
2691
|
+
fileName,
|
|
2692
|
+
isModified: fileInfo.hash !== currentHash,
|
|
2693
|
+
originalHash: fileInfo.hash,
|
|
2694
|
+
currentHash
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
return results;
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Create backup of module
|
|
2701
|
+
*/
|
|
2702
|
+
async createBackup(moduleName, moduleDir) {
|
|
2703
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
2704
|
+
const backupDir = path5.join(path5.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
|
|
2705
|
+
await this.copyDirectory(moduleDir, backupDir);
|
|
2706
|
+
return backupDir;
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Copy directory recursively
|
|
2710
|
+
*/
|
|
2711
|
+
async copyDirectory(src, dest) {
|
|
2712
|
+
await fs4.mkdir(dest, { recursive: true });
|
|
2713
|
+
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
2714
|
+
for (const entry of entries) {
|
|
2715
|
+
const srcPath = path5.join(src, entry.name);
|
|
2716
|
+
const destPath = path5.join(dest, entry.name);
|
|
2717
|
+
if (entry.isDirectory()) {
|
|
2718
|
+
await this.copyDirectory(srcPath, destPath);
|
|
2719
|
+
} else {
|
|
2720
|
+
await fs4.copyFile(srcPath, destPath);
|
|
2721
|
+
}
|
|
3342
2722
|
}
|
|
3343
|
-
await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
|
|
3344
|
-
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
3345
|
-
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
3346
|
-
} catch (err) {
|
|
3347
|
-
spinner.fail("Failed to generate schemas");
|
|
3348
|
-
error(err instanceof Error ? err.message : String(err));
|
|
3349
2723
|
}
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
const
|
|
3355
|
-
|
|
3356
|
-
const
|
|
3357
|
-
const
|
|
3358
|
-
const
|
|
3359
|
-
const
|
|
3360
|
-
const
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
2724
|
+
/**
|
|
2725
|
+
* Perform 3-way merge
|
|
2726
|
+
*/
|
|
2727
|
+
async mergeFiles(original, modified, incoming) {
|
|
2728
|
+
const conflicts = [];
|
|
2729
|
+
let hasConflicts = false;
|
|
2730
|
+
const originalLines = original.split("\n");
|
|
2731
|
+
const modifiedLines = modified.split("\n");
|
|
2732
|
+
const incomingLines = incoming.split("\n");
|
|
2733
|
+
const merged = [];
|
|
2734
|
+
const maxLength = Math.max(originalLines.length, modifiedLines.length, incomingLines.length);
|
|
2735
|
+
for (let i = 0; i < maxLength; i++) {
|
|
2736
|
+
const origLine = originalLines[i] || "";
|
|
2737
|
+
const modLine = modifiedLines[i] || "";
|
|
2738
|
+
const incLine = incomingLines[i] || "";
|
|
2739
|
+
if (modLine === origLine) {
|
|
2740
|
+
merged.push(incLine);
|
|
2741
|
+
} else if (incLine === origLine) {
|
|
2742
|
+
merged.push(modLine);
|
|
2743
|
+
} else if (modLine === incLine) {
|
|
2744
|
+
merged.push(modLine);
|
|
2745
|
+
} else {
|
|
2746
|
+
hasConflicts = true;
|
|
2747
|
+
conflicts.push(`Line ${i + 1}: User and template both modified`);
|
|
2748
|
+
merged.push(`<<<<<<< YOUR VERSION`);
|
|
2749
|
+
merged.push(modLine);
|
|
2750
|
+
merged.push(`=======`);
|
|
2751
|
+
merged.push(incLine);
|
|
2752
|
+
merged.push(`>>>>>>> NEW VERSION`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return {
|
|
2756
|
+
merged: merged.join("\n"),
|
|
2757
|
+
hasConflicts,
|
|
2758
|
+
conflicts
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Generate diff between two files
|
|
2763
|
+
*/
|
|
2764
|
+
generateDiff(original, modified) {
|
|
2765
|
+
const originalLines = original.split("\n");
|
|
2766
|
+
const modifiedLines = modified.split("\n");
|
|
2767
|
+
const diff = [];
|
|
2768
|
+
diff.push("--- Original");
|
|
2769
|
+
diff.push("+++ Modified");
|
|
2770
|
+
diff.push("");
|
|
2771
|
+
const maxLength = Math.max(originalLines.length, modifiedLines.length);
|
|
2772
|
+
for (let i = 0; i < maxLength; i++) {
|
|
2773
|
+
const origLine = originalLines[i];
|
|
2774
|
+
const modLine = modifiedLines[i];
|
|
2775
|
+
if (origLine !== modLine) {
|
|
2776
|
+
if (origLine !== void 0) {
|
|
2777
|
+
diff.push(`- ${origLine}`);
|
|
2778
|
+
}
|
|
2779
|
+
if (modLine !== void 0) {
|
|
2780
|
+
diff.push(`+ ${modLine}`);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return diff.join("\n");
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Hash content for change detection
|
|
2788
|
+
*/
|
|
2789
|
+
hashContent(content) {
|
|
2790
|
+
return createHash("md5").update(content).digest("hex");
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Check if module is installed
|
|
2794
|
+
*/
|
|
2795
|
+
async isModuleInstalled(moduleName) {
|
|
2796
|
+
const manifest = await this.getManifest(moduleName);
|
|
2797
|
+
return manifest !== null;
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Update manifest after merge
|
|
2801
|
+
*/
|
|
2802
|
+
async updateManifest(moduleName, files) {
|
|
2803
|
+
const manifest = await this.getManifest(moduleName);
|
|
2804
|
+
if (!manifest) {
|
|
2805
|
+
await this.saveManifest(moduleName, files);
|
|
3364
2806
|
return;
|
|
3365
2807
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
2808
|
+
const fileHashes = {};
|
|
2809
|
+
for (const [fileName, content] of Object.entries(files)) {
|
|
2810
|
+
fileHashes[fileName] = {
|
|
2811
|
+
hash: this.hashContent(content),
|
|
2812
|
+
path: `src/modules/${moduleName}/${fileName}`
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
manifest.files = fileHashes;
|
|
2816
|
+
manifest.updatedAt = /* @__PURE__ */ new Date();
|
|
2817
|
+
const manifestPath = path5.join(this.manifestsDir, `${moduleName}.json`);
|
|
2818
|
+
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
3372
2819
|
}
|
|
3373
|
-
}
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
"decimal",
|
|
3390
|
-
"json"
|
|
3391
|
-
];
|
|
3392
|
-
let addMore = true;
|
|
3393
|
-
while (addMore) {
|
|
3394
|
-
const answers = await inquirer2.prompt([
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
// src/cli/utils/interactive-prompt.ts
|
|
2823
|
+
import inquirer3 from "inquirer";
|
|
2824
|
+
import chalk3 from "chalk";
|
|
2825
|
+
var InteractivePrompt = class {
|
|
2826
|
+
/**
|
|
2827
|
+
* Ask what to do when module already exists
|
|
2828
|
+
*/
|
|
2829
|
+
static async askModuleExists(moduleName, hasModifications) {
|
|
2830
|
+
console.log(chalk3.yellow(`
|
|
2831
|
+
\u26A0\uFE0F Module "${moduleName}" already exists`));
|
|
2832
|
+
if (hasModifications) {
|
|
2833
|
+
console.log(chalk3.yellow(" Some files have been modified by you.\n"));
|
|
2834
|
+
}
|
|
2835
|
+
const { action } = await inquirer3.prompt([
|
|
3395
2836
|
{
|
|
3396
|
-
type: "
|
|
3397
|
-
name: "
|
|
3398
|
-
message: "
|
|
2837
|
+
type: "list",
|
|
2838
|
+
name: "action",
|
|
2839
|
+
message: "What would you like to do?",
|
|
2840
|
+
choices: [
|
|
2841
|
+
{
|
|
2842
|
+
name: "\u2713 Skip (keep existing files, recommended if you made custom changes)",
|
|
2843
|
+
value: "skip"
|
|
2844
|
+
},
|
|
2845
|
+
{
|
|
2846
|
+
name: "\u21BB Update (merge new features, preserve your customizations)",
|
|
2847
|
+
value: "update"
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
name: "\u26A0 Overwrite (replace all files, will lose your changes)",
|
|
2851
|
+
value: "overwrite"
|
|
2852
|
+
},
|
|
2853
|
+
{
|
|
2854
|
+
name: "\u{1F4CB} Show diff (see what changed)",
|
|
2855
|
+
value: "diff"
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
name: "\u{1F4BE} Backup & overwrite (save backup then replace)",
|
|
2859
|
+
value: "backup-overwrite"
|
|
2860
|
+
}
|
|
2861
|
+
],
|
|
2862
|
+
default: "skip"
|
|
3399
2863
|
}
|
|
3400
2864
|
]);
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
2865
|
+
return { action };
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Ask what to do with a specific file
|
|
2869
|
+
*/
|
|
2870
|
+
static async askFileAction(fileName, isModified, yourLines, newLines) {
|
|
2871
|
+
console.log(chalk3.cyan(`
|
|
2872
|
+
\u{1F4C1} ${fileName}`));
|
|
2873
|
+
console.log(
|
|
2874
|
+
chalk3.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
|
|
2875
|
+
);
|
|
2876
|
+
console.log(chalk3.gray(` New version: ${newLines} lines
|
|
2877
|
+
`));
|
|
2878
|
+
const { action } = await inquirer3.prompt([
|
|
3406
2879
|
{
|
|
3407
2880
|
type: "list",
|
|
3408
|
-
name: "
|
|
3409
|
-
message:
|
|
3410
|
-
choices:
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
2881
|
+
name: "action",
|
|
2882
|
+
message: "Action for this file:",
|
|
2883
|
+
choices: [
|
|
2884
|
+
{
|
|
2885
|
+
name: "[M]erge - Smart merge, preserve your changes",
|
|
2886
|
+
value: "merge"
|
|
2887
|
+
},
|
|
2888
|
+
{
|
|
2889
|
+
name: "[K]eep - Keep your version (skip update)",
|
|
2890
|
+
value: "keep"
|
|
2891
|
+
},
|
|
2892
|
+
{
|
|
2893
|
+
name: "[O]verwrite - Use new version",
|
|
2894
|
+
value: "overwrite"
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
name: "[D]iff - Show differences",
|
|
2898
|
+
value: "diff"
|
|
2899
|
+
},
|
|
2900
|
+
{
|
|
2901
|
+
name: "[S]kip - Skip this file",
|
|
2902
|
+
value: "skip"
|
|
2903
|
+
}
|
|
2904
|
+
],
|
|
2905
|
+
default: isModified ? "merge" : "overwrite"
|
|
2906
|
+
}
|
|
2907
|
+
]);
|
|
2908
|
+
return { action };
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Confirm action
|
|
2912
|
+
*/
|
|
2913
|
+
static async confirm(message, defaultValue = false) {
|
|
2914
|
+
const { confirmed } = await inquirer3.prompt([
|
|
3425
2915
|
{
|
|
3426
2916
|
type: "confirm",
|
|
3427
|
-
name: "
|
|
3428
|
-
message
|
|
3429
|
-
default:
|
|
2917
|
+
name: "confirmed",
|
|
2918
|
+
message,
|
|
2919
|
+
default: defaultValue
|
|
3430
2920
|
}
|
|
3431
2921
|
]);
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
2922
|
+
return confirmed;
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Display diff and ask to continue
|
|
2926
|
+
*/
|
|
2927
|
+
static async showDiffAndAsk(diff) {
|
|
2928
|
+
console.log(chalk3.cyan("\n\u{1F4CA} Differences:\n"));
|
|
2929
|
+
console.log(diff);
|
|
2930
|
+
return await this.confirm("\nDo you want to proceed with this change?", true);
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Display merge conflicts
|
|
2934
|
+
*/
|
|
2935
|
+
static displayConflicts(conflicts) {
|
|
2936
|
+
console.log(chalk3.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
|
|
2937
|
+
conflicts.forEach((conflict, i) => {
|
|
2938
|
+
console.log(chalk3.yellow(` ${i + 1}. ${conflict}`));
|
|
3438
2939
|
});
|
|
3439
|
-
console.log(
|
|
3440
|
-
|
|
2940
|
+
console.log(chalk3.gray("\n Conflict markers have been added to the file:"));
|
|
2941
|
+
console.log(chalk3.gray(" <<<<<<< YOUR VERSION"));
|
|
2942
|
+
console.log(chalk3.gray(" ... your code ..."));
|
|
2943
|
+
console.log(chalk3.gray(" ======="));
|
|
2944
|
+
console.log(chalk3.gray(" ... new code ..."));
|
|
2945
|
+
console.log(chalk3.gray(" >>>>>>> NEW VERSION\n"));
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Show backup location
|
|
2949
|
+
*/
|
|
2950
|
+
static showBackupCreated(backupPath) {
|
|
2951
|
+
console.log(chalk3.green(`
|
|
2952
|
+
\u2713 Backup created: ${chalk3.cyan(backupPath)}`));
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Show merge summary
|
|
2956
|
+
*/
|
|
2957
|
+
static showMergeSummary(stats) {
|
|
2958
|
+
console.log(chalk3.bold("\n\u{1F4CA} Merge Summary:\n"));
|
|
2959
|
+
if (stats.merged > 0) {
|
|
2960
|
+
console.log(chalk3.green(` \u2713 Merged: ${stats.merged} file(s)`));
|
|
2961
|
+
}
|
|
2962
|
+
if (stats.kept > 0) {
|
|
2963
|
+
console.log(chalk3.blue(` \u2192 Kept: ${stats.kept} file(s)`));
|
|
2964
|
+
}
|
|
2965
|
+
if (stats.overwritten > 0) {
|
|
2966
|
+
console.log(chalk3.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
|
|
2967
|
+
}
|
|
2968
|
+
if (stats.conflicts > 0) {
|
|
2969
|
+
console.log(chalk3.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
|
|
2970
|
+
console.log(chalk3.gray("\n Please resolve conflicts manually before committing.\n"));
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Ask for batch action on all files
|
|
2975
|
+
*/
|
|
2976
|
+
static async askBatchAction() {
|
|
2977
|
+
const { action } = await inquirer3.prompt([
|
|
2978
|
+
{
|
|
2979
|
+
type: "list",
|
|
2980
|
+
name: "action",
|
|
2981
|
+
message: "Multiple files found. Choose action:",
|
|
2982
|
+
choices: [
|
|
2983
|
+
{
|
|
2984
|
+
name: "Ask for each file individually (recommended)",
|
|
2985
|
+
value: "individual"
|
|
2986
|
+
},
|
|
2987
|
+
{
|
|
2988
|
+
name: "Merge all files automatically",
|
|
2989
|
+
value: "merge-all"
|
|
2990
|
+
},
|
|
2991
|
+
{
|
|
2992
|
+
name: "Keep all existing files (skip update)",
|
|
2993
|
+
value: "keep-all"
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
name: "Overwrite all files with new versions",
|
|
2997
|
+
value: "overwrite-all"
|
|
2998
|
+
}
|
|
2999
|
+
],
|
|
3000
|
+
default: "individual"
|
|
3001
|
+
}
|
|
3002
|
+
]);
|
|
3003
|
+
return action;
|
|
3441
3004
|
}
|
|
3442
|
-
|
|
3443
|
-
}
|
|
3005
|
+
};
|
|
3444
3006
|
|
|
3445
3007
|
// src/cli/commands/add-module.ts
|
|
3446
|
-
import { Command as Command3 } from "commander";
|
|
3447
|
-
import path5 from "path";
|
|
3448
|
-
import ora4 from "ora";
|
|
3449
|
-
import chalk3 from "chalk";
|
|
3450
3008
|
var AVAILABLE_MODULES = {
|
|
3451
3009
|
auth: {
|
|
3452
3010
|
name: "Authentication",
|
|
3453
3011
|
description: "JWT authentication with access/refresh tokens",
|
|
3454
|
-
files: [
|
|
3012
|
+
files: [
|
|
3013
|
+
"auth.service",
|
|
3014
|
+
"auth.controller",
|
|
3015
|
+
"auth.routes",
|
|
3016
|
+
"auth.middleware",
|
|
3017
|
+
"auth.schemas",
|
|
3018
|
+
"auth.types",
|
|
3019
|
+
"index"
|
|
3020
|
+
]
|
|
3455
3021
|
},
|
|
3456
3022
|
users: {
|
|
3457
3023
|
name: "User Management",
|
|
3458
3024
|
description: "User CRUD with RBAC (roles & permissions)",
|
|
3459
|
-
files: [
|
|
3025
|
+
files: [
|
|
3026
|
+
"user.service",
|
|
3027
|
+
"user.controller",
|
|
3028
|
+
"user.repository",
|
|
3029
|
+
"user.routes",
|
|
3030
|
+
"user.schemas",
|
|
3031
|
+
"user.types",
|
|
3032
|
+
"index"
|
|
3033
|
+
]
|
|
3460
3034
|
},
|
|
3461
3035
|
email: {
|
|
3462
3036
|
name: "Email Service",
|
|
@@ -3468,91 +3042,222 @@ var AVAILABLE_MODULES = {
|
|
|
3468
3042
|
description: "Activity logging and audit trail",
|
|
3469
3043
|
files: ["audit.service", "audit.types", "index"]
|
|
3470
3044
|
},
|
|
3471
|
-
upload: {
|
|
3472
|
-
name: "File Upload",
|
|
3473
|
-
description: "File upload with local/S3 storage",
|
|
3474
|
-
files: ["upload.service", "upload.controller", "upload.routes", "upload.types", "index"]
|
|
3475
|
-
},
|
|
3476
3045
|
cache: {
|
|
3477
3046
|
name: "Redis Cache",
|
|
3478
|
-
description: "Redis caching
|
|
3047
|
+
description: "Redis caching with TTL & invalidation",
|
|
3479
3048
|
files: ["cache.service", "cache.types", "index"]
|
|
3480
3049
|
},
|
|
3481
|
-
|
|
3050
|
+
upload: {
|
|
3051
|
+
name: "File Upload",
|
|
3052
|
+
description: "File upload with local/S3/Cloudinary storage",
|
|
3053
|
+
files: ["upload.service", "upload.routes", "upload.types", "index"]
|
|
3054
|
+
},
|
|
3055
|
+
mfa: {
|
|
3056
|
+
name: "MFA/TOTP",
|
|
3057
|
+
description: "Two-factor authentication with QR codes",
|
|
3058
|
+
files: ["mfa.service", "mfa.routes", "totp.ts", "types.ts", "index"]
|
|
3059
|
+
},
|
|
3060
|
+
oauth: {
|
|
3061
|
+
name: "OAuth",
|
|
3062
|
+
description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
|
|
3063
|
+
files: ["oauth.service", "oauth.routes", "providers", "types.ts", "index"]
|
|
3064
|
+
},
|
|
3065
|
+
payment: {
|
|
3066
|
+
name: "Payments",
|
|
3067
|
+
description: "Payment processing (Stripe, PayPal, Mobile Money)",
|
|
3068
|
+
files: ["payment.service", "payment.routes", "providers", "types.ts", "index"]
|
|
3069
|
+
},
|
|
3070
|
+
notification: {
|
|
3482
3071
|
name: "Notifications",
|
|
3483
|
-
description: "
|
|
3484
|
-
files: ["notification.service", "
|
|
3072
|
+
description: "Email, SMS, Push notifications",
|
|
3073
|
+
files: ["notification.service", "types.ts", "index"]
|
|
3074
|
+
},
|
|
3075
|
+
"rate-limit": {
|
|
3076
|
+
name: "Rate Limiting",
|
|
3077
|
+
description: "Advanced rate limiting with multiple algorithms",
|
|
3078
|
+
files: [
|
|
3079
|
+
"rate-limit.service",
|
|
3080
|
+
"rate-limit.middleware",
|
|
3081
|
+
"rate-limit.routes",
|
|
3082
|
+
"stores",
|
|
3083
|
+
"types.ts",
|
|
3084
|
+
"index"
|
|
3085
|
+
]
|
|
3086
|
+
},
|
|
3087
|
+
webhook: {
|
|
3088
|
+
name: "Webhooks",
|
|
3089
|
+
description: "Outgoing webhooks with HMAC signatures & retry",
|
|
3090
|
+
files: ["webhook.service", "webhook.routes", "signature.ts", "retry.ts", "types.ts", "index"]
|
|
3091
|
+
},
|
|
3092
|
+
queue: {
|
|
3093
|
+
name: "Queue/Jobs",
|
|
3094
|
+
description: "Background jobs with Bull/BullMQ & cron scheduling",
|
|
3095
|
+
files: ["queue.service", "cron.ts", "workers.ts", "routes.ts", "types.ts", "index"]
|
|
3096
|
+
},
|
|
3097
|
+
websocket: {
|
|
3098
|
+
name: "WebSockets",
|
|
3099
|
+
description: "Real-time communication with Socket.io",
|
|
3100
|
+
files: ["websocket.service", "features.ts", "middlewares.ts", "types.ts", "index"]
|
|
3101
|
+
},
|
|
3102
|
+
search: {
|
|
3103
|
+
name: "Search",
|
|
3104
|
+
description: "Full-text search with Elasticsearch/Meilisearch",
|
|
3105
|
+
files: ["search.service", "adapters", "types.ts", "index"]
|
|
3485
3106
|
},
|
|
3486
|
-
|
|
3487
|
-
name: "
|
|
3488
|
-
description: "
|
|
3489
|
-
files: ["
|
|
3107
|
+
i18n: {
|
|
3108
|
+
name: "i18n/Localization",
|
|
3109
|
+
description: "Multi-language support with 7+ locales",
|
|
3110
|
+
files: ["i18n.service", "i18n.middleware", "i18n.routes", "types.ts", "index"]
|
|
3111
|
+
},
|
|
3112
|
+
"feature-flag": {
|
|
3113
|
+
name: "Feature Flags",
|
|
3114
|
+
description: "A/B testing & progressive rollout",
|
|
3115
|
+
files: ["feature-flag.service", "feature-flag.routes", "types.ts", "index"]
|
|
3116
|
+
},
|
|
3117
|
+
analytics: {
|
|
3118
|
+
name: "Analytics/Metrics",
|
|
3119
|
+
description: "Prometheus metrics & event tracking",
|
|
3120
|
+
files: ["analytics.service", "analytics.routes", "types.ts", "index"]
|
|
3121
|
+
},
|
|
3122
|
+
"media-processing": {
|
|
3123
|
+
name: "Media Processing",
|
|
3124
|
+
description: "Image/video processing with FFmpeg",
|
|
3125
|
+
files: ["media-processing.service", "media-processing.routes", "types.ts", "index"]
|
|
3126
|
+
},
|
|
3127
|
+
"api-versioning": {
|
|
3128
|
+
name: "API Versioning",
|
|
3129
|
+
description: "Multiple API versions support",
|
|
3130
|
+
files: [
|
|
3131
|
+
"versioning.service",
|
|
3132
|
+
"versioning.middleware",
|
|
3133
|
+
"versioning.routes",
|
|
3134
|
+
"types.ts",
|
|
3135
|
+
"index"
|
|
3136
|
+
]
|
|
3490
3137
|
}
|
|
3491
3138
|
};
|
|
3492
|
-
var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3139
|
+
var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
|
|
3140
|
+
"[module]",
|
|
3141
|
+
"Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
|
|
3142
|
+
).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").action(
|
|
3143
|
+
async (moduleName, options) => {
|
|
3144
|
+
if (options?.list || !moduleName) {
|
|
3145
|
+
console.log(chalk4.bold("\n\u{1F4E6} Available Modules:\n"));
|
|
3146
|
+
for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
|
|
3147
|
+
console.log(` ${chalk4.cyan(key.padEnd(15))} ${mod.name}`);
|
|
3148
|
+
console.log(` ${" ".repeat(15)} ${chalk4.gray(mod.description)}
|
|
3498
3149
|
`);
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3150
|
+
}
|
|
3151
|
+
console.log(chalk4.bold("Usage:"));
|
|
3152
|
+
console.log(` ${chalk4.yellow("servcraft add auth")} Add authentication module`);
|
|
3153
|
+
console.log(` ${chalk4.yellow("servcraft add users")} Add user management module`);
|
|
3154
|
+
console.log(` ${chalk4.yellow("servcraft add email")} Add email service module
|
|
3504
3155
|
`);
|
|
3505
|
-
return;
|
|
3506
|
-
}
|
|
3507
|
-
const module = AVAILABLE_MODULES[moduleName];
|
|
3508
|
-
if (!module) {
|
|
3509
|
-
error(`Unknown module: ${moduleName}`);
|
|
3510
|
-
info('Run "servcraft add --list" to see available modules');
|
|
3511
|
-
return;
|
|
3512
|
-
}
|
|
3513
|
-
const spinner = ora4(`Adding ${module.name} module...`).start();
|
|
3514
|
-
try {
|
|
3515
|
-
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
3516
|
-
if (await fileExists(moduleDir)) {
|
|
3517
|
-
spinner.stop();
|
|
3518
|
-
warn(`Module "${moduleName}" already exists`);
|
|
3519
3156
|
return;
|
|
3520
3157
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3158
|
+
const module = AVAILABLE_MODULES[moduleName];
|
|
3159
|
+
if (!module) {
|
|
3160
|
+
error(`Unknown module: ${moduleName}`);
|
|
3161
|
+
info('Run "servcraft add --list" to see available modules');
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
const spinner = ora3(`Adding ${module.name} module...`).start();
|
|
3165
|
+
try {
|
|
3166
|
+
const moduleDir = path6.join(getModulesDir(), moduleName);
|
|
3167
|
+
const templateManager = new TemplateManager(process.cwd());
|
|
3168
|
+
const moduleExists = await fileExists(moduleDir);
|
|
3169
|
+
if (moduleExists) {
|
|
3170
|
+
spinner.stop();
|
|
3171
|
+
if (options?.skipExisting) {
|
|
3172
|
+
info(`Module "${moduleName}" already exists, skipping...`);
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
|
|
3176
|
+
const hasModifications = modifiedFiles.some((f) => f.isModified);
|
|
3177
|
+
let action;
|
|
3178
|
+
if (options?.force) {
|
|
3179
|
+
action = "overwrite";
|
|
3180
|
+
} else if (options?.update) {
|
|
3181
|
+
action = "update";
|
|
3182
|
+
} else {
|
|
3183
|
+
const choice = await InteractivePrompt.askModuleExists(moduleName, hasModifications);
|
|
3184
|
+
action = choice.action;
|
|
3185
|
+
}
|
|
3186
|
+
if (action === "skip") {
|
|
3187
|
+
info("Keeping existing module");
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
if (action === "diff") {
|
|
3191
|
+
await showDiffForModule(templateManager, moduleName, moduleDir);
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
if (action === "backup-overwrite" || action === "overwrite") {
|
|
3195
|
+
if (action === "backup-overwrite") {
|
|
3196
|
+
const backupPath = await templateManager.createBackup(moduleName, moduleDir);
|
|
3197
|
+
InteractivePrompt.showBackupCreated(backupPath);
|
|
3198
|
+
}
|
|
3199
|
+
await fs5.rm(moduleDir, { recursive: true, force: true });
|
|
3200
|
+
await ensureDir(moduleDir);
|
|
3201
|
+
await generateModuleFiles(moduleName, moduleDir);
|
|
3202
|
+
const files = await getModuleFiles(moduleName, moduleDir);
|
|
3203
|
+
await templateManager.saveTemplate(moduleName, files);
|
|
3204
|
+
await templateManager.saveManifest(moduleName, files);
|
|
3205
|
+
spinner.succeed(
|
|
3206
|
+
`${module.name} module ${action === "backup-overwrite" ? "backed up and " : ""}overwritten!`
|
|
3207
|
+
);
|
|
3208
|
+
} else if (action === "update") {
|
|
3209
|
+
await performSmartMerge(templateManager, moduleName, moduleDir, module.name);
|
|
3210
|
+
}
|
|
3211
|
+
} else {
|
|
3212
|
+
await ensureDir(moduleDir);
|
|
3213
|
+
await generateModuleFiles(moduleName, moduleDir);
|
|
3214
|
+
const files = await getModuleFiles(moduleName, moduleDir);
|
|
3215
|
+
await templateManager.saveTemplate(moduleName, files);
|
|
3216
|
+
await templateManager.saveManifest(moduleName, files);
|
|
3217
|
+
spinner.succeed(`${module.name} module added successfully!`);
|
|
3218
|
+
}
|
|
3219
|
+
if (!moduleExists) {
|
|
3220
|
+
console.log("\n\u{1F4C1} Files created:");
|
|
3221
|
+
module.files.forEach((f) => success(` src/modules/${moduleName}/${f}.ts`));
|
|
3222
|
+
}
|
|
3223
|
+
const envManager = new EnvManager(process.cwd());
|
|
3224
|
+
const envSections = EnvManager.getModuleEnvVariables(moduleName);
|
|
3225
|
+
if (envSections.length > 0) {
|
|
3226
|
+
const envSpinner = ora3("Updating environment variables...").start();
|
|
3227
|
+
try {
|
|
3228
|
+
const result = await envManager.addVariables(envSections);
|
|
3229
|
+
envSpinner.succeed("Environment variables updated!");
|
|
3230
|
+
if (result.created) {
|
|
3231
|
+
info("\n\u{1F4DD} Created new .env file");
|
|
3232
|
+
}
|
|
3233
|
+
if (result.added.length > 0) {
|
|
3234
|
+
console.log(chalk4.bold("\n\u2705 Added to .env:"));
|
|
3235
|
+
result.added.forEach((key) => success(` ${key}`));
|
|
3236
|
+
}
|
|
3237
|
+
if (result.skipped.length > 0) {
|
|
3238
|
+
console.log(chalk4.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
|
|
3239
|
+
result.skipped.forEach((key) => info(` ${key}`));
|
|
3240
|
+
}
|
|
3241
|
+
const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
|
|
3242
|
+
if (requiredVars.length > 0) {
|
|
3243
|
+
console.log(chalk4.bold("\n\u26A0\uFE0F Required configuration:"));
|
|
3244
|
+
requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
|
|
3245
|
+
}
|
|
3246
|
+
} catch (err) {
|
|
3247
|
+
envSpinner.fail("Failed to update environment variables");
|
|
3248
|
+
error(err instanceof Error ? err.message : String(err));
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
console.log("\n\u{1F4CC} Next steps:");
|
|
3252
|
+
info(" 1. Configure environment variables in .env (if needed)");
|
|
3253
|
+
info(" 2. Register the module in your main app file");
|
|
3254
|
+
info(" 3. Run database migrations if needed");
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
spinner.fail("Failed to add module");
|
|
3257
|
+
error(err instanceof Error ? err.message : String(err));
|
|
3258
|
+
}
|
|
3554
3259
|
}
|
|
3555
|
-
|
|
3260
|
+
);
|
|
3556
3261
|
async function generateAuthModule(dir) {
|
|
3557
3262
|
const files = {
|
|
3558
3263
|
"auth.types.ts": `export interface JwtPayload {
|
|
@@ -3597,7 +3302,7 @@ export * from './auth.schemas.js';
|
|
|
3597
3302
|
`
|
|
3598
3303
|
};
|
|
3599
3304
|
for (const [name, content] of Object.entries(files)) {
|
|
3600
|
-
await writeFile(
|
|
3305
|
+
await writeFile(path6.join(dir, name), content);
|
|
3601
3306
|
}
|
|
3602
3307
|
}
|
|
3603
3308
|
async function generateUsersModule(dir) {
|
|
@@ -3637,7 +3342,7 @@ export * from './user.schemas.js';
|
|
|
3637
3342
|
`
|
|
3638
3343
|
};
|
|
3639
3344
|
for (const [name, content] of Object.entries(files)) {
|
|
3640
|
-
await writeFile(
|
|
3345
|
+
await writeFile(path6.join(dir, name), content);
|
|
3641
3346
|
}
|
|
3642
3347
|
}
|
|
3643
3348
|
async function generateEmailModule(dir) {
|
|
@@ -3694,7 +3399,7 @@ export { EmailService, emailService } from './email.service.js';
|
|
|
3694
3399
|
`
|
|
3695
3400
|
};
|
|
3696
3401
|
for (const [name, content] of Object.entries(files)) {
|
|
3697
|
-
await writeFile(
|
|
3402
|
+
await writeFile(path6.join(dir, name), content);
|
|
3698
3403
|
}
|
|
3699
3404
|
}
|
|
3700
3405
|
async function generateAuditModule(dir) {
|
|
@@ -3738,7 +3443,7 @@ export { AuditService, auditService } from './audit.service.js';
|
|
|
3738
3443
|
`
|
|
3739
3444
|
};
|
|
3740
3445
|
for (const [name, content] of Object.entries(files)) {
|
|
3741
|
-
await writeFile(
|
|
3446
|
+
await writeFile(path6.join(dir, name), content);
|
|
3742
3447
|
}
|
|
3743
3448
|
}
|
|
3744
3449
|
async function generateUploadModule(dir) {
|
|
@@ -3764,7 +3469,7 @@ export interface UploadOptions {
|
|
|
3764
3469
|
`
|
|
3765
3470
|
};
|
|
3766
3471
|
for (const [name, content] of Object.entries(files)) {
|
|
3767
|
-
await writeFile(
|
|
3472
|
+
await writeFile(path6.join(dir, name), content);
|
|
3768
3473
|
}
|
|
3769
3474
|
}
|
|
3770
3475
|
async function generateCacheModule(dir) {
|
|
@@ -3810,7 +3515,7 @@ export { CacheService, cacheService } from './cache.service.js';
|
|
|
3810
3515
|
`
|
|
3811
3516
|
};
|
|
3812
3517
|
for (const [name, content] of Object.entries(files)) {
|
|
3813
|
-
await writeFile(
|
|
3518
|
+
await writeFile(path6.join(dir, name), content);
|
|
3814
3519
|
}
|
|
3815
3520
|
}
|
|
3816
3521
|
async function generateGenericModule(dir, name) {
|
|
@@ -3824,18 +3529,184 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
|
|
|
3824
3529
|
`
|
|
3825
3530
|
};
|
|
3826
3531
|
for (const [fileName, content] of Object.entries(files)) {
|
|
3827
|
-
await writeFile(
|
|
3532
|
+
await writeFile(path6.join(dir, fileName), content);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
async function generateModuleFiles(moduleName, moduleDir) {
|
|
3536
|
+
const sourceModuleDir = path6.join(process.cwd(), "src", "modules", moduleName);
|
|
3537
|
+
if (await fileExists(sourceModuleDir)) {
|
|
3538
|
+
await copyModuleFromSource(sourceModuleDir, moduleDir);
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
switch (moduleName) {
|
|
3542
|
+
case "auth":
|
|
3543
|
+
await generateAuthModule(moduleDir);
|
|
3544
|
+
break;
|
|
3545
|
+
case "users":
|
|
3546
|
+
await generateUsersModule(moduleDir);
|
|
3547
|
+
break;
|
|
3548
|
+
case "email":
|
|
3549
|
+
await generateEmailModule(moduleDir);
|
|
3550
|
+
break;
|
|
3551
|
+
case "audit":
|
|
3552
|
+
await generateAuditModule(moduleDir);
|
|
3553
|
+
break;
|
|
3554
|
+
case "upload":
|
|
3555
|
+
await generateUploadModule(moduleDir);
|
|
3556
|
+
break;
|
|
3557
|
+
case "cache":
|
|
3558
|
+
await generateCacheModule(moduleDir);
|
|
3559
|
+
break;
|
|
3560
|
+
default:
|
|
3561
|
+
await generateGenericModule(moduleDir, moduleName);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
async function copyModuleFromSource(sourceDir, targetDir) {
|
|
3565
|
+
const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
|
|
3566
|
+
for (const entry of entries) {
|
|
3567
|
+
const sourcePath = path6.join(sourceDir, entry.name);
|
|
3568
|
+
const targetPath = path6.join(targetDir, entry.name);
|
|
3569
|
+
if (entry.isDirectory()) {
|
|
3570
|
+
await fs5.mkdir(targetPath, { recursive: true });
|
|
3571
|
+
await copyModuleFromSource(sourcePath, targetPath);
|
|
3572
|
+
} else {
|
|
3573
|
+
await fs5.copyFile(sourcePath, targetPath);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
async function getModuleFiles(moduleName, moduleDir) {
|
|
3578
|
+
const files = {};
|
|
3579
|
+
const entries = await fs5.readdir(moduleDir);
|
|
3580
|
+
for (const entry of entries) {
|
|
3581
|
+
const filePath = path6.join(moduleDir, entry);
|
|
3582
|
+
const stat2 = await fs5.stat(filePath);
|
|
3583
|
+
if (stat2.isFile() && entry.endsWith(".ts")) {
|
|
3584
|
+
const content = await fs5.readFile(filePath, "utf-8");
|
|
3585
|
+
files[entry] = content;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
return files;
|
|
3589
|
+
}
|
|
3590
|
+
async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
3591
|
+
const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
|
|
3592
|
+
console.log(chalk4.cyan(`
|
|
3593
|
+
\u{1F4CA} Changes in module "${moduleName}":
|
|
3594
|
+
`));
|
|
3595
|
+
for (const file of modifiedFiles) {
|
|
3596
|
+
if (file.isModified) {
|
|
3597
|
+
console.log(chalk4.yellow(`
|
|
3598
|
+
\u{1F4C4} ${file.fileName}:`));
|
|
3599
|
+
const currentPath = path6.join(moduleDir, file.fileName);
|
|
3600
|
+
const currentContent = await fs5.readFile(currentPath, "utf-8");
|
|
3601
|
+
const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
|
|
3602
|
+
if (originalContent) {
|
|
3603
|
+
const diff = templateManager.generateDiff(originalContent, currentContent);
|
|
3604
|
+
console.log(diff);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
|
|
3610
|
+
const spinner = ora3("Analyzing files for merge...").start();
|
|
3611
|
+
const newFiles = {};
|
|
3612
|
+
const templateDir = path6.join(templateManager["templatesDir"], moduleName);
|
|
3613
|
+
try {
|
|
3614
|
+
const entries = await fs5.readdir(templateDir);
|
|
3615
|
+
for (const entry of entries) {
|
|
3616
|
+
const content = await fs5.readFile(path6.join(templateDir, entry), "utf-8");
|
|
3617
|
+
newFiles[entry] = content;
|
|
3618
|
+
}
|
|
3619
|
+
} catch {
|
|
3620
|
+
spinner.fail("Could not find template files");
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
|
|
3624
|
+
spinner.stop();
|
|
3625
|
+
const batchAction = await InteractivePrompt.askBatchAction();
|
|
3626
|
+
const stats = {
|
|
3627
|
+
merged: 0,
|
|
3628
|
+
kept: 0,
|
|
3629
|
+
overwritten: 0,
|
|
3630
|
+
conflicts: 0
|
|
3631
|
+
};
|
|
3632
|
+
for (const fileInfo of modifiedFiles) {
|
|
3633
|
+
const fileName = fileInfo.fileName;
|
|
3634
|
+
const filePath = path6.join(moduleDir, fileName);
|
|
3635
|
+
const newContent = newFiles[fileName];
|
|
3636
|
+
if (!newContent) {
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
let fileAction;
|
|
3640
|
+
if (batchAction === "merge-all") {
|
|
3641
|
+
fileAction = "merge";
|
|
3642
|
+
} else if (batchAction === "keep-all") {
|
|
3643
|
+
fileAction = "keep";
|
|
3644
|
+
} else if (batchAction === "overwrite-all") {
|
|
3645
|
+
fileAction = "overwrite";
|
|
3646
|
+
} else {
|
|
3647
|
+
const currentContent = await fs5.readFile(filePath, "utf-8");
|
|
3648
|
+
const yourLines = currentContent.split("\n").length;
|
|
3649
|
+
const newLines = newContent.split("\n").length;
|
|
3650
|
+
const choice = await InteractivePrompt.askFileAction(
|
|
3651
|
+
fileName,
|
|
3652
|
+
fileInfo.isModified,
|
|
3653
|
+
yourLines,
|
|
3654
|
+
newLines
|
|
3655
|
+
);
|
|
3656
|
+
fileAction = choice.action;
|
|
3657
|
+
if (fileAction === "diff") {
|
|
3658
|
+
const originalContent = await templateManager.getTemplate(moduleName, fileName);
|
|
3659
|
+
if (originalContent) {
|
|
3660
|
+
const diff = templateManager.generateDiff(originalContent, currentContent);
|
|
3661
|
+
const proceed = await InteractivePrompt.showDiffAndAsk(diff);
|
|
3662
|
+
fileAction = proceed ? "merge" : "keep";
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
if (fileAction === "keep" || fileAction === "skip") {
|
|
3667
|
+
stats.kept++;
|
|
3668
|
+
continue;
|
|
3669
|
+
}
|
|
3670
|
+
if (fileAction === "overwrite") {
|
|
3671
|
+
await fs5.writeFile(filePath, newContent, "utf-8");
|
|
3672
|
+
stats.overwritten++;
|
|
3673
|
+
continue;
|
|
3674
|
+
}
|
|
3675
|
+
if (fileAction === "merge") {
|
|
3676
|
+
const originalContent = await templateManager.getTemplate(moduleName, fileName);
|
|
3677
|
+
const currentContent = await fs5.readFile(filePath, "utf-8");
|
|
3678
|
+
if (originalContent) {
|
|
3679
|
+
const mergeResult = await templateManager.mergeFiles(
|
|
3680
|
+
originalContent,
|
|
3681
|
+
currentContent,
|
|
3682
|
+
newContent
|
|
3683
|
+
);
|
|
3684
|
+
await fs5.writeFile(filePath, mergeResult.merged, "utf-8");
|
|
3685
|
+
if (mergeResult.hasConflicts) {
|
|
3686
|
+
stats.conflicts++;
|
|
3687
|
+
InteractivePrompt.displayConflicts(mergeResult.conflicts);
|
|
3688
|
+
} else {
|
|
3689
|
+
stats.merged++;
|
|
3690
|
+
}
|
|
3691
|
+
} else {
|
|
3692
|
+
await fs5.writeFile(filePath, newContent, "utf-8");
|
|
3693
|
+
stats.overwritten++;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3828
3696
|
}
|
|
3697
|
+
const files = await getModuleFiles(moduleName, moduleDir);
|
|
3698
|
+
await templateManager.updateManifest(moduleName, files);
|
|
3699
|
+
InteractivePrompt.showMergeSummary(stats);
|
|
3829
3700
|
}
|
|
3830
3701
|
|
|
3831
3702
|
// src/cli/commands/db.ts
|
|
3832
3703
|
import { Command as Command4 } from "commander";
|
|
3833
3704
|
import { execSync as execSync2, spawn } from "child_process";
|
|
3834
|
-
import
|
|
3835
|
-
import
|
|
3705
|
+
import ora4 from "ora";
|
|
3706
|
+
import chalk5 from "chalk";
|
|
3836
3707
|
var dbCommand = new Command4("db").description("Database management commands");
|
|
3837
3708
|
dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
|
|
3838
|
-
const spinner =
|
|
3709
|
+
const spinner = ora4("Running migrations...").start();
|
|
3839
3710
|
try {
|
|
3840
3711
|
const cmd = options.name ? `npx prisma migrate dev --name ${options.name}` : "npx prisma migrate dev";
|
|
3841
3712
|
execSync2(cmd, { stdio: "inherit" });
|
|
@@ -3846,7 +3717,7 @@ dbCommand.command("migrate").description("Run database migrations").option("-n,
|
|
|
3846
3717
|
}
|
|
3847
3718
|
});
|
|
3848
3719
|
dbCommand.command("push").description("Push schema changes to database (no migration)").action(async () => {
|
|
3849
|
-
const spinner =
|
|
3720
|
+
const spinner = ora4("Pushing schema...").start();
|
|
3850
3721
|
try {
|
|
3851
3722
|
execSync2("npx prisma db push", { stdio: "inherit" });
|
|
3852
3723
|
spinner.succeed("Schema pushed successfully!");
|
|
@@ -3856,7 +3727,7 @@ dbCommand.command("push").description("Push schema changes to database (no migra
|
|
|
3856
3727
|
}
|
|
3857
3728
|
});
|
|
3858
3729
|
dbCommand.command("generate").description("Generate Prisma client").action(async () => {
|
|
3859
|
-
const spinner =
|
|
3730
|
+
const spinner = ora4("Generating Prisma client...").start();
|
|
3860
3731
|
try {
|
|
3861
3732
|
execSync2("npx prisma generate", { stdio: "inherit" });
|
|
3862
3733
|
spinner.succeed("Prisma client generated!");
|
|
@@ -3878,7 +3749,7 @@ dbCommand.command("studio").description("Open Prisma Studio").action(async () =>
|
|
|
3878
3749
|
});
|
|
3879
3750
|
});
|
|
3880
3751
|
dbCommand.command("seed").description("Run database seed").action(async () => {
|
|
3881
|
-
const spinner =
|
|
3752
|
+
const spinner = ora4("Seeding database...").start();
|
|
3882
3753
|
try {
|
|
3883
3754
|
execSync2("npx prisma db seed", { stdio: "inherit" });
|
|
3884
3755
|
spinner.succeed("Database seeded!");
|
|
@@ -3889,7 +3760,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
|
|
|
3889
3760
|
});
|
|
3890
3761
|
dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
|
|
3891
3762
|
if (!options.force) {
|
|
3892
|
-
console.log(
|
|
3763
|
+
console.log(chalk5.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
|
|
3893
3764
|
const readline = await import("readline");
|
|
3894
3765
|
const rl = readline.createInterface({
|
|
3895
3766
|
input: process.stdin,
|
|
@@ -3904,7 +3775,7 @@ dbCommand.command("reset").description("Reset database (drop all data and re-run
|
|
|
3904
3775
|
return;
|
|
3905
3776
|
}
|
|
3906
3777
|
}
|
|
3907
|
-
const spinner =
|
|
3778
|
+
const spinner = ora4("Resetting database...").start();
|
|
3908
3779
|
try {
|
|
3909
3780
|
execSync2("npx prisma migrate reset --force", { stdio: "inherit" });
|
|
3910
3781
|
spinner.succeed("Database reset completed!");
|
|
@@ -3916,30 +3787,17 @@ dbCommand.command("reset").description("Reset database (drop all data and re-run
|
|
|
3916
3787
|
dbCommand.command("status").description("Show migration status").action(async () => {
|
|
3917
3788
|
try {
|
|
3918
3789
|
execSync2("npx prisma migrate status", { stdio: "inherit" });
|
|
3919
|
-
} catch
|
|
3790
|
+
} catch {
|
|
3920
3791
|
error("Failed to get migration status");
|
|
3921
3792
|
}
|
|
3922
3793
|
});
|
|
3923
3794
|
|
|
3924
|
-
// src/cli/commands/docs.ts
|
|
3925
|
-
import { Command as Command5 } from "commander";
|
|
3926
|
-
var docsCommand = new Command5("docs").description("Generate Swagger/OpenAPI documentation").option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
|
|
3927
|
-
try {
|
|
3928
|
-
const outputPath = await generateDocs(options.output);
|
|
3929
|
-
success(`Documentation written to ${outputPath}`);
|
|
3930
|
-
} catch (err) {
|
|
3931
|
-
error(err instanceof Error ? err.message : String(err));
|
|
3932
|
-
process.exitCode = 1;
|
|
3933
|
-
}
|
|
3934
|
-
});
|
|
3935
|
-
|
|
3936
3795
|
// src/cli/index.ts
|
|
3937
|
-
var program = new
|
|
3796
|
+
var program = new Command5();
|
|
3938
3797
|
program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
|
|
3939
3798
|
program.addCommand(initCommand);
|
|
3940
3799
|
program.addCommand(generateCommand);
|
|
3941
3800
|
program.addCommand(addModuleCommand);
|
|
3942
3801
|
program.addCommand(dbCommand);
|
|
3943
|
-
program.addCommand(docsCommand);
|
|
3944
3802
|
program.parse();
|
|
3945
3803
|
//# sourceMappingURL=index.js.map
|