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