servcraft 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -4
- package/ROADMAP.md +327 -0
- package/dist/cli/index.cjs +3039 -222
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3058 -220
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/commands/add-module.ts +37 -36
- package/src/cli/commands/doctor.ts +8 -0
- package/src/cli/commands/generate.ts +43 -1
- package/src/cli/commands/init.ts +470 -75
- package/src/cli/commands/list.ts +274 -0
- package/src/cli/commands/remove.ts +102 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/utils/dry-run.ts +155 -0
- package/src/cli/utils/error-handler.ts +184 -0
- package/src/cli/utils/helpers.ts +13 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,21 +1,316 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
7
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
8
|
+
}) : x)(function(x) {
|
|
9
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
10
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
11
|
+
});
|
|
12
|
+
var __esm = (fn, res) => function __init() {
|
|
13
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
+
};
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (let key of __getOwnPropNames(from))
|
|
22
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
23
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
30
|
+
import path from "path";
|
|
31
|
+
import { fileURLToPath } from "url";
|
|
32
|
+
var init_esm_shims = __esm({
|
|
33
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
34
|
+
"use strict";
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// src/cli/utils/error-handler.ts
|
|
39
|
+
var error_handler_exports = {};
|
|
40
|
+
__export(error_handler_exports, {
|
|
41
|
+
ErrorTypes: () => ErrorTypes,
|
|
42
|
+
ServCraftError: () => ServCraftError,
|
|
43
|
+
displayError: () => displayError,
|
|
44
|
+
handleSystemError: () => handleSystemError,
|
|
45
|
+
validateProject: () => validateProject
|
|
46
|
+
});
|
|
47
|
+
import chalk4 from "chalk";
|
|
48
|
+
function displayError(error2) {
|
|
49
|
+
console.error("\n" + chalk4.red.bold("\u2717 Error: ") + chalk4.red(error2.message));
|
|
50
|
+
if (error2 instanceof ServCraftError) {
|
|
51
|
+
if (error2.suggestions.length > 0) {
|
|
52
|
+
console.log("\n" + chalk4.yellow.bold("\u{1F4A1} Suggestions:"));
|
|
53
|
+
error2.suggestions.forEach((suggestion) => {
|
|
54
|
+
console.log(chalk4.yellow(" \u2022 ") + suggestion);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (error2.docsLink) {
|
|
58
|
+
console.log("\n" + chalk4.blue.bold("\u{1F4DA} Documentation: ") + chalk4.blue.underline(error2.docsLink));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
function handleSystemError(err) {
|
|
64
|
+
switch (err.code) {
|
|
65
|
+
case "ENOENT":
|
|
66
|
+
return new ServCraftError(
|
|
67
|
+
`File or directory not found: ${err.path}`,
|
|
68
|
+
[`Check if the path exists`, `Create the directory first`]
|
|
69
|
+
);
|
|
70
|
+
case "EACCES":
|
|
71
|
+
case "EPERM":
|
|
72
|
+
return new ServCraftError(
|
|
73
|
+
`Permission denied: ${err.path}`,
|
|
74
|
+
[
|
|
75
|
+
`Check file permissions`,
|
|
76
|
+
`Try running with elevated privileges (not recommended)`,
|
|
77
|
+
`Change ownership of the directory`
|
|
78
|
+
]
|
|
79
|
+
);
|
|
80
|
+
case "EEXIST":
|
|
81
|
+
return new ServCraftError(
|
|
82
|
+
`File or directory already exists: ${err.path}`,
|
|
83
|
+
[`Use a different name`, `Remove the existing file first`, `Use ${chalk4.cyan("--force")} to overwrite`]
|
|
84
|
+
);
|
|
85
|
+
case "ENOTDIR":
|
|
86
|
+
return new ServCraftError(
|
|
87
|
+
`Not a directory: ${err.path}`,
|
|
88
|
+
[`Check the path`, `A file exists where a directory is expected`]
|
|
89
|
+
);
|
|
90
|
+
case "EISDIR":
|
|
91
|
+
return new ServCraftError(
|
|
92
|
+
`Is a directory: ${err.path}`,
|
|
93
|
+
[`Cannot perform this operation on a directory`, `Did you mean to target a file?`]
|
|
94
|
+
);
|
|
95
|
+
default:
|
|
96
|
+
return new ServCraftError(
|
|
97
|
+
err.message,
|
|
98
|
+
[`Check system error code: ${err.code}`, `Review the error details above`]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function validateProject() {
|
|
103
|
+
try {
|
|
104
|
+
const fs10 = __require("fs");
|
|
105
|
+
const path12 = __require("path");
|
|
106
|
+
if (!fs10.existsSync("package.json")) {
|
|
107
|
+
return ErrorTypes.NOT_IN_PROJECT();
|
|
108
|
+
}
|
|
109
|
+
const packageJson = JSON.parse(fs10.readFileSync("package.json", "utf-8"));
|
|
110
|
+
if (!packageJson.dependencies?.fastify) {
|
|
111
|
+
return new ServCraftError(
|
|
112
|
+
"This does not appear to be a ServCraft project",
|
|
113
|
+
[
|
|
114
|
+
`ServCraft projects require Fastify`,
|
|
115
|
+
`Run ${chalk4.cyan("servcraft init")} to create a new project`
|
|
116
|
+
]
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return new ServCraftError(
|
|
122
|
+
"Failed to validate project",
|
|
123
|
+
[`Ensure you are in the project root directory`, `Check if ${chalk4.yellow("package.json")} is valid`]
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var ServCraftError, ErrorTypes;
|
|
128
|
+
var init_error_handler = __esm({
|
|
129
|
+
"src/cli/utils/error-handler.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
init_esm_shims();
|
|
132
|
+
ServCraftError = class extends Error {
|
|
133
|
+
suggestions;
|
|
134
|
+
docsLink;
|
|
135
|
+
constructor(message, suggestions = [], docsLink) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = "ServCraftError";
|
|
138
|
+
this.suggestions = suggestions;
|
|
139
|
+
this.docsLink = docsLink;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
ErrorTypes = {
|
|
143
|
+
MODULE_NOT_FOUND: (moduleName) => new ServCraftError(
|
|
144
|
+
`Module "${moduleName}" not found`,
|
|
145
|
+
[
|
|
146
|
+
`Run ${chalk4.cyan("servcraft list")} to see available modules`,
|
|
147
|
+
`Check the spelling of the module name`,
|
|
148
|
+
`Visit ${chalk4.blue("https://github.com/Le-Sourcier/servcraft#modules")} for module list`
|
|
149
|
+
],
|
|
150
|
+
"https://github.com/Le-Sourcier/servcraft#add-pre-built-modules"
|
|
151
|
+
),
|
|
152
|
+
MODULE_ALREADY_EXISTS: (moduleName) => new ServCraftError(
|
|
153
|
+
`Module "${moduleName}" already exists`,
|
|
154
|
+
[
|
|
155
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --force")} to overwrite`,
|
|
156
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --update")} to update`,
|
|
157
|
+
`Use ${chalk4.cyan("servcraft add " + moduleName + " --skip-existing")} to skip`
|
|
158
|
+
]
|
|
159
|
+
),
|
|
160
|
+
NOT_IN_PROJECT: () => new ServCraftError(
|
|
161
|
+
"Not in a ServCraft project directory",
|
|
162
|
+
[
|
|
163
|
+
`Run ${chalk4.cyan("servcraft init")} to create a new project`,
|
|
164
|
+
`Navigate to your ServCraft project directory`,
|
|
165
|
+
`Check if ${chalk4.yellow("package.json")} exists`
|
|
166
|
+
],
|
|
167
|
+
"https://github.com/Le-Sourcier/servcraft#initialize-project"
|
|
168
|
+
),
|
|
169
|
+
FILE_ALREADY_EXISTS: (fileName) => new ServCraftError(
|
|
170
|
+
`File "${fileName}" already exists`,
|
|
171
|
+
[
|
|
172
|
+
`Use ${chalk4.cyan("--force")} flag to overwrite`,
|
|
173
|
+
`Choose a different name`,
|
|
174
|
+
`Delete the existing file first`
|
|
175
|
+
]
|
|
176
|
+
),
|
|
177
|
+
INVALID_DATABASE: (database) => new ServCraftError(
|
|
178
|
+
`Invalid database type: "${database}"`,
|
|
179
|
+
[
|
|
180
|
+
`Valid options: ${chalk4.cyan("postgresql, mysql, sqlite, mongodb, none")}`,
|
|
181
|
+
`Use ${chalk4.cyan("servcraft init --db postgresql")} for PostgreSQL`
|
|
182
|
+
]
|
|
183
|
+
),
|
|
184
|
+
INVALID_VALIDATOR: (validator) => new ServCraftError(
|
|
185
|
+
`Invalid validator type: "${validator}"`,
|
|
186
|
+
[`Valid options: ${chalk4.cyan("zod, joi, yup")}`, `Default is ${chalk4.cyan("zod")}`]
|
|
187
|
+
),
|
|
188
|
+
MISSING_DEPENDENCY: (dependency, command) => new ServCraftError(
|
|
189
|
+
`Missing dependency: "${dependency}"`,
|
|
190
|
+
[`Run ${chalk4.cyan(command)} to install`, `Check your ${chalk4.yellow("package.json")}`]
|
|
191
|
+
),
|
|
192
|
+
INVALID_FIELD_FORMAT: (field) => new ServCraftError(
|
|
193
|
+
`Invalid field format: "${field}"`,
|
|
194
|
+
[
|
|
195
|
+
`Expected format: ${chalk4.cyan("name:type")}`,
|
|
196
|
+
`Example: ${chalk4.cyan("name:string age:number isActive:boolean")}`,
|
|
197
|
+
`Supported types: string, number, boolean, date`
|
|
198
|
+
]
|
|
199
|
+
),
|
|
200
|
+
GIT_NOT_INITIALIZED: () => new ServCraftError(
|
|
201
|
+
"Git repository not initialized",
|
|
202
|
+
[
|
|
203
|
+
`Run ${chalk4.cyan("git init")} to initialize git`,
|
|
204
|
+
`This is required for some ServCraft features`
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
2
210
|
|
|
3
211
|
// src/cli/index.ts
|
|
4
|
-
|
|
212
|
+
init_esm_shims();
|
|
213
|
+
import { Command as Command9 } from "commander";
|
|
5
214
|
|
|
6
215
|
// src/cli/commands/init.ts
|
|
216
|
+
init_esm_shims();
|
|
7
217
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
218
|
+
import path4 from "path";
|
|
9
219
|
import fs2 from "fs/promises";
|
|
10
220
|
import ora from "ora";
|
|
11
221
|
import inquirer from "inquirer";
|
|
12
|
-
import
|
|
222
|
+
import chalk3 from "chalk";
|
|
13
223
|
import { execSync } from "child_process";
|
|
14
224
|
|
|
15
225
|
// src/cli/utils/helpers.ts
|
|
226
|
+
init_esm_shims();
|
|
16
227
|
import fs from "fs/promises";
|
|
17
|
-
import
|
|
228
|
+
import path3 from "path";
|
|
229
|
+
import chalk2 from "chalk";
|
|
230
|
+
|
|
231
|
+
// src/cli/utils/dry-run.ts
|
|
232
|
+
init_esm_shims();
|
|
18
233
|
import chalk from "chalk";
|
|
234
|
+
import path2 from "path";
|
|
235
|
+
var DryRunManager = class _DryRunManager {
|
|
236
|
+
static instance;
|
|
237
|
+
enabled = false;
|
|
238
|
+
operations = [];
|
|
239
|
+
constructor() {
|
|
240
|
+
}
|
|
241
|
+
static getInstance() {
|
|
242
|
+
if (!_DryRunManager.instance) {
|
|
243
|
+
_DryRunManager.instance = new _DryRunManager();
|
|
244
|
+
}
|
|
245
|
+
return _DryRunManager.instance;
|
|
246
|
+
}
|
|
247
|
+
enable() {
|
|
248
|
+
this.enabled = true;
|
|
249
|
+
this.operations = [];
|
|
250
|
+
}
|
|
251
|
+
disable() {
|
|
252
|
+
this.enabled = false;
|
|
253
|
+
this.operations = [];
|
|
254
|
+
}
|
|
255
|
+
isEnabled() {
|
|
256
|
+
return this.enabled;
|
|
257
|
+
}
|
|
258
|
+
addOperation(operation) {
|
|
259
|
+
if (this.enabled) {
|
|
260
|
+
this.operations.push(operation);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
getOperations() {
|
|
264
|
+
return [...this.operations];
|
|
265
|
+
}
|
|
266
|
+
printSummary() {
|
|
267
|
+
if (!this.enabled || this.operations.length === 0) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.log(chalk.bold.yellow("\n\u{1F4CB} Dry Run - Preview of changes:\n"));
|
|
271
|
+
console.log(chalk.gray("No files will be written. Remove --dry-run to apply changes.\n"));
|
|
272
|
+
const createOps = this.operations.filter((op) => op.type === "create");
|
|
273
|
+
const modifyOps = this.operations.filter((op) => op.type === "modify");
|
|
274
|
+
const deleteOps = this.operations.filter((op) => op.type === "delete");
|
|
275
|
+
if (createOps.length > 0) {
|
|
276
|
+
console.log(chalk.green.bold(`
|
|
277
|
+
\u2713 Files to be created (${createOps.length}):`));
|
|
278
|
+
createOps.forEach((op) => {
|
|
279
|
+
const size = op.content ? `${op.content.length} bytes` : "unknown size";
|
|
280
|
+
console.log(` ${chalk.green("+")} ${chalk.cyan(op.path)} ${chalk.gray(`(${size})`)}`);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (modifyOps.length > 0) {
|
|
284
|
+
console.log(chalk.yellow.bold(`
|
|
285
|
+
~ Files to be modified (${modifyOps.length}):`));
|
|
286
|
+
modifyOps.forEach((op) => {
|
|
287
|
+
console.log(` ${chalk.yellow("~")} ${chalk.cyan(op.path)}`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (deleteOps.length > 0) {
|
|
291
|
+
console.log(chalk.red.bold(`
|
|
292
|
+
- Files to be deleted (${deleteOps.length}):`));
|
|
293
|
+
deleteOps.forEach((op) => {
|
|
294
|
+
console.log(` ${chalk.red("-")} ${chalk.cyan(op.path)}`);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
console.log(chalk.gray("\n" + "\u2500".repeat(60)));
|
|
298
|
+
console.log(
|
|
299
|
+
chalk.bold(` Total operations: ${this.operations.length}`) + chalk.gray(
|
|
300
|
+
` (${createOps.length} create, ${modifyOps.length} modify, ${deleteOps.length} delete)`
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
console.log(chalk.gray("\u2500".repeat(60)));
|
|
304
|
+
console.log(chalk.yellow("\n\u26A0 This was a dry run. No files were created or modified."));
|
|
305
|
+
console.log(chalk.gray(" Remove --dry-run to apply these changes.\n"));
|
|
306
|
+
}
|
|
307
|
+
// Helper to format file path relative to cwd
|
|
308
|
+
relativePath(filePath) {
|
|
309
|
+
return path2.relative(process.cwd(), filePath);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/cli/utils/helpers.ts
|
|
19
314
|
function toPascalCase(str) {
|
|
20
315
|
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
21
316
|
}
|
|
@@ -47,39 +342,54 @@ async function ensureDir(dirPath) {
|
|
|
47
342
|
await fs.mkdir(dirPath, { recursive: true });
|
|
48
343
|
}
|
|
49
344
|
async function writeFile(filePath, content) {
|
|
50
|
-
|
|
345
|
+
const dryRun = DryRunManager.getInstance();
|
|
346
|
+
if (dryRun.isEnabled()) {
|
|
347
|
+
dryRun.addOperation({
|
|
348
|
+
type: "create",
|
|
349
|
+
path: dryRun.relativePath(filePath),
|
|
350
|
+
content,
|
|
351
|
+
size: content.length
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
await ensureDir(path3.dirname(filePath));
|
|
51
356
|
await fs.writeFile(filePath, content, "utf-8");
|
|
52
357
|
}
|
|
53
358
|
function success(message) {
|
|
54
|
-
console.log(
|
|
359
|
+
console.log(chalk2.green("\u2713"), message);
|
|
55
360
|
}
|
|
56
361
|
function error(message) {
|
|
57
|
-
console.error(
|
|
362
|
+
console.error(chalk2.red("\u2717"), message);
|
|
58
363
|
}
|
|
59
364
|
function warn(message) {
|
|
60
|
-
console.log(
|
|
365
|
+
console.log(chalk2.yellow("\u26A0"), message);
|
|
61
366
|
}
|
|
62
367
|
function info(message) {
|
|
63
|
-
console.log(
|
|
368
|
+
console.log(chalk2.blue("\u2139"), message);
|
|
64
369
|
}
|
|
65
370
|
function getProjectRoot() {
|
|
66
371
|
return process.cwd();
|
|
67
372
|
}
|
|
68
373
|
function getSourceDir() {
|
|
69
|
-
return
|
|
374
|
+
return path3.join(getProjectRoot(), "src");
|
|
70
375
|
}
|
|
71
376
|
function getModulesDir() {
|
|
72
|
-
return
|
|
377
|
+
return path3.join(getSourceDir(), "modules");
|
|
73
378
|
}
|
|
74
379
|
|
|
75
380
|
// src/cli/commands/init.ts
|
|
76
|
-
var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
|
|
381
|
+
var initCommand = new Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--esm", "Use ES Modules (import/export) - default").option("--cjs, --commonjs", "Use CommonJS (require/module.exports)").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").option("--dry-run", "Preview changes without writing files").action(
|
|
77
382
|
async (name, cmdOptions) => {
|
|
383
|
+
const dryRun = DryRunManager.getInstance();
|
|
384
|
+
if (cmdOptions?.dryRun) {
|
|
385
|
+
dryRun.enable();
|
|
386
|
+
console.log(chalk3.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
387
|
+
}
|
|
78
388
|
console.log(
|
|
79
|
-
|
|
389
|
+
chalk3.blue(`
|
|
80
390
|
\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
|
|
81
391
|
\u2551 \u2551
|
|
82
|
-
\u2551 ${
|
|
392
|
+
\u2551 ${chalk3.bold("\u{1F680} Servcraft Project Generator")} \u2551
|
|
83
393
|
\u2551 \u2551
|
|
84
394
|
\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
|
|
85
395
|
`)
|
|
@@ -90,6 +400,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
90
400
|
options = {
|
|
91
401
|
name: name || "my-servcraft-app",
|
|
92
402
|
language: cmdOptions.javascript ? "javascript" : "typescript",
|
|
403
|
+
moduleSystem: cmdOptions.commonjs ? "commonjs" : "esm",
|
|
93
404
|
database: db,
|
|
94
405
|
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma",
|
|
95
406
|
validator: "zod",
|
|
@@ -119,6 +430,16 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
119
430
|
],
|
|
120
431
|
default: "typescript"
|
|
121
432
|
},
|
|
433
|
+
{
|
|
434
|
+
type: "list",
|
|
435
|
+
name: "moduleSystem",
|
|
436
|
+
message: "Select module system:",
|
|
437
|
+
choices: [
|
|
438
|
+
{ name: "ESM (import/export) - Recommended", value: "esm" },
|
|
439
|
+
{ name: "CommonJS (require/module.exports)", value: "commonjs" }
|
|
440
|
+
],
|
|
441
|
+
default: "esm"
|
|
442
|
+
},
|
|
122
443
|
{
|
|
123
444
|
type: "list",
|
|
124
445
|
name: "database",
|
|
@@ -163,7 +484,7 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
163
484
|
orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma"
|
|
164
485
|
};
|
|
165
486
|
}
|
|
166
|
-
const projectDir =
|
|
487
|
+
const projectDir = path4.resolve(process.cwd(), options.name);
|
|
167
488
|
const spinner = ora("Creating project...").start();
|
|
168
489
|
try {
|
|
169
490
|
try {
|
|
@@ -177,24 +498,24 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
177
498
|
spinner.text = "Generating project files...";
|
|
178
499
|
const packageJson = generatePackageJson(options);
|
|
179
500
|
await writeFile(
|
|
180
|
-
|
|
501
|
+
path4.join(projectDir, "package.json"),
|
|
181
502
|
JSON.stringify(packageJson, null, 2)
|
|
182
503
|
);
|
|
183
504
|
if (options.language === "typescript") {
|
|
184
|
-
await writeFile(
|
|
185
|
-
await writeFile(
|
|
505
|
+
await writeFile(path4.join(projectDir, "tsconfig.json"), generateTsConfig(options));
|
|
506
|
+
await writeFile(path4.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
|
|
186
507
|
} else {
|
|
187
|
-
await writeFile(
|
|
508
|
+
await writeFile(path4.join(projectDir, "jsconfig.json"), generateJsConfig(options));
|
|
188
509
|
}
|
|
189
|
-
await writeFile(
|
|
190
|
-
await writeFile(
|
|
191
|
-
await writeFile(
|
|
192
|
-
await writeFile(
|
|
510
|
+
await writeFile(path4.join(projectDir, ".env.example"), generateEnvExample(options));
|
|
511
|
+
await writeFile(path4.join(projectDir, ".env"), generateEnvExample(options));
|
|
512
|
+
await writeFile(path4.join(projectDir, ".gitignore"), generateGitignore());
|
|
513
|
+
await writeFile(path4.join(projectDir, "Dockerfile"), generateDockerfile(options));
|
|
193
514
|
await writeFile(
|
|
194
|
-
|
|
515
|
+
path4.join(projectDir, "docker-compose.yml"),
|
|
195
516
|
generateDockerCompose(options)
|
|
196
517
|
);
|
|
197
|
-
const ext = options.language === "typescript" ? "ts" : "js";
|
|
518
|
+
const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
|
|
198
519
|
const dirs = [
|
|
199
520
|
"src/core",
|
|
200
521
|
"src/config",
|
|
@@ -212,43 +533,63 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
212
533
|
dirs.push("src/database/models");
|
|
213
534
|
}
|
|
214
535
|
for (const dir of dirs) {
|
|
215
|
-
await ensureDir(
|
|
536
|
+
await ensureDir(path4.join(projectDir, dir));
|
|
216
537
|
}
|
|
217
|
-
await writeFile(
|
|
538
|
+
await writeFile(path4.join(projectDir, `src/index.${ext}`), generateEntryFile(options));
|
|
218
539
|
await writeFile(
|
|
219
|
-
|
|
540
|
+
path4.join(projectDir, `src/core/server.${ext}`),
|
|
220
541
|
generateServerFile(options)
|
|
221
542
|
);
|
|
222
543
|
await writeFile(
|
|
223
|
-
|
|
544
|
+
path4.join(projectDir, `src/core/logger.${ext}`),
|
|
224
545
|
generateLoggerFile(options)
|
|
225
546
|
);
|
|
547
|
+
await writeFile(
|
|
548
|
+
path4.join(projectDir, `src/config/index.${ext}`),
|
|
549
|
+
generateConfigFile(options)
|
|
550
|
+
);
|
|
551
|
+
await writeFile(
|
|
552
|
+
path4.join(projectDir, `src/middleware/index.${ext}`),
|
|
553
|
+
generateMiddlewareFile(options)
|
|
554
|
+
);
|
|
555
|
+
await writeFile(
|
|
556
|
+
path4.join(projectDir, `src/utils/index.${ext}`),
|
|
557
|
+
generateUtilsFile(options)
|
|
558
|
+
);
|
|
559
|
+
await writeFile(
|
|
560
|
+
path4.join(projectDir, `src/types/index.${ext}`),
|
|
561
|
+
generateTypesFile(options)
|
|
562
|
+
);
|
|
226
563
|
if (options.orm === "prisma") {
|
|
227
564
|
await writeFile(
|
|
228
|
-
|
|
565
|
+
path4.join(projectDir, "prisma/schema.prisma"),
|
|
229
566
|
generatePrismaSchema(options)
|
|
230
567
|
);
|
|
231
568
|
} else if (options.orm === "mongoose") {
|
|
232
569
|
await writeFile(
|
|
233
|
-
|
|
570
|
+
path4.join(projectDir, `src/database/connection.${ext}`),
|
|
234
571
|
generateMongooseConnection(options)
|
|
235
572
|
);
|
|
236
573
|
await writeFile(
|
|
237
|
-
|
|
574
|
+
path4.join(projectDir, `src/database/models/user.model.${ext}`),
|
|
238
575
|
generateMongooseUserModel(options)
|
|
239
576
|
);
|
|
240
577
|
}
|
|
241
578
|
spinner.succeed("Project files generated!");
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
579
|
+
if (!cmdOptions?.dryRun) {
|
|
580
|
+
const installSpinner = ora("Installing dependencies...").start();
|
|
581
|
+
try {
|
|
582
|
+
execSync("npm install", { cwd: projectDir, stdio: "pipe" });
|
|
583
|
+
installSpinner.succeed("Dependencies installed!");
|
|
584
|
+
} catch {
|
|
585
|
+
installSpinner.warn("Failed to install dependencies automatically");
|
|
586
|
+
warn(' Run "npm install" manually in the project directory');
|
|
587
|
+
}
|
|
249
588
|
}
|
|
250
|
-
|
|
251
|
-
|
|
589
|
+
if (!cmdOptions?.dryRun) {
|
|
590
|
+
console.log("\n" + chalk3.green("\u2728 Project created successfully!"));
|
|
591
|
+
}
|
|
592
|
+
console.log("\n" + chalk3.bold("\u{1F4C1} Project structure:"));
|
|
252
593
|
console.log(`
|
|
253
594
|
${options.name}/
|
|
254
595
|
\u251C\u2500\u2500 src/
|
|
@@ -263,19 +604,22 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
263
604
|
\u251C\u2500\u2500 docker-compose.yml
|
|
264
605
|
\u2514\u2500\u2500 package.json
|
|
265
606
|
`);
|
|
266
|
-
console.log(
|
|
607
|
+
console.log(chalk3.bold("\u{1F680} Get started:"));
|
|
267
608
|
console.log(`
|
|
268
|
-
${
|
|
269
|
-
${options.database !== "none" ?
|
|
270
|
-
${
|
|
609
|
+
${chalk3.cyan(`cd ${options.name}`)}
|
|
610
|
+
${options.database !== "none" ? chalk3.cyan("npm run db:push # Setup database") : ""}
|
|
611
|
+
${chalk3.cyan("npm run dev # Start development server")}
|
|
271
612
|
`);
|
|
272
|
-
console.log(
|
|
613
|
+
console.log(chalk3.bold("\u{1F4DA} Available commands:"));
|
|
273
614
|
console.log(`
|
|
274
|
-
${
|
|
275
|
-
${
|
|
276
|
-
${
|
|
277
|
-
${
|
|
615
|
+
${chalk3.yellow("servcraft generate module <name>")} Generate a new module
|
|
616
|
+
${chalk3.yellow("servcraft generate controller <name>")} Generate a controller
|
|
617
|
+
${chalk3.yellow("servcraft generate service <name>")} Generate a service
|
|
618
|
+
${chalk3.yellow("servcraft add auth")} Add authentication module
|
|
278
619
|
`);
|
|
620
|
+
if (cmdOptions?.dryRun) {
|
|
621
|
+
dryRun.printSummary();
|
|
622
|
+
}
|
|
279
623
|
} catch (err) {
|
|
280
624
|
spinner.fail("Failed to create project");
|
|
281
625
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -284,18 +628,35 @@ var initCommand = new Command("init").alias("new").description("Initialize a new
|
|
|
284
628
|
);
|
|
285
629
|
function generatePackageJson(options) {
|
|
286
630
|
const isTS = options.language === "typescript";
|
|
631
|
+
const isESM = options.moduleSystem === "esm";
|
|
632
|
+
let devCommand;
|
|
633
|
+
if (isTS) {
|
|
634
|
+
devCommand = "tsx watch src/index.ts";
|
|
635
|
+
} else if (isESM) {
|
|
636
|
+
devCommand = "node --watch src/index.js";
|
|
637
|
+
} else {
|
|
638
|
+
devCommand = "node --watch src/index.cjs";
|
|
639
|
+
}
|
|
640
|
+
let startCommand;
|
|
641
|
+
if (isTS) {
|
|
642
|
+
startCommand = isESM ? "node dist/index.js" : "node dist/index.cjs";
|
|
643
|
+
} else if (isESM) {
|
|
644
|
+
startCommand = "node src/index.js";
|
|
645
|
+
} else {
|
|
646
|
+
startCommand = "node src/index.cjs";
|
|
647
|
+
}
|
|
287
648
|
const pkg = {
|
|
288
649
|
name: options.name,
|
|
289
650
|
version: "0.1.0",
|
|
290
651
|
description: "A Servcraft application",
|
|
291
|
-
main: isTS ? "dist/index.js" : "src/index.js",
|
|
292
|
-
type: "module",
|
|
652
|
+
main: isTS ? isESM ? "dist/index.js" : "dist/index.cjs" : isESM ? "src/index.js" : "src/index.cjs",
|
|
653
|
+
...isESM && { type: "module" },
|
|
293
654
|
scripts: {
|
|
294
|
-
dev:
|
|
655
|
+
dev: devCommand,
|
|
295
656
|
build: isTS ? "tsup" : 'echo "No build needed for JS"',
|
|
296
|
-
start:
|
|
657
|
+
start: startCommand,
|
|
297
658
|
test: "vitest",
|
|
298
|
-
lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js"
|
|
659
|
+
lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js,.cjs"
|
|
299
660
|
},
|
|
300
661
|
dependencies: {
|
|
301
662
|
fastify: "^4.28.1",
|
|
@@ -357,13 +718,14 @@ function generatePackageJson(options) {
|
|
|
357
718
|
}
|
|
358
719
|
return pkg;
|
|
359
720
|
}
|
|
360
|
-
function generateTsConfig() {
|
|
721
|
+
function generateTsConfig(options) {
|
|
722
|
+
const isESM = options.moduleSystem === "esm";
|
|
361
723
|
return JSON.stringify(
|
|
362
724
|
{
|
|
363
725
|
compilerOptions: {
|
|
364
726
|
target: "ES2022",
|
|
365
|
-
module: "NodeNext",
|
|
366
|
-
moduleResolution: "NodeNext",
|
|
727
|
+
module: isESM ? "NodeNext" : "CommonJS",
|
|
728
|
+
moduleResolution: isESM ? "NodeNext" : "Node",
|
|
367
729
|
lib: ["ES2022"],
|
|
368
730
|
outDir: "./dist",
|
|
369
731
|
rootDir: "./src",
|
|
@@ -382,12 +744,13 @@ function generateTsConfig() {
|
|
|
382
744
|
2
|
|
383
745
|
);
|
|
384
746
|
}
|
|
385
|
-
function generateJsConfig() {
|
|
747
|
+
function generateJsConfig(options) {
|
|
748
|
+
const isESM = options.moduleSystem === "esm";
|
|
386
749
|
return JSON.stringify(
|
|
387
750
|
{
|
|
388
751
|
compilerOptions: {
|
|
389
|
-
module: "NodeNext",
|
|
390
|
-
moduleResolution: "NodeNext",
|
|
752
|
+
module: isESM ? "NodeNext" : "CommonJS",
|
|
753
|
+
moduleResolution: isESM ? "NodeNext" : "Node",
|
|
391
754
|
target: "ES2022",
|
|
392
755
|
checkJs: true
|
|
393
756
|
},
|
|
@@ -398,12 +761,13 @@ function generateJsConfig() {
|
|
|
398
761
|
2
|
|
399
762
|
);
|
|
400
763
|
}
|
|
401
|
-
function generateTsupConfig() {
|
|
764
|
+
function generateTsupConfig(options) {
|
|
765
|
+
const isESM = options.moduleSystem === "esm";
|
|
402
766
|
return `import { defineConfig } from 'tsup';
|
|
403
767
|
|
|
404
768
|
export default defineConfig({
|
|
405
769
|
entry: ['src/index.ts'],
|
|
406
|
-
format: ['esm'],
|
|
770
|
+
format: ['${isESM ? "esm" : "cjs"}'],
|
|
407
771
|
dts: true,
|
|
408
772
|
clean: true,
|
|
409
773
|
sourcemap: true,
|
|
@@ -412,7 +776,7 @@ export default defineConfig({
|
|
|
412
776
|
`;
|
|
413
777
|
}
|
|
414
778
|
function generateEnvExample(options) {
|
|
415
|
-
let
|
|
779
|
+
let env2 = `# Server
|
|
416
780
|
NODE_ENV=development
|
|
417
781
|
PORT=3000
|
|
418
782
|
HOST=0.0.0.0
|
|
@@ -430,31 +794,31 @@ RATE_LIMIT_MAX=100
|
|
|
430
794
|
LOG_LEVEL=info
|
|
431
795
|
`;
|
|
432
796
|
if (options.database === "postgresql") {
|
|
433
|
-
|
|
797
|
+
env2 += `
|
|
434
798
|
# Database (PostgreSQL)
|
|
435
799
|
DATABASE_PROVIDER=postgresql
|
|
436
800
|
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
|
|
437
801
|
`;
|
|
438
802
|
} else if (options.database === "mysql") {
|
|
439
|
-
|
|
803
|
+
env2 += `
|
|
440
804
|
# Database (MySQL)
|
|
441
805
|
DATABASE_PROVIDER=mysql
|
|
442
806
|
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
|
|
443
807
|
`;
|
|
444
808
|
} else if (options.database === "sqlite") {
|
|
445
|
-
|
|
809
|
+
env2 += `
|
|
446
810
|
# Database (SQLite)
|
|
447
811
|
DATABASE_PROVIDER=sqlite
|
|
448
812
|
DATABASE_URL="file:./dev.db"
|
|
449
813
|
`;
|
|
450
814
|
} else if (options.database === "mongodb") {
|
|
451
|
-
|
|
815
|
+
env2 += `
|
|
452
816
|
# Database (MongoDB)
|
|
453
817
|
MONGODB_URI="mongodb://localhost:27017/mydb"
|
|
454
818
|
`;
|
|
455
819
|
}
|
|
456
820
|
if (options.features.includes("email")) {
|
|
457
|
-
|
|
821
|
+
env2 += `
|
|
458
822
|
# Email
|
|
459
823
|
SMTP_HOST=smtp.example.com
|
|
460
824
|
SMTP_PORT=587
|
|
@@ -464,12 +828,12 @@ SMTP_FROM="App <noreply@example.com>"
|
|
|
464
828
|
`;
|
|
465
829
|
}
|
|
466
830
|
if (options.features.includes("redis")) {
|
|
467
|
-
|
|
831
|
+
env2 += `
|
|
468
832
|
# Redis
|
|
469
833
|
REDIS_URL=redis://localhost:6379
|
|
470
834
|
`;
|
|
471
835
|
}
|
|
472
|
-
return
|
|
836
|
+
return env2;
|
|
473
837
|
}
|
|
474
838
|
function generateGitignore() {
|
|
475
839
|
return `node_modules/
|
|
@@ -598,7 +962,12 @@ model User {
|
|
|
598
962
|
}
|
|
599
963
|
function generateEntryFile(options) {
|
|
600
964
|
const isTS = options.language === "typescript";
|
|
601
|
-
|
|
965
|
+
const isESM = options.moduleSystem === "esm";
|
|
966
|
+
const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
|
|
967
|
+
if (isESM || isTS) {
|
|
968
|
+
return `import 'dotenv/config';
|
|
969
|
+
import { createServer } from './core/server.${fileExt}';
|
|
970
|
+
import { logger } from './core/logger.${fileExt}';
|
|
602
971
|
|
|
603
972
|
async function main()${isTS ? ": Promise<void>" : ""} {
|
|
604
973
|
const server = createServer();
|
|
@@ -613,16 +982,31 @@ async function main()${isTS ? ": Promise<void>" : ""} {
|
|
|
613
982
|
|
|
614
983
|
main();
|
|
615
984
|
`;
|
|
985
|
+
} else {
|
|
986
|
+
return `require('dotenv').config();
|
|
987
|
+
const { createServer } = require('./core/server.cjs');
|
|
988
|
+
const { logger } = require('./core/logger.cjs');
|
|
989
|
+
|
|
990
|
+
async function main() {
|
|
991
|
+
const server = createServer();
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
await server.start();
|
|
995
|
+
} catch (error) {
|
|
996
|
+
logger.error({ err: error }, 'Failed to start server');
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
main();
|
|
1002
|
+
`;
|
|
1003
|
+
}
|
|
616
1004
|
}
|
|
617
1005
|
function generateServerFile(options) {
|
|
618
1006
|
const isTS = options.language === "typescript";
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const { logger } = require('./logger.js');`}
|
|
623
|
-
|
|
624
|
-
${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "function createServer()"} {
|
|
625
|
-
const app = Fastify({ logger });
|
|
1007
|
+
const isESM = options.moduleSystem === "esm";
|
|
1008
|
+
const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
|
|
1009
|
+
const serverBody = ` const app = Fastify({ logger });
|
|
626
1010
|
|
|
627
1011
|
// Health check
|
|
628
1012
|
app.get('/health', async () => ({
|
|
@@ -649,33 +1033,58 @@ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: ()
|
|
|
649
1033
|
logger.info(\`Server listening on \${host}:\${port}\`);
|
|
650
1034
|
},
|
|
651
1035
|
};
|
|
652
|
-
}
|
|
1036
|
+
}`;
|
|
1037
|
+
if (isESM || isTS) {
|
|
1038
|
+
return `import Fastify from 'fastify';
|
|
1039
|
+
${isTS ? "import type { FastifyInstance } from 'fastify';" : ""}
|
|
1040
|
+
import { logger } from './logger.${fileExt}';
|
|
1041
|
+
|
|
1042
|
+
${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "export function createServer()"} {
|
|
1043
|
+
${serverBody}
|
|
1044
|
+
`;
|
|
1045
|
+
} else {
|
|
1046
|
+
return `const Fastify = require('fastify');
|
|
1047
|
+
const { logger } = require('./logger.cjs');
|
|
1048
|
+
|
|
1049
|
+
function createServer() {
|
|
1050
|
+
${serverBody}
|
|
653
1051
|
|
|
654
|
-
|
|
1052
|
+
module.exports = { createServer };
|
|
655
1053
|
`;
|
|
1054
|
+
}
|
|
656
1055
|
}
|
|
657
1056
|
function generateLoggerFile(options) {
|
|
658
1057
|
const isTS = options.language === "typescript";
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
${isTS ? "export const logger: Logger" : "const logger"} = pino({
|
|
1058
|
+
const isESM = options.moduleSystem === "esm";
|
|
1059
|
+
const loggerBody = `pino({
|
|
662
1060
|
level: process.env.LOG_LEVEL || 'info',
|
|
663
1061
|
transport: process.env.NODE_ENV !== 'production' ? {
|
|
664
1062
|
target: 'pino-pretty',
|
|
665
1063
|
options: { colorize: true },
|
|
666
1064
|
} : undefined,
|
|
667
|
-
})
|
|
1065
|
+
})`;
|
|
1066
|
+
if (isESM || isTS) {
|
|
1067
|
+
return `import pino from 'pino';
|
|
1068
|
+
${isTS ? "import type { Logger } from 'pino';" : ""}
|
|
1069
|
+
|
|
1070
|
+
export const logger${isTS ? ": Logger" : ""} = ${loggerBody};
|
|
1071
|
+
`;
|
|
1072
|
+
} else {
|
|
1073
|
+
return `const pino = require('pino');
|
|
1074
|
+
|
|
1075
|
+
const logger = ${loggerBody};
|
|
668
1076
|
|
|
669
|
-
|
|
1077
|
+
module.exports = { logger };
|
|
670
1078
|
`;
|
|
1079
|
+
}
|
|
671
1080
|
}
|
|
672
1081
|
function generateMongooseConnection(options) {
|
|
673
1082
|
const isTS = options.language === "typescript";
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
|
|
1083
|
+
const isESM = options.moduleSystem === "esm";
|
|
1084
|
+
const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
|
|
1085
|
+
const connectionBody = `const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
|
|
677
1086
|
|
|
678
|
-
${isTS ? "
|
|
1087
|
+
async function connectDatabase()${isTS ? ": Promise<typeof mongoose>" : ""} {
|
|
679
1088
|
try {
|
|
680
1089
|
const conn = await mongoose.connect(MONGODB_URI);
|
|
681
1090
|
logger.info(\`MongoDB connected: \${conn.connection.host}\`);
|
|
@@ -686,35 +1095,36 @@ ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "
|
|
|
686
1095
|
}
|
|
687
1096
|
}
|
|
688
1097
|
|
|
689
|
-
${isTS ? "
|
|
1098
|
+
async function disconnectDatabase()${isTS ? ": Promise<void>" : ""} {
|
|
690
1099
|
try {
|
|
691
1100
|
await mongoose.disconnect();
|
|
692
1101
|
logger.info('MongoDB disconnected');
|
|
693
1102
|
} catch (error) {
|
|
694
1103
|
logger.error({ err: error }, 'MongoDB disconnect failed');
|
|
695
1104
|
}
|
|
696
|
-
}
|
|
1105
|
+
}`;
|
|
1106
|
+
if (isESM || isTS) {
|
|
1107
|
+
return `import mongoose from 'mongoose';
|
|
1108
|
+
import { logger } from '../core/logger.${fileExt}';
|
|
1109
|
+
|
|
1110
|
+
${connectionBody}
|
|
1111
|
+
|
|
1112
|
+
export { connectDatabase, disconnectDatabase, mongoose };
|
|
1113
|
+
`;
|
|
1114
|
+
} else {
|
|
1115
|
+
return `const mongoose = require('mongoose');
|
|
1116
|
+
const { logger } = require('../core/logger.cjs');
|
|
697
1117
|
|
|
698
|
-
${
|
|
1118
|
+
${connectionBody}
|
|
1119
|
+
|
|
1120
|
+
module.exports = { connectDatabase, disconnectDatabase, mongoose };
|
|
699
1121
|
`;
|
|
1122
|
+
}
|
|
700
1123
|
}
|
|
701
1124
|
function generateMongooseUserModel(options) {
|
|
702
1125
|
const isTS = options.language === "typescript";
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
${isTS ? `export interface IUser extends Document {
|
|
706
|
-
email: string;
|
|
707
|
-
password: string;
|
|
708
|
-
name?: string;
|
|
709
|
-
role: 'user' | 'admin';
|
|
710
|
-
status: 'active' | 'inactive' | 'suspended';
|
|
711
|
-
emailVerified: boolean;
|
|
712
|
-
createdAt: Date;
|
|
713
|
-
updatedAt: Date;
|
|
714
|
-
comparePassword(candidatePassword: string): Promise<boolean>;
|
|
715
|
-
}` : ""}
|
|
716
|
-
|
|
717
|
-
const userSchema = new Schema${isTS ? "<IUser>" : ""}({
|
|
1126
|
+
const isESM = options.moduleSystem === "esm";
|
|
1127
|
+
const schemaBody = `const userSchema = new Schema${isTS ? "<IUser>" : ""}({
|
|
718
1128
|
email: {
|
|
719
1129
|
type: String,
|
|
720
1130
|
required: true,
|
|
@@ -765,19 +1175,250 @@ userSchema.pre('save', async function(next) {
|
|
|
765
1175
|
// Compare password method
|
|
766
1176
|
userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ": string" : ""})${isTS ? ": Promise<boolean>" : ""} {
|
|
767
1177
|
return bcrypt.compare(candidatePassword, this.password);
|
|
768
|
-
}
|
|
1178
|
+
};`;
|
|
1179
|
+
const tsInterface = isTS ? `
|
|
1180
|
+
export interface IUser extends Document {
|
|
1181
|
+
email: string;
|
|
1182
|
+
password: string;
|
|
1183
|
+
name?: string;
|
|
1184
|
+
role: 'user' | 'admin';
|
|
1185
|
+
status: 'active' | 'inactive' | 'suspended';
|
|
1186
|
+
emailVerified: boolean;
|
|
1187
|
+
createdAt: Date;
|
|
1188
|
+
updatedAt: Date;
|
|
1189
|
+
comparePassword(candidatePassword: string): Promise<boolean>;
|
|
1190
|
+
}
|
|
1191
|
+
` : "";
|
|
1192
|
+
if (isESM || isTS) {
|
|
1193
|
+
return `import mongoose${isTS ? ", { Schema, Document }" : ""} from 'mongoose';
|
|
1194
|
+
import bcrypt from 'bcryptjs';
|
|
1195
|
+
${!isTS ? "const { Schema } = mongoose;" : ""}
|
|
1196
|
+
${tsInterface}
|
|
1197
|
+
${schemaBody}
|
|
1198
|
+
|
|
1199
|
+
export const User = mongoose.model${isTS ? "<IUser>" : ""}('User', userSchema);
|
|
1200
|
+
`;
|
|
1201
|
+
} else {
|
|
1202
|
+
return `const mongoose = require('mongoose');
|
|
1203
|
+
const bcrypt = require('bcryptjs');
|
|
1204
|
+
const { Schema } = mongoose;
|
|
1205
|
+
|
|
1206
|
+
${schemaBody}
|
|
1207
|
+
|
|
1208
|
+
const User = mongoose.model('User', userSchema);
|
|
1209
|
+
|
|
1210
|
+
module.exports = { User };
|
|
1211
|
+
`;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function generateConfigFile(options) {
|
|
1215
|
+
const isTS = options.language === "typescript";
|
|
1216
|
+
const isESM = options.moduleSystem === "esm";
|
|
1217
|
+
const configBody = `{
|
|
1218
|
+
env: process.env.NODE_ENV || 'development',
|
|
1219
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
1220
|
+
host: process.env.HOST || '0.0.0.0',
|
|
1221
|
+
|
|
1222
|
+
jwt: {
|
|
1223
|
+
secret: process.env.JWT_SECRET || 'your-secret-key',
|
|
1224
|
+
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
|
|
1225
|
+
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
|
|
1226
|
+
},
|
|
1227
|
+
|
|
1228
|
+
cors: {
|
|
1229
|
+
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
|
1230
|
+
},
|
|
1231
|
+
|
|
1232
|
+
rateLimit: {
|
|
1233
|
+
max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
|
|
1234
|
+
},
|
|
1235
|
+
|
|
1236
|
+
log: {
|
|
1237
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
1238
|
+
},
|
|
1239
|
+
}${isTS ? " as const" : ""}`;
|
|
1240
|
+
if (isESM || isTS) {
|
|
1241
|
+
return `import 'dotenv/config';
|
|
1242
|
+
|
|
1243
|
+
export const config = ${configBody};
|
|
1244
|
+
`;
|
|
1245
|
+
} else {
|
|
1246
|
+
return `require('dotenv').config();
|
|
1247
|
+
|
|
1248
|
+
const config = ${configBody};
|
|
1249
|
+
|
|
1250
|
+
module.exports = { config };
|
|
1251
|
+
`;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function generateMiddlewareFile(options) {
|
|
1255
|
+
const isTS = options.language === "typescript";
|
|
1256
|
+
const isESM = options.moduleSystem === "esm";
|
|
1257
|
+
const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
|
|
1258
|
+
const middlewareBody = `/**
|
|
1259
|
+
* Error handler middleware
|
|
1260
|
+
*/
|
|
1261
|
+
function errorHandler(error${isTS ? ": Error" : ""}, request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""})${isTS ? ": void" : ""} {
|
|
1262
|
+
logger.error({ err: error, url: request.url, method: request.method }, 'Request error');
|
|
1263
|
+
|
|
1264
|
+
const statusCode = (error${isTS ? " as any" : ""}).statusCode || 500;
|
|
1265
|
+
const message = statusCode === 500 ? 'Internal Server Error' : error.message;
|
|
1266
|
+
|
|
1267
|
+
reply.status(statusCode).send({
|
|
1268
|
+
success: false,
|
|
1269
|
+
error: message,
|
|
1270
|
+
...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Request logging middleware
|
|
1276
|
+
*/
|
|
1277
|
+
function requestLogger(request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""}, done${isTS ? ": () => void" : ""})${isTS ? ": void" : ""} {
|
|
1278
|
+
logger.info({ url: request.url, method: request.method, ip: request.ip }, 'Incoming request');
|
|
1279
|
+
done();
|
|
1280
|
+
}`;
|
|
1281
|
+
if (isESM || isTS) {
|
|
1282
|
+
return `${isTS ? "import type { FastifyRequest, FastifyReply } from 'fastify';" : ""}
|
|
1283
|
+
import { logger } from '../core/logger.${fileExt}';
|
|
1284
|
+
|
|
1285
|
+
${middlewareBody}
|
|
1286
|
+
|
|
1287
|
+
export { errorHandler, requestLogger };
|
|
1288
|
+
`;
|
|
1289
|
+
} else {
|
|
1290
|
+
return `const { logger } = require('../core/logger.cjs');
|
|
1291
|
+
|
|
1292
|
+
${middlewareBody}
|
|
1293
|
+
|
|
1294
|
+
module.exports = { errorHandler, requestLogger };
|
|
1295
|
+
`;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function generateUtilsFile(options) {
|
|
1299
|
+
const isTS = options.language === "typescript";
|
|
1300
|
+
const isESM = options.moduleSystem === "esm";
|
|
1301
|
+
const utilsBody = `/**
|
|
1302
|
+
* Standard API response helper
|
|
1303
|
+
*/
|
|
1304
|
+
function apiResponse${isTS ? "<T>" : ""}(data${isTS ? ": T" : ""}, message = "Success")${isTS ? ": { success: boolean; message: string; data: T }" : ""} {
|
|
1305
|
+
return {
|
|
1306
|
+
success: true,
|
|
1307
|
+
message,
|
|
1308
|
+
data,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Error response helper
|
|
1314
|
+
*/
|
|
1315
|
+
function errorResponse(message${isTS ? ": string" : ""}, code${isTS ? "?: string" : ""})${isTS ? ": { success: boolean; error: string; code?: string }" : ""} {
|
|
1316
|
+
return {
|
|
1317
|
+
success: false,
|
|
1318
|
+
error: message,
|
|
1319
|
+
...(code && { code }),
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Pagination helper
|
|
1325
|
+
*/
|
|
1326
|
+
function paginate${isTS ? "<T>" : ""}(data${isTS ? ": T[]" : ""}, page${isTS ? ": number" : ""}, limit${isTS ? ": number" : ""}, total${isTS ? ": number" : ""})${isTS ? ": PaginationResult<T>" : ""} {
|
|
1327
|
+
const totalPages = Math.ceil(total / limit);
|
|
1328
|
+
|
|
1329
|
+
return {
|
|
1330
|
+
data,
|
|
1331
|
+
pagination: {
|
|
1332
|
+
page,
|
|
1333
|
+
limit,
|
|
1334
|
+
total,
|
|
1335
|
+
totalPages,
|
|
1336
|
+
hasNextPage: page < totalPages,
|
|
1337
|
+
hasPrevPage: page > 1,
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
}`;
|
|
1341
|
+
const tsInterface = isTS ? `
|
|
1342
|
+
/**
|
|
1343
|
+
* Pagination result type
|
|
1344
|
+
*/
|
|
1345
|
+
export interface PaginationResult<T> {
|
|
1346
|
+
data: T[];
|
|
1347
|
+
pagination: {
|
|
1348
|
+
page: number;
|
|
1349
|
+
limit: number;
|
|
1350
|
+
total: number;
|
|
1351
|
+
totalPages: number;
|
|
1352
|
+
hasNextPage: boolean;
|
|
1353
|
+
hasPrevPage: boolean;
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
` : "";
|
|
1357
|
+
if (isESM || isTS) {
|
|
1358
|
+
return `${tsInterface}
|
|
1359
|
+
${utilsBody}
|
|
1360
|
+
|
|
1361
|
+
export { apiResponse, errorResponse, paginate };
|
|
1362
|
+
`;
|
|
1363
|
+
} else {
|
|
1364
|
+
return `${utilsBody}
|
|
1365
|
+
|
|
1366
|
+
module.exports = { apiResponse, errorResponse, paginate };
|
|
1367
|
+
`;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function generateTypesFile(options) {
|
|
1371
|
+
if (options.language !== "typescript") {
|
|
1372
|
+
return "// Types file - not needed for JavaScript\n";
|
|
1373
|
+
}
|
|
1374
|
+
return `/**
|
|
1375
|
+
* Common type definitions
|
|
1376
|
+
*/
|
|
1377
|
+
|
|
1378
|
+
export interface ApiResponse<T = unknown> {
|
|
1379
|
+
success: boolean;
|
|
1380
|
+
message?: string;
|
|
1381
|
+
data?: T;
|
|
1382
|
+
error?: string;
|
|
1383
|
+
code?: string;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
export interface PaginatedResponse<T> {
|
|
1387
|
+
data: T[];
|
|
1388
|
+
pagination: {
|
|
1389
|
+
page: number;
|
|
1390
|
+
limit: number;
|
|
1391
|
+
total: number;
|
|
1392
|
+
totalPages: number;
|
|
1393
|
+
hasNextPage: boolean;
|
|
1394
|
+
hasPrevPage: boolean;
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
export interface RequestUser {
|
|
1399
|
+
id: string;
|
|
1400
|
+
email: string;
|
|
1401
|
+
role: string;
|
|
1402
|
+
}
|
|
769
1403
|
|
|
770
|
-
|
|
1404
|
+
declare module 'fastify' {
|
|
1405
|
+
interface FastifyRequest {
|
|
1406
|
+
user?: RequestUser;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
771
1409
|
`;
|
|
772
1410
|
}
|
|
773
1411
|
|
|
774
1412
|
// src/cli/commands/generate.ts
|
|
1413
|
+
init_esm_shims();
|
|
775
1414
|
import { Command as Command2 } from "commander";
|
|
776
|
-
import
|
|
1415
|
+
import path5 from "path";
|
|
777
1416
|
import ora2 from "ora";
|
|
778
1417
|
import inquirer2 from "inquirer";
|
|
1418
|
+
import chalk5 from "chalk";
|
|
779
1419
|
|
|
780
1420
|
// src/cli/utils/field-parser.ts
|
|
1421
|
+
init_esm_shims();
|
|
781
1422
|
var tsTypeMap = {
|
|
782
1423
|
string: "string",
|
|
783
1424
|
number: "number",
|
|
@@ -919,7 +1560,11 @@ function parseFields(fieldsStr) {
|
|
|
919
1560
|
return fieldsStr.split(/\s+/).filter(Boolean).map(parseField);
|
|
920
1561
|
}
|
|
921
1562
|
|
|
1563
|
+
// src/cli/commands/generate.ts
|
|
1564
|
+
init_error_handler();
|
|
1565
|
+
|
|
922
1566
|
// src/cli/templates/controller.ts
|
|
1567
|
+
init_esm_shims();
|
|
923
1568
|
function controllerTemplate(name, pascalName, camelName) {
|
|
924
1569
|
return `import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
925
1570
|
import type { ${pascalName}Service } from './${name}.service.js';
|
|
@@ -989,6 +1634,7 @@ export function create${pascalName}Controller(${camelName}Service: ${pascalName}
|
|
|
989
1634
|
}
|
|
990
1635
|
|
|
991
1636
|
// src/cli/templates/service.ts
|
|
1637
|
+
init_esm_shims();
|
|
992
1638
|
function serviceTemplate(name, pascalName, camelName) {
|
|
993
1639
|
return `import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
994
1640
|
import { NotFoundError, ConflictError } from '../../utils/errors.js';
|
|
@@ -1049,6 +1695,7 @@ export function create${pascalName}Service(repository?: ${pascalName}Repository)
|
|
|
1049
1695
|
}
|
|
1050
1696
|
|
|
1051
1697
|
// src/cli/templates/repository.ts
|
|
1698
|
+
init_esm_shims();
|
|
1052
1699
|
function repositoryTemplate(name, pascalName, camelName, pluralName) {
|
|
1053
1700
|
return `import { randomUUID } from 'crypto';
|
|
1054
1701
|
import type { PaginatedResult, PaginationParams } from '../../types/index.js';
|
|
@@ -1155,6 +1802,7 @@ export function create${pascalName}Repository(): ${pascalName}Repository {
|
|
|
1155
1802
|
}
|
|
1156
1803
|
|
|
1157
1804
|
// src/cli/templates/types.ts
|
|
1805
|
+
init_esm_shims();
|
|
1158
1806
|
function typesTemplate(name, pascalName) {
|
|
1159
1807
|
return `import type { BaseEntity } from '../../types/index.js';
|
|
1160
1808
|
|
|
@@ -1184,6 +1832,7 @@ export interface ${pascalName}Filters {
|
|
|
1184
1832
|
}
|
|
1185
1833
|
|
|
1186
1834
|
// src/cli/templates/schemas.ts
|
|
1835
|
+
init_esm_shims();
|
|
1187
1836
|
function schemasTemplate(name, pascalName, camelName) {
|
|
1188
1837
|
return `import { z } from 'zod';
|
|
1189
1838
|
|
|
@@ -1212,6 +1861,7 @@ export type ${pascalName}QueryInput = z.infer<typeof ${camelName}QuerySchema>;
|
|
|
1212
1861
|
}
|
|
1213
1862
|
|
|
1214
1863
|
// src/cli/templates/routes.ts
|
|
1864
|
+
init_esm_shims();
|
|
1215
1865
|
function routesTemplate(name, pascalName, camelName, pluralName) {
|
|
1216
1866
|
return `import type { FastifyInstance } from 'fastify';
|
|
1217
1867
|
import type { ${pascalName}Controller } from './${name}.controller.js';
|
|
@@ -1264,6 +1914,7 @@ export function register${pascalName}Routes(
|
|
|
1264
1914
|
}
|
|
1265
1915
|
|
|
1266
1916
|
// src/cli/templates/module-index.ts
|
|
1917
|
+
init_esm_shims();
|
|
1267
1918
|
function moduleIndexTemplate(name, pascalName, camelName) {
|
|
1268
1919
|
return `import type { FastifyInstance } from 'fastify';
|
|
1269
1920
|
import { logger } from '../../core/logger.js';
|
|
@@ -1299,6 +1950,7 @@ export * from './${name}.schemas.js';
|
|
|
1299
1950
|
}
|
|
1300
1951
|
|
|
1301
1952
|
// src/cli/templates/prisma-model.ts
|
|
1953
|
+
init_esm_shims();
|
|
1302
1954
|
function prismaModelTemplate(name, pascalName, tableName) {
|
|
1303
1955
|
return `
|
|
1304
1956
|
// Add this model to your prisma/schema.prisma file
|
|
@@ -1318,6 +1970,7 @@ model ${pascalName} {
|
|
|
1318
1970
|
}
|
|
1319
1971
|
|
|
1320
1972
|
// src/cli/templates/dynamic-types.ts
|
|
1973
|
+
init_esm_shims();
|
|
1321
1974
|
function dynamicTypesTemplate(name, pascalName, fields) {
|
|
1322
1975
|
const fieldLines = fields.map((field) => {
|
|
1323
1976
|
const tsType = tsTypeMap[field.type];
|
|
@@ -1362,6 +2015,7 @@ ${fields.filter((f) => ["string", "enum", "boolean"].includes(f.type)).map((f) =
|
|
|
1362
2015
|
}
|
|
1363
2016
|
|
|
1364
2017
|
// src/cli/templates/dynamic-schemas.ts
|
|
2018
|
+
init_esm_shims();
|
|
1365
2019
|
function dynamicSchemasTemplate(name, pascalName, camelName, fields, validator = "zod") {
|
|
1366
2020
|
switch (validator) {
|
|
1367
2021
|
case "joi":
|
|
@@ -1540,6 +2194,7 @@ function getJsType(field) {
|
|
|
1540
2194
|
}
|
|
1541
2195
|
|
|
1542
2196
|
// src/cli/templates/dynamic-prisma.ts
|
|
2197
|
+
init_esm_shims();
|
|
1543
2198
|
function dynamicPrismaTemplate(modelName, tableName, fields) {
|
|
1544
2199
|
const fieldLines = [];
|
|
1545
2200
|
for (const field of fields) {
|
|
@@ -1608,10 +2263,23 @@ ${indexLines.join("\n")}
|
|
|
1608
2263
|
}
|
|
1609
2264
|
|
|
1610
2265
|
// src/cli/commands/generate.ts
|
|
2266
|
+
function enableDryRunIfNeeded(options) {
|
|
2267
|
+
const dryRun = DryRunManager.getInstance();
|
|
2268
|
+
if (options.dryRun) {
|
|
2269
|
+
dryRun.enable();
|
|
2270
|
+
console.log(chalk5.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
function showDryRunSummary(options) {
|
|
2274
|
+
if (options.dryRun) {
|
|
2275
|
+
DryRunManager.getInstance().printSummary();
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
1611
2278
|
var generateCommand = new Command2("generate").alias("g").description("Generate resources (module, controller, service, etc.)");
|
|
1612
2279
|
generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
1613
2280
|
"Generate a complete module with controller, service, repository, types, schemas, and routes"
|
|
1614
|
-
).option("--no-routes", "Skip routes generation").option("--no-repository", "Skip repository generation").option("--prisma", "Generate Prisma model suggestion").option("--validator <type>", "Validator type: zod, joi, yup", "zod").option("-i, --interactive", "Interactive mode to define fields").action(async (name, fieldsArgs, options) => {
|
|
2281
|
+
).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").option("--dry-run", "Preview changes without writing files").action(async (name, fieldsArgs, options) => {
|
|
2282
|
+
enableDryRunIfNeeded(options);
|
|
1615
2283
|
let fields = [];
|
|
1616
2284
|
if (options.interactive) {
|
|
1617
2285
|
fields = await promptForFields();
|
|
@@ -1626,7 +2294,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
1626
2294
|
const pluralName = pluralize(kebabName);
|
|
1627
2295
|
const tableName = pluralize(kebabName.replace(/-/g, "_"));
|
|
1628
2296
|
const validatorType = options.validator || "zod";
|
|
1629
|
-
const moduleDir =
|
|
2297
|
+
const moduleDir = path5.join(getModulesDir(), kebabName);
|
|
1630
2298
|
if (await fileExists(moduleDir)) {
|
|
1631
2299
|
spinner.stop();
|
|
1632
2300
|
error(`Module "${kebabName}" already exists`);
|
|
@@ -1665,7 +2333,7 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
1665
2333
|
});
|
|
1666
2334
|
}
|
|
1667
2335
|
for (const file of files) {
|
|
1668
|
-
await writeFile(
|
|
2336
|
+
await writeFile(path5.join(moduleDir, file.name), file.content);
|
|
1669
2337
|
}
|
|
1670
2338
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
1671
2339
|
if (options.prisma || hasFields) {
|
|
@@ -1703,42 +2371,46 @@ generateCommand.command("module <name> [fields...]").alias("m").description(
|
|
|
1703
2371
|
info(` ${hasFields ? "3" : "4"}. Add the Prisma model to schema.prisma`);
|
|
1704
2372
|
info(` ${hasFields ? "4" : "5"}. Run: npm run db:migrate`);
|
|
1705
2373
|
}
|
|
2374
|
+
showDryRunSummary(options);
|
|
1706
2375
|
} catch (err) {
|
|
1707
2376
|
spinner.fail("Failed to generate module");
|
|
1708
2377
|
error(err instanceof Error ? err.message : String(err));
|
|
1709
2378
|
}
|
|
1710
2379
|
});
|
|
1711
|
-
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2380
|
+
generateCommand.command("controller <name>").alias("c").description("Generate a controller").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2381
|
+
enableDryRunIfNeeded(options);
|
|
1712
2382
|
const spinner = ora2("Generating controller...").start();
|
|
1713
2383
|
try {
|
|
1714
2384
|
const kebabName = toKebabCase(name);
|
|
1715
2385
|
const pascalName = toPascalCase(name);
|
|
1716
2386
|
const camelName = toCamelCase(name);
|
|
1717
2387
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1718
|
-
const moduleDir =
|
|
1719
|
-
const filePath =
|
|
2388
|
+
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
2389
|
+
const filePath = path5.join(moduleDir, `${kebabName}.controller.ts`);
|
|
1720
2390
|
if (await fileExists(filePath)) {
|
|
1721
2391
|
spinner.stop();
|
|
1722
|
-
|
|
2392
|
+
displayError(ErrorTypes.FILE_ALREADY_EXISTS(`${kebabName}.controller.ts`));
|
|
1723
2393
|
return;
|
|
1724
2394
|
}
|
|
1725
2395
|
await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
|
|
1726
2396
|
spinner.succeed(`Controller "${pascalName}Controller" generated!`);
|
|
1727
2397
|
success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
|
|
2398
|
+
showDryRunSummary(options);
|
|
1728
2399
|
} catch (err) {
|
|
1729
2400
|
spinner.fail("Failed to generate controller");
|
|
1730
2401
|
error(err instanceof Error ? err.message : String(err));
|
|
1731
2402
|
}
|
|
1732
2403
|
});
|
|
1733
|
-
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2404
|
+
generateCommand.command("service <name>").alias("s").description("Generate a service").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2405
|
+
enableDryRunIfNeeded(options);
|
|
1734
2406
|
const spinner = ora2("Generating service...").start();
|
|
1735
2407
|
try {
|
|
1736
2408
|
const kebabName = toKebabCase(name);
|
|
1737
2409
|
const pascalName = toPascalCase(name);
|
|
1738
2410
|
const camelName = toCamelCase(name);
|
|
1739
2411
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1740
|
-
const moduleDir =
|
|
1741
|
-
const filePath =
|
|
2412
|
+
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
2413
|
+
const filePath = path5.join(moduleDir, `${kebabName}.service.ts`);
|
|
1742
2414
|
if (await fileExists(filePath)) {
|
|
1743
2415
|
spinner.stop();
|
|
1744
2416
|
error(`Service "${kebabName}" already exists`);
|
|
@@ -1747,12 +2419,14 @@ generateCommand.command("service <name>").alias("s").description("Generate a ser
|
|
|
1747
2419
|
await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
|
|
1748
2420
|
spinner.succeed(`Service "${pascalName}Service" generated!`);
|
|
1749
2421
|
success(` src/modules/${moduleName}/${kebabName}.service.ts`);
|
|
2422
|
+
showDryRunSummary(options);
|
|
1750
2423
|
} catch (err) {
|
|
1751
2424
|
spinner.fail("Failed to generate service");
|
|
1752
2425
|
error(err instanceof Error ? err.message : String(err));
|
|
1753
2426
|
}
|
|
1754
2427
|
});
|
|
1755
|
-
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2428
|
+
generateCommand.command("repository <name>").alias("r").description("Generate a repository").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2429
|
+
enableDryRunIfNeeded(options);
|
|
1756
2430
|
const spinner = ora2("Generating repository...").start();
|
|
1757
2431
|
try {
|
|
1758
2432
|
const kebabName = toKebabCase(name);
|
|
@@ -1760,8 +2434,8 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
1760
2434
|
const camelName = toCamelCase(name);
|
|
1761
2435
|
const pluralName = pluralize(kebabName);
|
|
1762
2436
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1763
|
-
const moduleDir =
|
|
1764
|
-
const filePath =
|
|
2437
|
+
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
2438
|
+
const filePath = path5.join(moduleDir, `${kebabName}.repository.ts`);
|
|
1765
2439
|
if (await fileExists(filePath)) {
|
|
1766
2440
|
spinner.stop();
|
|
1767
2441
|
error(`Repository "${kebabName}" already exists`);
|
|
@@ -1770,19 +2444,21 @@ generateCommand.command("repository <name>").alias("r").description("Generate a
|
|
|
1770
2444
|
await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
|
|
1771
2445
|
spinner.succeed(`Repository "${pascalName}Repository" generated!`);
|
|
1772
2446
|
success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
|
|
2447
|
+
showDryRunSummary(options);
|
|
1773
2448
|
} catch (err) {
|
|
1774
2449
|
spinner.fail("Failed to generate repository");
|
|
1775
2450
|
error(err instanceof Error ? err.message : String(err));
|
|
1776
2451
|
}
|
|
1777
2452
|
});
|
|
1778
|
-
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2453
|
+
generateCommand.command("types <name>").alias("t").description("Generate types/interfaces").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2454
|
+
enableDryRunIfNeeded(options);
|
|
1779
2455
|
const spinner = ora2("Generating types...").start();
|
|
1780
2456
|
try {
|
|
1781
2457
|
const kebabName = toKebabCase(name);
|
|
1782
2458
|
const pascalName = toPascalCase(name);
|
|
1783
2459
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1784
|
-
const moduleDir =
|
|
1785
|
-
const filePath =
|
|
2460
|
+
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
2461
|
+
const filePath = path5.join(moduleDir, `${kebabName}.types.ts`);
|
|
1786
2462
|
if (await fileExists(filePath)) {
|
|
1787
2463
|
spinner.stop();
|
|
1788
2464
|
error(`Types file "${kebabName}.types.ts" already exists`);
|
|
@@ -1791,20 +2467,22 @@ generateCommand.command("types <name>").alias("t").description("Generate types/i
|
|
|
1791
2467
|
await writeFile(filePath, typesTemplate(kebabName, pascalName));
|
|
1792
2468
|
spinner.succeed(`Types for "${pascalName}" generated!`);
|
|
1793
2469
|
success(` src/modules/${moduleName}/${kebabName}.types.ts`);
|
|
2470
|
+
showDryRunSummary(options);
|
|
1794
2471
|
} catch (err) {
|
|
1795
2472
|
spinner.fail("Failed to generate types");
|
|
1796
2473
|
error(err instanceof Error ? err.message : String(err));
|
|
1797
2474
|
}
|
|
1798
2475
|
});
|
|
1799
|
-
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2476
|
+
generateCommand.command("schema <name>").alias("v").description("Generate validation schemas").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2477
|
+
enableDryRunIfNeeded(options);
|
|
1800
2478
|
const spinner = ora2("Generating schemas...").start();
|
|
1801
2479
|
try {
|
|
1802
2480
|
const kebabName = toKebabCase(name);
|
|
1803
2481
|
const pascalName = toPascalCase(name);
|
|
1804
2482
|
const camelName = toCamelCase(name);
|
|
1805
2483
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1806
|
-
const moduleDir =
|
|
1807
|
-
const filePath =
|
|
2484
|
+
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
2485
|
+
const filePath = path5.join(moduleDir, `${kebabName}.schemas.ts`);
|
|
1808
2486
|
if (await fileExists(filePath)) {
|
|
1809
2487
|
spinner.stop();
|
|
1810
2488
|
error(`Schemas file "${kebabName}.schemas.ts" already exists`);
|
|
@@ -1813,12 +2491,14 @@ generateCommand.command("schema <name>").alias("v").description("Generate valida
|
|
|
1813
2491
|
await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
|
|
1814
2492
|
spinner.succeed(`Schemas for "${pascalName}" generated!`);
|
|
1815
2493
|
success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
|
|
2494
|
+
showDryRunSummary(options);
|
|
1816
2495
|
} catch (err) {
|
|
1817
2496
|
spinner.fail("Failed to generate schemas");
|
|
1818
2497
|
error(err instanceof Error ? err.message : String(err));
|
|
1819
2498
|
}
|
|
1820
2499
|
});
|
|
1821
|
-
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").action(async (name, options) => {
|
|
2500
|
+
generateCommand.command("routes <name>").description("Generate routes").option("-m, --module <module>", "Target module name").option("--dry-run", "Preview changes without writing files").action(async (name, options) => {
|
|
2501
|
+
enableDryRunIfNeeded(options);
|
|
1822
2502
|
const spinner = ora2("Generating routes...").start();
|
|
1823
2503
|
try {
|
|
1824
2504
|
const kebabName = toKebabCase(name);
|
|
@@ -1826,8 +2506,8 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
1826
2506
|
const camelName = toCamelCase(name);
|
|
1827
2507
|
const pluralName = pluralize(kebabName);
|
|
1828
2508
|
const moduleName = options.module ? toKebabCase(options.module) : kebabName;
|
|
1829
|
-
const moduleDir =
|
|
1830
|
-
const filePath =
|
|
2509
|
+
const moduleDir = path5.join(getModulesDir(), moduleName);
|
|
2510
|
+
const filePath = path5.join(moduleDir, `${kebabName}.routes.ts`);
|
|
1831
2511
|
if (await fileExists(filePath)) {
|
|
1832
2512
|
spinner.stop();
|
|
1833
2513
|
error(`Routes file "${kebabName}.routes.ts" already exists`);
|
|
@@ -1836,6 +2516,7 @@ generateCommand.command("routes <name>").description("Generate routes").option("
|
|
|
1836
2516
|
await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
|
|
1837
2517
|
spinner.succeed(`Routes for "${pascalName}" generated!`);
|
|
1838
2518
|
success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
|
|
2519
|
+
showDryRunSummary(options);
|
|
1839
2520
|
} catch (err) {
|
|
1840
2521
|
spinner.fail("Failed to generate routes");
|
|
1841
2522
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -1913,22 +2594,24 @@ async function promptForFields() {
|
|
|
1913
2594
|
}
|
|
1914
2595
|
|
|
1915
2596
|
// src/cli/commands/add-module.ts
|
|
2597
|
+
init_esm_shims();
|
|
1916
2598
|
import { Command as Command3 } from "commander";
|
|
1917
|
-
import
|
|
2599
|
+
import path8 from "path";
|
|
1918
2600
|
import ora3 from "ora";
|
|
1919
|
-
import
|
|
2601
|
+
import chalk7 from "chalk";
|
|
1920
2602
|
import * as fs5 from "fs/promises";
|
|
1921
2603
|
|
|
1922
2604
|
// src/cli/utils/env-manager.ts
|
|
2605
|
+
init_esm_shims();
|
|
1923
2606
|
import * as fs3 from "fs/promises";
|
|
1924
|
-
import * as
|
|
2607
|
+
import * as path6 from "path";
|
|
1925
2608
|
import { existsSync } from "fs";
|
|
1926
2609
|
var EnvManager = class {
|
|
1927
2610
|
envPath;
|
|
1928
2611
|
envExamplePath;
|
|
1929
2612
|
constructor(projectRoot) {
|
|
1930
|
-
this.envPath =
|
|
1931
|
-
this.envExamplePath =
|
|
2613
|
+
this.envPath = path6.join(projectRoot, ".env");
|
|
2614
|
+
this.envExamplePath = path6.join(projectRoot, ".env.example");
|
|
1932
2615
|
}
|
|
1933
2616
|
/**
|
|
1934
2617
|
* Add environment variables to .env file
|
|
@@ -1936,12 +2619,12 @@ var EnvManager = class {
|
|
|
1936
2619
|
async addVariables(sections) {
|
|
1937
2620
|
const added = [];
|
|
1938
2621
|
const skipped = [];
|
|
1939
|
-
let
|
|
2622
|
+
let created2 = false;
|
|
1940
2623
|
let envContent = "";
|
|
1941
2624
|
if (existsSync(this.envPath)) {
|
|
1942
2625
|
envContent = await fs3.readFile(this.envPath, "utf-8");
|
|
1943
2626
|
} else {
|
|
1944
|
-
|
|
2627
|
+
created2 = true;
|
|
1945
2628
|
}
|
|
1946
2629
|
const existingKeys = this.parseExistingKeys(envContent);
|
|
1947
2630
|
let newContent = envContent;
|
|
@@ -1972,7 +2655,7 @@ var EnvManager = class {
|
|
|
1972
2655
|
if (existsSync(this.envExamplePath)) {
|
|
1973
2656
|
await this.updateEnvExample(sections);
|
|
1974
2657
|
}
|
|
1975
|
-
return { added, skipped, created };
|
|
2658
|
+
return { added, skipped, created: created2 };
|
|
1976
2659
|
}
|
|
1977
2660
|
/**
|
|
1978
2661
|
* Update .env.example file
|
|
@@ -2578,16 +3261,17 @@ var EnvManager = class {
|
|
|
2578
3261
|
};
|
|
2579
3262
|
|
|
2580
3263
|
// src/cli/utils/template-manager.ts
|
|
3264
|
+
init_esm_shims();
|
|
2581
3265
|
import * as fs4 from "fs/promises";
|
|
2582
|
-
import * as
|
|
3266
|
+
import * as path7 from "path";
|
|
2583
3267
|
import { createHash } from "crypto";
|
|
2584
3268
|
import { existsSync as existsSync2 } from "fs";
|
|
2585
3269
|
var TemplateManager = class {
|
|
2586
3270
|
templatesDir;
|
|
2587
3271
|
manifestsDir;
|
|
2588
3272
|
constructor(projectRoot) {
|
|
2589
|
-
this.templatesDir =
|
|
2590
|
-
this.manifestsDir =
|
|
3273
|
+
this.templatesDir = path7.join(projectRoot, ".servcraft", "templates");
|
|
3274
|
+
this.manifestsDir = path7.join(projectRoot, ".servcraft", "manifests");
|
|
2591
3275
|
}
|
|
2592
3276
|
/**
|
|
2593
3277
|
* Initialize template system
|
|
@@ -2601,10 +3285,10 @@ var TemplateManager = class {
|
|
|
2601
3285
|
*/
|
|
2602
3286
|
async saveTemplate(moduleName, files) {
|
|
2603
3287
|
await this.initialize();
|
|
2604
|
-
const moduleTemplateDir =
|
|
3288
|
+
const moduleTemplateDir = path7.join(this.templatesDir, moduleName);
|
|
2605
3289
|
await fs4.mkdir(moduleTemplateDir, { recursive: true });
|
|
2606
3290
|
for (const [fileName, content] of Object.entries(files)) {
|
|
2607
|
-
const filePath =
|
|
3291
|
+
const filePath = path7.join(moduleTemplateDir, fileName);
|
|
2608
3292
|
await fs4.writeFile(filePath, content, "utf-8");
|
|
2609
3293
|
}
|
|
2610
3294
|
}
|
|
@@ -2613,7 +3297,7 @@ var TemplateManager = class {
|
|
|
2613
3297
|
*/
|
|
2614
3298
|
async getTemplate(moduleName, fileName) {
|
|
2615
3299
|
try {
|
|
2616
|
-
const filePath =
|
|
3300
|
+
const filePath = path7.join(this.templatesDir, moduleName, fileName);
|
|
2617
3301
|
return await fs4.readFile(filePath, "utf-8");
|
|
2618
3302
|
} catch {
|
|
2619
3303
|
return null;
|
|
@@ -2638,7 +3322,7 @@ var TemplateManager = class {
|
|
|
2638
3322
|
installedAt: /* @__PURE__ */ new Date(),
|
|
2639
3323
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2640
3324
|
};
|
|
2641
|
-
const manifestPath =
|
|
3325
|
+
const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
|
|
2642
3326
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2643
3327
|
}
|
|
2644
3328
|
/**
|
|
@@ -2646,7 +3330,7 @@ var TemplateManager = class {
|
|
|
2646
3330
|
*/
|
|
2647
3331
|
async getManifest(moduleName) {
|
|
2648
3332
|
try {
|
|
2649
|
-
const manifestPath =
|
|
3333
|
+
const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
|
|
2650
3334
|
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
2651
3335
|
return JSON.parse(content);
|
|
2652
3336
|
} catch {
|
|
@@ -2675,7 +3359,7 @@ var TemplateManager = class {
|
|
|
2675
3359
|
}
|
|
2676
3360
|
const results = [];
|
|
2677
3361
|
for (const [fileName, fileInfo] of Object.entries(manifest.files)) {
|
|
2678
|
-
const filePath =
|
|
3362
|
+
const filePath = path7.join(moduleDir, fileName);
|
|
2679
3363
|
if (!existsSync2(filePath)) {
|
|
2680
3364
|
results.push({
|
|
2681
3365
|
fileName,
|
|
@@ -2701,7 +3385,7 @@ var TemplateManager = class {
|
|
|
2701
3385
|
*/
|
|
2702
3386
|
async createBackup(moduleName, moduleDir) {
|
|
2703
3387
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
2704
|
-
const backupDir =
|
|
3388
|
+
const backupDir = path7.join(path7.dirname(moduleDir), `${moduleName}.backup-${timestamp}`);
|
|
2705
3389
|
await this.copyDirectory(moduleDir, backupDir);
|
|
2706
3390
|
return backupDir;
|
|
2707
3391
|
}
|
|
@@ -2712,8 +3396,8 @@ var TemplateManager = class {
|
|
|
2712
3396
|
await fs4.mkdir(dest, { recursive: true });
|
|
2713
3397
|
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
2714
3398
|
for (const entry of entries) {
|
|
2715
|
-
const srcPath =
|
|
2716
|
-
const destPath =
|
|
3399
|
+
const srcPath = path7.join(src, entry.name);
|
|
3400
|
+
const destPath = path7.join(dest, entry.name);
|
|
2717
3401
|
if (entry.isDirectory()) {
|
|
2718
3402
|
await this.copyDirectory(srcPath, destPath);
|
|
2719
3403
|
} else {
|
|
@@ -2814,23 +3498,24 @@ var TemplateManager = class {
|
|
|
2814
3498
|
}
|
|
2815
3499
|
manifest.files = fileHashes;
|
|
2816
3500
|
manifest.updatedAt = /* @__PURE__ */ new Date();
|
|
2817
|
-
const manifestPath =
|
|
3501
|
+
const manifestPath = path7.join(this.manifestsDir, `${moduleName}.json`);
|
|
2818
3502
|
await fs4.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2819
3503
|
}
|
|
2820
3504
|
};
|
|
2821
3505
|
|
|
2822
3506
|
// src/cli/utils/interactive-prompt.ts
|
|
3507
|
+
init_esm_shims();
|
|
2823
3508
|
import inquirer3 from "inquirer";
|
|
2824
|
-
import
|
|
3509
|
+
import chalk6 from "chalk";
|
|
2825
3510
|
var InteractivePrompt = class {
|
|
2826
3511
|
/**
|
|
2827
3512
|
* Ask what to do when module already exists
|
|
2828
3513
|
*/
|
|
2829
3514
|
static async askModuleExists(moduleName, hasModifications) {
|
|
2830
|
-
console.log(
|
|
3515
|
+
console.log(chalk6.yellow(`
|
|
2831
3516
|
\u26A0\uFE0F Module "${moduleName}" already exists`));
|
|
2832
3517
|
if (hasModifications) {
|
|
2833
|
-
console.log(
|
|
3518
|
+
console.log(chalk6.yellow(" Some files have been modified by you.\n"));
|
|
2834
3519
|
}
|
|
2835
3520
|
const { action } = await inquirer3.prompt([
|
|
2836
3521
|
{
|
|
@@ -2868,12 +3553,12 @@ var InteractivePrompt = class {
|
|
|
2868
3553
|
* Ask what to do with a specific file
|
|
2869
3554
|
*/
|
|
2870
3555
|
static async askFileAction(fileName, isModified, yourLines, newLines) {
|
|
2871
|
-
console.log(
|
|
3556
|
+
console.log(chalk6.cyan(`
|
|
2872
3557
|
\u{1F4C1} ${fileName}`));
|
|
2873
3558
|
console.log(
|
|
2874
|
-
|
|
3559
|
+
chalk6.gray(` Your version: ${yourLines} lines${isModified ? " (modified)" : ""}`)
|
|
2875
3560
|
);
|
|
2876
|
-
console.log(
|
|
3561
|
+
console.log(chalk6.gray(` New version: ${newLines} lines
|
|
2877
3562
|
`));
|
|
2878
3563
|
const { action } = await inquirer3.prompt([
|
|
2879
3564
|
{
|
|
@@ -2925,7 +3610,7 @@ var InteractivePrompt = class {
|
|
|
2925
3610
|
* Display diff and ask to continue
|
|
2926
3611
|
*/
|
|
2927
3612
|
static async showDiffAndAsk(diff) {
|
|
2928
|
-
console.log(
|
|
3613
|
+
console.log(chalk6.cyan("\n\u{1F4CA} Differences:\n"));
|
|
2929
3614
|
console.log(diff);
|
|
2930
3615
|
return await this.confirm("\nDo you want to proceed with this change?", true);
|
|
2931
3616
|
}
|
|
@@ -2933,41 +3618,41 @@ var InteractivePrompt = class {
|
|
|
2933
3618
|
* Display merge conflicts
|
|
2934
3619
|
*/
|
|
2935
3620
|
static displayConflicts(conflicts) {
|
|
2936
|
-
console.log(
|
|
3621
|
+
console.log(chalk6.red("\n\u26A0\uFE0F Merge Conflicts Detected:\n"));
|
|
2937
3622
|
conflicts.forEach((conflict, i) => {
|
|
2938
|
-
console.log(
|
|
3623
|
+
console.log(chalk6.yellow(` ${i + 1}. ${conflict}`));
|
|
2939
3624
|
});
|
|
2940
|
-
console.log(
|
|
2941
|
-
console.log(
|
|
2942
|
-
console.log(
|
|
2943
|
-
console.log(
|
|
2944
|
-
console.log(
|
|
2945
|
-
console.log(
|
|
3625
|
+
console.log(chalk6.gray("\n Conflict markers have been added to the file:"));
|
|
3626
|
+
console.log(chalk6.gray(" <<<<<<< YOUR VERSION"));
|
|
3627
|
+
console.log(chalk6.gray(" ... your code ..."));
|
|
3628
|
+
console.log(chalk6.gray(" ======="));
|
|
3629
|
+
console.log(chalk6.gray(" ... new code ..."));
|
|
3630
|
+
console.log(chalk6.gray(" >>>>>>> NEW VERSION\n"));
|
|
2946
3631
|
}
|
|
2947
3632
|
/**
|
|
2948
3633
|
* Show backup location
|
|
2949
3634
|
*/
|
|
2950
3635
|
static showBackupCreated(backupPath) {
|
|
2951
|
-
console.log(
|
|
2952
|
-
\u2713 Backup created: ${
|
|
3636
|
+
console.log(chalk6.green(`
|
|
3637
|
+
\u2713 Backup created: ${chalk6.cyan(backupPath)}`));
|
|
2953
3638
|
}
|
|
2954
3639
|
/**
|
|
2955
3640
|
* Show merge summary
|
|
2956
3641
|
*/
|
|
2957
3642
|
static showMergeSummary(stats) {
|
|
2958
|
-
console.log(
|
|
3643
|
+
console.log(chalk6.bold("\n\u{1F4CA} Merge Summary:\n"));
|
|
2959
3644
|
if (stats.merged > 0) {
|
|
2960
|
-
console.log(
|
|
3645
|
+
console.log(chalk6.green(` \u2713 Merged: ${stats.merged} file(s)`));
|
|
2961
3646
|
}
|
|
2962
3647
|
if (stats.kept > 0) {
|
|
2963
|
-
console.log(
|
|
3648
|
+
console.log(chalk6.blue(` \u2192 Kept: ${stats.kept} file(s)`));
|
|
2964
3649
|
}
|
|
2965
3650
|
if (stats.overwritten > 0) {
|
|
2966
|
-
console.log(
|
|
3651
|
+
console.log(chalk6.yellow(` \u26A0 Overwritten: ${stats.overwritten} file(s)`));
|
|
2967
3652
|
}
|
|
2968
3653
|
if (stats.conflicts > 0) {
|
|
2969
|
-
console.log(
|
|
2970
|
-
console.log(
|
|
3654
|
+
console.log(chalk6.red(` \u26A0 Conflicts: ${stats.conflicts} file(s)`));
|
|
3655
|
+
console.log(chalk6.gray("\n Please resolve conflicts manually before committing.\n"));
|
|
2971
3656
|
}
|
|
2972
3657
|
}
|
|
2973
3658
|
/**
|
|
@@ -3005,6 +3690,7 @@ var InteractivePrompt = class {
|
|
|
3005
3690
|
};
|
|
3006
3691
|
|
|
3007
3692
|
// src/cli/commands/add-module.ts
|
|
3693
|
+
init_error_handler();
|
|
3008
3694
|
var AVAILABLE_MODULES = {
|
|
3009
3695
|
auth: {
|
|
3010
3696
|
name: "Authentication",
|
|
@@ -3139,31 +3825,40 @@ var AVAILABLE_MODULES = {
|
|
|
3139
3825
|
var addModuleCommand = new Command3("add").description("Add a pre-built module to your project").argument(
|
|
3140
3826
|
"[module]",
|
|
3141
3827
|
"Module to add (auth, users, email, audit, upload, cache, notifications, settings)"
|
|
3142
|
-
).option("-l, --list", "List available modules").option("-f, --force", "Force overwrite existing module").option("-u, --update", "Update existing module (smart merge)").option("--skip-existing", "Skip if module already exists").action(
|
|
3828
|
+
).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").option("--dry-run", "Preview changes without writing files").action(
|
|
3143
3829
|
async (moduleName, options) => {
|
|
3144
3830
|
if (options?.list || !moduleName) {
|
|
3145
|
-
console.log(
|
|
3831
|
+
console.log(chalk7.bold("\n\u{1F4E6} Available Modules:\n"));
|
|
3146
3832
|
for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
|
|
3147
|
-
console.log(` ${
|
|
3148
|
-
console.log(` ${" ".repeat(15)} ${
|
|
3833
|
+
console.log(` ${chalk7.cyan(key.padEnd(15))} ${mod.name}`);
|
|
3834
|
+
console.log(` ${" ".repeat(15)} ${chalk7.gray(mod.description)}
|
|
3149
3835
|
`);
|
|
3150
3836
|
}
|
|
3151
|
-
console.log(
|
|
3152
|
-
console.log(` ${
|
|
3153
|
-
console.log(` ${
|
|
3154
|
-
console.log(` ${
|
|
3837
|
+
console.log(chalk7.bold("Usage:"));
|
|
3838
|
+
console.log(` ${chalk7.yellow("servcraft add auth")} Add authentication module`);
|
|
3839
|
+
console.log(` ${chalk7.yellow("servcraft add users")} Add user management module`);
|
|
3840
|
+
console.log(` ${chalk7.yellow("servcraft add email")} Add email service module
|
|
3155
3841
|
`);
|
|
3156
3842
|
return;
|
|
3157
3843
|
}
|
|
3844
|
+
const dryRun = DryRunManager.getInstance();
|
|
3845
|
+
if (options?.dryRun) {
|
|
3846
|
+
dryRun.enable();
|
|
3847
|
+
console.log(chalk7.yellow("\n\u26A0 DRY RUN MODE - No files will be written\n"));
|
|
3848
|
+
}
|
|
3158
3849
|
const module = AVAILABLE_MODULES[moduleName];
|
|
3159
3850
|
if (!module) {
|
|
3160
|
-
|
|
3161
|
-
|
|
3851
|
+
displayError(ErrorTypes.MODULE_NOT_FOUND(moduleName));
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
const projectError = validateProject();
|
|
3855
|
+
if (projectError) {
|
|
3856
|
+
displayError(projectError);
|
|
3162
3857
|
return;
|
|
3163
3858
|
}
|
|
3164
3859
|
const spinner = ora3(`Adding ${module.name} module...`).start();
|
|
3165
3860
|
try {
|
|
3166
|
-
const moduleDir =
|
|
3861
|
+
const moduleDir = path8.join(getModulesDir(), moduleName);
|
|
3167
3862
|
const templateManager = new TemplateManager(process.cwd());
|
|
3168
3863
|
const moduleExists = await fileExists(moduleDir);
|
|
3169
3864
|
if (moduleExists) {
|
|
@@ -3231,16 +3926,16 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
|
|
|
3231
3926
|
info("\n\u{1F4DD} Created new .env file");
|
|
3232
3927
|
}
|
|
3233
3928
|
if (result.added.length > 0) {
|
|
3234
|
-
console.log(
|
|
3929
|
+
console.log(chalk7.bold("\n\u2705 Added to .env:"));
|
|
3235
3930
|
result.added.forEach((key) => success(` ${key}`));
|
|
3236
3931
|
}
|
|
3237
3932
|
if (result.skipped.length > 0) {
|
|
3238
|
-
console.log(
|
|
3933
|
+
console.log(chalk7.bold("\n\u23ED\uFE0F Already in .env (skipped):"));
|
|
3239
3934
|
result.skipped.forEach((key) => info(` ${key}`));
|
|
3240
3935
|
}
|
|
3241
3936
|
const requiredVars = envSections.flatMap((section) => section.variables).filter((v) => v.required && !v.value).map((v) => v.key);
|
|
3242
3937
|
if (requiredVars.length > 0) {
|
|
3243
|
-
console.log(
|
|
3938
|
+
console.log(chalk7.bold("\n\u26A0\uFE0F Required configuration:"));
|
|
3244
3939
|
requiredVars.forEach((key) => warn(` ${key} - Please configure this variable`));
|
|
3245
3940
|
}
|
|
3246
3941
|
} catch (err) {
|
|
@@ -3248,10 +3943,15 @@ var addModuleCommand = new Command3("add").description("Add a pre-built module t
|
|
|
3248
3943
|
error(err instanceof Error ? err.message : String(err));
|
|
3249
3944
|
}
|
|
3250
3945
|
}
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3946
|
+
if (!options?.dryRun) {
|
|
3947
|
+
console.log("\n\u{1F4CC} Next steps:");
|
|
3948
|
+
info(" 1. Configure environment variables in .env (if needed)");
|
|
3949
|
+
info(" 2. Register the module in your main app file");
|
|
3950
|
+
info(" 3. Run database migrations if needed");
|
|
3951
|
+
}
|
|
3952
|
+
if (options?.dryRun) {
|
|
3953
|
+
dryRun.printSummary();
|
|
3954
|
+
}
|
|
3255
3955
|
} catch (err) {
|
|
3256
3956
|
spinner.fail("Failed to add module");
|
|
3257
3957
|
error(err instanceof Error ? err.message : String(err));
|
|
@@ -3302,7 +4002,7 @@ export * from './auth.schemas.js';
|
|
|
3302
4002
|
`
|
|
3303
4003
|
};
|
|
3304
4004
|
for (const [name, content] of Object.entries(files)) {
|
|
3305
|
-
await writeFile(
|
|
4005
|
+
await writeFile(path8.join(dir, name), content);
|
|
3306
4006
|
}
|
|
3307
4007
|
}
|
|
3308
4008
|
async function generateUsersModule(dir) {
|
|
@@ -3342,7 +4042,7 @@ export * from './user.schemas.js';
|
|
|
3342
4042
|
`
|
|
3343
4043
|
};
|
|
3344
4044
|
for (const [name, content] of Object.entries(files)) {
|
|
3345
|
-
await writeFile(
|
|
4045
|
+
await writeFile(path8.join(dir, name), content);
|
|
3346
4046
|
}
|
|
3347
4047
|
}
|
|
3348
4048
|
async function generateEmailModule(dir) {
|
|
@@ -3399,7 +4099,7 @@ export { EmailService, emailService } from './email.service.js';
|
|
|
3399
4099
|
`
|
|
3400
4100
|
};
|
|
3401
4101
|
for (const [name, content] of Object.entries(files)) {
|
|
3402
|
-
await writeFile(
|
|
4102
|
+
await writeFile(path8.join(dir, name), content);
|
|
3403
4103
|
}
|
|
3404
4104
|
}
|
|
3405
4105
|
async function generateAuditModule(dir) {
|
|
@@ -3443,7 +4143,7 @@ export { AuditService, auditService } from './audit.service.js';
|
|
|
3443
4143
|
`
|
|
3444
4144
|
};
|
|
3445
4145
|
for (const [name, content] of Object.entries(files)) {
|
|
3446
|
-
await writeFile(
|
|
4146
|
+
await writeFile(path8.join(dir, name), content);
|
|
3447
4147
|
}
|
|
3448
4148
|
}
|
|
3449
4149
|
async function generateUploadModule(dir) {
|
|
@@ -3469,7 +4169,7 @@ export interface UploadOptions {
|
|
|
3469
4169
|
`
|
|
3470
4170
|
};
|
|
3471
4171
|
for (const [name, content] of Object.entries(files)) {
|
|
3472
|
-
await writeFile(
|
|
4172
|
+
await writeFile(path8.join(dir, name), content);
|
|
3473
4173
|
}
|
|
3474
4174
|
}
|
|
3475
4175
|
async function generateCacheModule(dir) {
|
|
@@ -3515,7 +4215,7 @@ export { CacheService, cacheService } from './cache.service.js';
|
|
|
3515
4215
|
`
|
|
3516
4216
|
};
|
|
3517
4217
|
for (const [name, content] of Object.entries(files)) {
|
|
3518
|
-
await writeFile(
|
|
4218
|
+
await writeFile(path8.join(dir, name), content);
|
|
3519
4219
|
}
|
|
3520
4220
|
}
|
|
3521
4221
|
async function generateGenericModule(dir, name) {
|
|
@@ -3529,18 +4229,18 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
|
|
|
3529
4229
|
`
|
|
3530
4230
|
};
|
|
3531
4231
|
for (const [fileName, content] of Object.entries(files)) {
|
|
3532
|
-
await writeFile(
|
|
4232
|
+
await writeFile(path8.join(dir, fileName), content);
|
|
3533
4233
|
}
|
|
3534
4234
|
}
|
|
3535
4235
|
async function findServercraftModules() {
|
|
3536
|
-
const scriptDir =
|
|
4236
|
+
const scriptDir = path8.dirname(new URL(import.meta.url).pathname);
|
|
3537
4237
|
const possiblePaths = [
|
|
3538
4238
|
// Local node_modules (when servcraft is a dependency)
|
|
3539
|
-
|
|
4239
|
+
path8.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
|
|
3540
4240
|
// From dist/cli/index.js -> src/modules (npx or global install)
|
|
3541
|
-
|
|
4241
|
+
path8.resolve(scriptDir, "..", "..", "src", "modules"),
|
|
3542
4242
|
// From src/cli/commands/add-module.ts -> src/modules (development)
|
|
3543
|
-
|
|
4243
|
+
path8.resolve(scriptDir, "..", "..", "modules")
|
|
3544
4244
|
];
|
|
3545
4245
|
for (const p of possiblePaths) {
|
|
3546
4246
|
try {
|
|
@@ -3555,7 +4255,7 @@ async function findServercraftModules() {
|
|
|
3555
4255
|
}
|
|
3556
4256
|
async function generateModuleFiles(moduleName, moduleDir) {
|
|
3557
4257
|
const moduleNameMap = {
|
|
3558
|
-
|
|
4258
|
+
users: "user",
|
|
3559
4259
|
"rate-limit": "rate-limit",
|
|
3560
4260
|
"feature-flag": "feature-flag",
|
|
3561
4261
|
"api-versioning": "api-versioning",
|
|
@@ -3564,7 +4264,7 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
3564
4264
|
const sourceDirName = moduleNameMap[moduleName] || moduleName;
|
|
3565
4265
|
const servercraftModulesDir = await findServercraftModules();
|
|
3566
4266
|
if (servercraftModulesDir) {
|
|
3567
|
-
const sourceModuleDir =
|
|
4267
|
+
const sourceModuleDir = path8.join(servercraftModulesDir, sourceDirName);
|
|
3568
4268
|
if (await fileExists(sourceModuleDir)) {
|
|
3569
4269
|
await copyModuleFromSource(sourceModuleDir, moduleDir);
|
|
3570
4270
|
return;
|
|
@@ -3596,8 +4296,8 @@ async function generateModuleFiles(moduleName, moduleDir) {
|
|
|
3596
4296
|
async function copyModuleFromSource(sourceDir, targetDir) {
|
|
3597
4297
|
const entries = await fs5.readdir(sourceDir, { withFileTypes: true });
|
|
3598
4298
|
for (const entry of entries) {
|
|
3599
|
-
const sourcePath =
|
|
3600
|
-
const targetPath =
|
|
4299
|
+
const sourcePath = path8.join(sourceDir, entry.name);
|
|
4300
|
+
const targetPath = path8.join(targetDir, entry.name);
|
|
3601
4301
|
if (entry.isDirectory()) {
|
|
3602
4302
|
await fs5.mkdir(targetPath, { recursive: true });
|
|
3603
4303
|
await copyModuleFromSource(sourcePath, targetPath);
|
|
@@ -3610,7 +4310,7 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
3610
4310
|
const files = {};
|
|
3611
4311
|
const entries = await fs5.readdir(moduleDir);
|
|
3612
4312
|
for (const entry of entries) {
|
|
3613
|
-
const filePath =
|
|
4313
|
+
const filePath = path8.join(moduleDir, entry);
|
|
3614
4314
|
const stat2 = await fs5.stat(filePath);
|
|
3615
4315
|
if (stat2.isFile() && entry.endsWith(".ts")) {
|
|
3616
4316
|
const content = await fs5.readFile(filePath, "utf-8");
|
|
@@ -3621,14 +4321,14 @@ async function getModuleFiles(moduleName, moduleDir) {
|
|
|
3621
4321
|
}
|
|
3622
4322
|
async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
3623
4323
|
const modifiedFiles = await templateManager.getModifiedFiles(moduleName, moduleDir);
|
|
3624
|
-
console.log(
|
|
4324
|
+
console.log(chalk7.cyan(`
|
|
3625
4325
|
\u{1F4CA} Changes in module "${moduleName}":
|
|
3626
4326
|
`));
|
|
3627
4327
|
for (const file of modifiedFiles) {
|
|
3628
4328
|
if (file.isModified) {
|
|
3629
|
-
console.log(
|
|
4329
|
+
console.log(chalk7.yellow(`
|
|
3630
4330
|
\u{1F4C4} ${file.fileName}:`));
|
|
3631
|
-
const currentPath =
|
|
4331
|
+
const currentPath = path8.join(moduleDir, file.fileName);
|
|
3632
4332
|
const currentContent = await fs5.readFile(currentPath, "utf-8");
|
|
3633
4333
|
const originalContent = await templateManager.getTemplate(moduleName, file.fileName);
|
|
3634
4334
|
if (originalContent) {
|
|
@@ -3641,11 +4341,11 @@ async function showDiffForModule(templateManager, moduleName, moduleDir) {
|
|
|
3641
4341
|
async function performSmartMerge(templateManager, moduleName, moduleDir, _displayName) {
|
|
3642
4342
|
const spinner = ora3("Analyzing files for merge...").start();
|
|
3643
4343
|
const newFiles = {};
|
|
3644
|
-
const templateDir =
|
|
4344
|
+
const templateDir = path8.join(templateManager["templatesDir"], moduleName);
|
|
3645
4345
|
try {
|
|
3646
4346
|
const entries = await fs5.readdir(templateDir);
|
|
3647
4347
|
for (const entry of entries) {
|
|
3648
|
-
const content = await fs5.readFile(
|
|
4348
|
+
const content = await fs5.readFile(path8.join(templateDir, entry), "utf-8");
|
|
3649
4349
|
newFiles[entry] = content;
|
|
3650
4350
|
}
|
|
3651
4351
|
} catch {
|
|
@@ -3663,7 +4363,7 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
3663
4363
|
};
|
|
3664
4364
|
for (const fileInfo of modifiedFiles) {
|
|
3665
4365
|
const fileName = fileInfo.fileName;
|
|
3666
|
-
const filePath =
|
|
4366
|
+
const filePath = path8.join(moduleDir, fileName);
|
|
3667
4367
|
const newContent = newFiles[fileName];
|
|
3668
4368
|
if (!newContent) {
|
|
3669
4369
|
continue;
|
|
@@ -3732,10 +4432,11 @@ async function performSmartMerge(templateManager, moduleName, moduleDir, _displa
|
|
|
3732
4432
|
}
|
|
3733
4433
|
|
|
3734
4434
|
// src/cli/commands/db.ts
|
|
4435
|
+
init_esm_shims();
|
|
3735
4436
|
import { Command as Command4 } from "commander";
|
|
3736
4437
|
import { execSync as execSync2, spawn } from "child_process";
|
|
3737
4438
|
import ora4 from "ora";
|
|
3738
|
-
import
|
|
4439
|
+
import chalk8 from "chalk";
|
|
3739
4440
|
var dbCommand = new Command4("db").description("Database management commands");
|
|
3740
4441
|
dbCommand.command("migrate").description("Run database migrations").option("-n, --name <name>", "Migration name").action(async (options) => {
|
|
3741
4442
|
const spinner = ora4("Running migrations...").start();
|
|
@@ -3792,7 +4493,7 @@ dbCommand.command("seed").description("Run database seed").action(async () => {
|
|
|
3792
4493
|
});
|
|
3793
4494
|
dbCommand.command("reset").description("Reset database (drop all data and re-run migrations)").option("-f, --force", "Skip confirmation").action(async (options) => {
|
|
3794
4495
|
if (!options.force) {
|
|
3795
|
-
console.log(
|
|
4496
|
+
console.log(chalk8.yellow("\n\u26A0\uFE0F WARNING: This will delete all data in your database!\n"));
|
|
3796
4497
|
const readline = await import("readline");
|
|
3797
4498
|
const rl = readline.createInterface({
|
|
3798
4499
|
input: process.stdin,
|
|
@@ -3824,12 +4525,2149 @@ dbCommand.command("status").description("Show migration status").action(async ()
|
|
|
3824
4525
|
}
|
|
3825
4526
|
});
|
|
3826
4527
|
|
|
4528
|
+
// src/cli/commands/docs.ts
|
|
4529
|
+
init_esm_shims();
|
|
4530
|
+
import { Command as Command5 } from "commander";
|
|
4531
|
+
import path10 from "path";
|
|
4532
|
+
import fs7 from "fs/promises";
|
|
4533
|
+
import ora6 from "ora";
|
|
4534
|
+
import chalk9 from "chalk";
|
|
4535
|
+
|
|
4536
|
+
// src/cli/utils/docs-generator.ts
|
|
4537
|
+
init_esm_shims();
|
|
4538
|
+
import fs6 from "fs/promises";
|
|
4539
|
+
import path9 from "path";
|
|
4540
|
+
import ora5 from "ora";
|
|
4541
|
+
|
|
4542
|
+
// src/core/server.ts
|
|
4543
|
+
init_esm_shims();
|
|
4544
|
+
import Fastify from "fastify";
|
|
4545
|
+
|
|
4546
|
+
// src/core/logger.ts
|
|
4547
|
+
init_esm_shims();
|
|
4548
|
+
import pino from "pino";
|
|
4549
|
+
var defaultConfig = {
|
|
4550
|
+
level: process.env.LOG_LEVEL || "info",
|
|
4551
|
+
pretty: process.env.NODE_ENV !== "production",
|
|
4552
|
+
name: "servcraft"
|
|
4553
|
+
};
|
|
4554
|
+
function createLogger(config2 = {}) {
|
|
4555
|
+
const mergedConfig = { ...defaultConfig, ...config2 };
|
|
4556
|
+
const transport = mergedConfig.pretty ? {
|
|
4557
|
+
target: "pino-pretty",
|
|
4558
|
+
options: {
|
|
4559
|
+
colorize: true,
|
|
4560
|
+
translateTime: "SYS:standard",
|
|
4561
|
+
ignore: "pid,hostname"
|
|
4562
|
+
}
|
|
4563
|
+
} : void 0;
|
|
4564
|
+
return pino({
|
|
4565
|
+
name: mergedConfig.name,
|
|
4566
|
+
level: mergedConfig.level,
|
|
4567
|
+
transport,
|
|
4568
|
+
formatters: {
|
|
4569
|
+
level: (label) => ({ level: label })
|
|
4570
|
+
},
|
|
4571
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
4572
|
+
});
|
|
4573
|
+
}
|
|
4574
|
+
var logger = createLogger();
|
|
4575
|
+
|
|
4576
|
+
// src/core/server.ts
|
|
4577
|
+
var defaultConfig2 = {
|
|
4578
|
+
port: parseInt(process.env.PORT || "3000", 10),
|
|
4579
|
+
host: process.env.HOST || "0.0.0.0",
|
|
4580
|
+
trustProxy: true,
|
|
4581
|
+
bodyLimit: 1048576,
|
|
4582
|
+
// 1MB
|
|
4583
|
+
requestTimeout: 3e4
|
|
4584
|
+
// 30s
|
|
4585
|
+
};
|
|
4586
|
+
var Server = class {
|
|
4587
|
+
app;
|
|
4588
|
+
config;
|
|
4589
|
+
logger;
|
|
4590
|
+
isShuttingDown = false;
|
|
4591
|
+
constructor(config2 = {}) {
|
|
4592
|
+
this.config = { ...defaultConfig2, ...config2 };
|
|
4593
|
+
this.logger = this.config.logger || logger;
|
|
4594
|
+
const fastifyOptions = {
|
|
4595
|
+
logger: this.logger,
|
|
4596
|
+
trustProxy: this.config.trustProxy,
|
|
4597
|
+
bodyLimit: this.config.bodyLimit,
|
|
4598
|
+
requestTimeout: this.config.requestTimeout
|
|
4599
|
+
};
|
|
4600
|
+
this.app = Fastify(fastifyOptions);
|
|
4601
|
+
this.setupHealthCheck();
|
|
4602
|
+
this.setupGracefulShutdown();
|
|
4603
|
+
}
|
|
4604
|
+
get instance() {
|
|
4605
|
+
return this.app;
|
|
4606
|
+
}
|
|
4607
|
+
setupHealthCheck() {
|
|
4608
|
+
this.app.get("/health", async (_request, reply) => {
|
|
4609
|
+
const healthcheck = {
|
|
4610
|
+
status: "ok",
|
|
4611
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4612
|
+
uptime: process.uptime(),
|
|
4613
|
+
memory: process.memoryUsage(),
|
|
4614
|
+
version: process.env.npm_package_version || "0.1.0"
|
|
4615
|
+
};
|
|
4616
|
+
return reply.status(200).send(healthcheck);
|
|
4617
|
+
});
|
|
4618
|
+
this.app.get("/ready", async (_request, reply) => {
|
|
4619
|
+
if (this.isShuttingDown) {
|
|
4620
|
+
return reply.status(503).send({ status: "shutting_down" });
|
|
4621
|
+
}
|
|
4622
|
+
return reply.status(200).send({ status: "ready" });
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
setupGracefulShutdown() {
|
|
4626
|
+
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
4627
|
+
signals.forEach((signal) => {
|
|
4628
|
+
process.on(signal, async () => {
|
|
4629
|
+
this.logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
4630
|
+
await this.shutdown();
|
|
4631
|
+
});
|
|
4632
|
+
});
|
|
4633
|
+
process.on("uncaughtException", async (error2) => {
|
|
4634
|
+
this.logger.error({ err: error2 }, "Uncaught exception");
|
|
4635
|
+
await this.shutdown(1);
|
|
4636
|
+
});
|
|
4637
|
+
process.on("unhandledRejection", async (reason) => {
|
|
4638
|
+
this.logger.error({ err: reason }, "Unhandled rejection");
|
|
4639
|
+
await this.shutdown(1);
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
async shutdown(exitCode = 0) {
|
|
4643
|
+
if (this.isShuttingDown) {
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
this.isShuttingDown = true;
|
|
4647
|
+
this.logger.info("Graceful shutdown initiated...");
|
|
4648
|
+
const shutdownTimeout = setTimeout(() => {
|
|
4649
|
+
this.logger.error("Graceful shutdown timeout, forcing exit");
|
|
4650
|
+
process.exit(1);
|
|
4651
|
+
}, 3e4);
|
|
4652
|
+
try {
|
|
4653
|
+
await this.app.close();
|
|
4654
|
+
this.logger.info("Server closed successfully");
|
|
4655
|
+
clearTimeout(shutdownTimeout);
|
|
4656
|
+
process.exit(exitCode);
|
|
4657
|
+
} catch (error2) {
|
|
4658
|
+
this.logger.error({ err: error2 }, "Error during shutdown");
|
|
4659
|
+
clearTimeout(shutdownTimeout);
|
|
4660
|
+
process.exit(1);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
async start() {
|
|
4664
|
+
try {
|
|
4665
|
+
await this.app.listen({
|
|
4666
|
+
port: this.config.port,
|
|
4667
|
+
host: this.config.host
|
|
4668
|
+
});
|
|
4669
|
+
this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
|
|
4670
|
+
} catch (error2) {
|
|
4671
|
+
this.logger.error({ err: error2 }, "Failed to start server");
|
|
4672
|
+
throw error2;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
};
|
|
4676
|
+
function createServer(config2 = {}) {
|
|
4677
|
+
return new Server(config2);
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
// src/middleware/index.ts
|
|
4681
|
+
init_esm_shims();
|
|
4682
|
+
|
|
4683
|
+
// src/middleware/error-handler.ts
|
|
4684
|
+
init_esm_shims();
|
|
4685
|
+
|
|
4686
|
+
// src/utils/errors.ts
|
|
4687
|
+
init_esm_shims();
|
|
4688
|
+
var AppError = class _AppError extends Error {
|
|
4689
|
+
statusCode;
|
|
4690
|
+
isOperational;
|
|
4691
|
+
errors;
|
|
4692
|
+
constructor(message, statusCode = 500, isOperational = true, errors) {
|
|
4693
|
+
super(message);
|
|
4694
|
+
this.statusCode = statusCode;
|
|
4695
|
+
this.isOperational = isOperational;
|
|
4696
|
+
this.errors = errors;
|
|
4697
|
+
Object.setPrototypeOf(this, _AppError.prototype);
|
|
4698
|
+
Error.captureStackTrace(this, this.constructor);
|
|
4699
|
+
}
|
|
4700
|
+
};
|
|
4701
|
+
var NotFoundError = class _NotFoundError extends AppError {
|
|
4702
|
+
constructor(resource = "Resource") {
|
|
4703
|
+
super(`${resource} not found`, 404);
|
|
4704
|
+
Object.setPrototypeOf(this, _NotFoundError.prototype);
|
|
4705
|
+
}
|
|
4706
|
+
};
|
|
4707
|
+
var UnauthorizedError = class _UnauthorizedError extends AppError {
|
|
4708
|
+
constructor(message = "Unauthorized") {
|
|
4709
|
+
super(message, 401);
|
|
4710
|
+
Object.setPrototypeOf(this, _UnauthorizedError.prototype);
|
|
4711
|
+
}
|
|
4712
|
+
};
|
|
4713
|
+
var ForbiddenError = class _ForbiddenError extends AppError {
|
|
4714
|
+
constructor(message = "Forbidden") {
|
|
4715
|
+
super(message, 403);
|
|
4716
|
+
Object.setPrototypeOf(this, _ForbiddenError.prototype);
|
|
4717
|
+
}
|
|
4718
|
+
};
|
|
4719
|
+
var BadRequestError = class _BadRequestError extends AppError {
|
|
4720
|
+
constructor(message = "Bad request", errors) {
|
|
4721
|
+
super(message, 400, true, errors);
|
|
4722
|
+
Object.setPrototypeOf(this, _BadRequestError.prototype);
|
|
4723
|
+
}
|
|
4724
|
+
};
|
|
4725
|
+
var ConflictError = class _ConflictError extends AppError {
|
|
4726
|
+
constructor(message = "Resource already exists") {
|
|
4727
|
+
super(message, 409);
|
|
4728
|
+
Object.setPrototypeOf(this, _ConflictError.prototype);
|
|
4729
|
+
}
|
|
4730
|
+
};
|
|
4731
|
+
var ValidationError = class _ValidationError extends AppError {
|
|
4732
|
+
constructor(errors) {
|
|
4733
|
+
super("Validation failed", 422, true, errors);
|
|
4734
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
4735
|
+
}
|
|
4736
|
+
};
|
|
4737
|
+
function isAppError(error2) {
|
|
4738
|
+
return error2 instanceof AppError;
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
// src/config/index.ts
|
|
4742
|
+
init_esm_shims();
|
|
4743
|
+
|
|
4744
|
+
// src/config/env.ts
|
|
4745
|
+
init_esm_shims();
|
|
4746
|
+
import { z } from "zod";
|
|
4747
|
+
import dotenv from "dotenv";
|
|
4748
|
+
dotenv.config();
|
|
4749
|
+
var envSchema = z.object({
|
|
4750
|
+
// Server
|
|
4751
|
+
NODE_ENV: z.enum(["development", "staging", "production", "test"]).default("development"),
|
|
4752
|
+
PORT: z.string().transform(Number).default("3000"),
|
|
4753
|
+
HOST: z.string().default("0.0.0.0"),
|
|
4754
|
+
// Database
|
|
4755
|
+
DATABASE_URL: z.string().optional(),
|
|
4756
|
+
// JWT
|
|
4757
|
+
JWT_SECRET: z.string().min(32).optional(),
|
|
4758
|
+
JWT_ACCESS_EXPIRES_IN: z.string().default("15m"),
|
|
4759
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default("7d"),
|
|
4760
|
+
// Security
|
|
4761
|
+
CORS_ORIGIN: z.string().default("*"),
|
|
4762
|
+
RATE_LIMIT_MAX: z.string().transform(Number).default("100"),
|
|
4763
|
+
RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default("60000"),
|
|
4764
|
+
// Email
|
|
4765
|
+
SMTP_HOST: z.string().optional(),
|
|
4766
|
+
SMTP_PORT: z.string().transform(Number).optional(),
|
|
4767
|
+
SMTP_USER: z.string().optional(),
|
|
4768
|
+
SMTP_PASS: z.string().optional(),
|
|
4769
|
+
SMTP_FROM: z.string().optional(),
|
|
4770
|
+
// Redis (optional)
|
|
4771
|
+
REDIS_URL: z.string().optional(),
|
|
4772
|
+
// Logging
|
|
4773
|
+
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
|
|
4774
|
+
});
|
|
4775
|
+
function validateEnv() {
|
|
4776
|
+
const parsed = envSchema.safeParse(process.env);
|
|
4777
|
+
if (!parsed.success) {
|
|
4778
|
+
logger.error({ errors: parsed.error.flatten().fieldErrors }, "Invalid environment variables");
|
|
4779
|
+
throw new Error("Invalid environment variables");
|
|
4780
|
+
}
|
|
4781
|
+
return parsed.data;
|
|
4782
|
+
}
|
|
4783
|
+
var env = validateEnv();
|
|
4784
|
+
function isProduction() {
|
|
4785
|
+
return env.NODE_ENV === "production";
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
// src/config/index.ts
|
|
4789
|
+
function parseCorsOrigin(origin) {
|
|
4790
|
+
if (origin === "*") return "*";
|
|
4791
|
+
if (origin.includes(",")) {
|
|
4792
|
+
return origin.split(",").map((o) => o.trim());
|
|
4793
|
+
}
|
|
4794
|
+
return origin;
|
|
4795
|
+
}
|
|
4796
|
+
function createConfig() {
|
|
4797
|
+
return {
|
|
4798
|
+
env,
|
|
4799
|
+
server: {
|
|
4800
|
+
port: env.PORT,
|
|
4801
|
+
host: env.HOST
|
|
4802
|
+
},
|
|
4803
|
+
jwt: {
|
|
4804
|
+
secret: env.JWT_SECRET || "change-me-in-production-please-32chars",
|
|
4805
|
+
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
4806
|
+
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN
|
|
4807
|
+
},
|
|
4808
|
+
security: {
|
|
4809
|
+
corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
|
|
4810
|
+
rateLimit: {
|
|
4811
|
+
max: env.RATE_LIMIT_MAX,
|
|
4812
|
+
windowMs: env.RATE_LIMIT_WINDOW_MS
|
|
4813
|
+
}
|
|
4814
|
+
},
|
|
4815
|
+
email: {
|
|
4816
|
+
host: env.SMTP_HOST,
|
|
4817
|
+
port: env.SMTP_PORT,
|
|
4818
|
+
user: env.SMTP_USER,
|
|
4819
|
+
pass: env.SMTP_PASS,
|
|
4820
|
+
from: env.SMTP_FROM
|
|
4821
|
+
},
|
|
4822
|
+
database: {
|
|
4823
|
+
url: env.DATABASE_URL
|
|
4824
|
+
},
|
|
4825
|
+
redis: {
|
|
4826
|
+
url: env.REDIS_URL
|
|
4827
|
+
}
|
|
4828
|
+
};
|
|
4829
|
+
}
|
|
4830
|
+
var config = createConfig();
|
|
4831
|
+
|
|
4832
|
+
// src/middleware/error-handler.ts
|
|
4833
|
+
function registerErrorHandler(app) {
|
|
4834
|
+
app.setErrorHandler(
|
|
4835
|
+
(error2, request, reply) => {
|
|
4836
|
+
logger.error(
|
|
4837
|
+
{
|
|
4838
|
+
err: error2,
|
|
4839
|
+
requestId: request.id,
|
|
4840
|
+
method: request.method,
|
|
4841
|
+
url: request.url
|
|
4842
|
+
},
|
|
4843
|
+
"Request error"
|
|
4844
|
+
);
|
|
4845
|
+
if (isAppError(error2)) {
|
|
4846
|
+
return reply.status(error2.statusCode).send({
|
|
4847
|
+
success: false,
|
|
4848
|
+
message: error2.message,
|
|
4849
|
+
errors: error2.errors,
|
|
4850
|
+
...isProduction() ? {} : { stack: error2.stack }
|
|
4851
|
+
});
|
|
4852
|
+
}
|
|
4853
|
+
if ("validation" in error2 && error2.validation) {
|
|
4854
|
+
const errors = {};
|
|
4855
|
+
for (const err of error2.validation) {
|
|
4856
|
+
const field = err.instancePath?.replace("/", "") || "body";
|
|
4857
|
+
if (!errors[field]) {
|
|
4858
|
+
errors[field] = [];
|
|
4859
|
+
}
|
|
4860
|
+
errors[field].push(err.message || "Invalid value");
|
|
4861
|
+
}
|
|
4862
|
+
return reply.status(400).send({
|
|
4863
|
+
success: false,
|
|
4864
|
+
message: "Validation failed",
|
|
4865
|
+
errors
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
if ("statusCode" in error2 && typeof error2.statusCode === "number") {
|
|
4869
|
+
return reply.status(error2.statusCode).send({
|
|
4870
|
+
success: false,
|
|
4871
|
+
message: error2.message,
|
|
4872
|
+
...isProduction() ? {} : { stack: error2.stack }
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
return reply.status(500).send({
|
|
4876
|
+
success: false,
|
|
4877
|
+
message: isProduction() ? "Internal server error" : error2.message,
|
|
4878
|
+
...isProduction() ? {} : { stack: error2.stack }
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4881
|
+
);
|
|
4882
|
+
app.setNotFoundHandler((request, reply) => {
|
|
4883
|
+
return reply.status(404).send({
|
|
4884
|
+
success: false,
|
|
4885
|
+
message: `Route ${request.method} ${request.url} not found`
|
|
4886
|
+
});
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
// src/middleware/security.ts
|
|
4891
|
+
init_esm_shims();
|
|
4892
|
+
import helmet from "@fastify/helmet";
|
|
4893
|
+
import cors from "@fastify/cors";
|
|
4894
|
+
import rateLimit from "@fastify/rate-limit";
|
|
4895
|
+
var defaultOptions = {
|
|
4896
|
+
helmet: true,
|
|
4897
|
+
cors: true,
|
|
4898
|
+
rateLimit: true
|
|
4899
|
+
};
|
|
4900
|
+
async function registerSecurity(app, options = {}) {
|
|
4901
|
+
const opts = { ...defaultOptions, ...options };
|
|
4902
|
+
if (opts.helmet) {
|
|
4903
|
+
await app.register(helmet, {
|
|
4904
|
+
contentSecurityPolicy: {
|
|
4905
|
+
directives: {
|
|
4906
|
+
defaultSrc: ["'self'"],
|
|
4907
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
4908
|
+
scriptSrc: ["'self'"],
|
|
4909
|
+
imgSrc: ["'self'", "data:", "https:"]
|
|
4910
|
+
}
|
|
4911
|
+
},
|
|
4912
|
+
crossOriginEmbedderPolicy: false
|
|
4913
|
+
});
|
|
4914
|
+
logger.debug("Helmet security headers enabled");
|
|
4915
|
+
}
|
|
4916
|
+
if (opts.cors) {
|
|
4917
|
+
await app.register(cors, {
|
|
4918
|
+
origin: config.security.corsOrigin,
|
|
4919
|
+
credentials: true,
|
|
4920
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
4921
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
|
4922
|
+
exposedHeaders: ["X-Total-Count", "X-Page", "X-Limit"],
|
|
4923
|
+
maxAge: 86400
|
|
4924
|
+
// 24 hours
|
|
4925
|
+
});
|
|
4926
|
+
logger.debug({ origin: config.security.corsOrigin }, "CORS enabled");
|
|
4927
|
+
}
|
|
4928
|
+
if (opts.rateLimit) {
|
|
4929
|
+
await app.register(rateLimit, {
|
|
4930
|
+
max: config.security.rateLimit.max,
|
|
4931
|
+
timeWindow: config.security.rateLimit.windowMs,
|
|
4932
|
+
errorResponseBuilder: (_request, context) => ({
|
|
4933
|
+
success: false,
|
|
4934
|
+
message: "Too many requests, please try again later",
|
|
4935
|
+
retryAfter: context.after
|
|
4936
|
+
}),
|
|
4937
|
+
keyGenerator: (request) => {
|
|
4938
|
+
return request.headers["x-forwarded-for"]?.toString().split(",")[0] || request.ip || "unknown";
|
|
4939
|
+
}
|
|
4940
|
+
});
|
|
4941
|
+
logger.debug(
|
|
4942
|
+
{
|
|
4943
|
+
max: config.security.rateLimit.max,
|
|
4944
|
+
windowMs: config.security.rateLimit.windowMs
|
|
4945
|
+
},
|
|
4946
|
+
"Rate limiting enabled"
|
|
4947
|
+
);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
// src/modules/swagger/index.ts
|
|
4952
|
+
init_esm_shims();
|
|
4953
|
+
|
|
4954
|
+
// src/modules/swagger/swagger.service.ts
|
|
4955
|
+
init_esm_shims();
|
|
4956
|
+
import swagger from "@fastify/swagger";
|
|
4957
|
+
import swaggerUi from "@fastify/swagger-ui";
|
|
4958
|
+
var defaultConfig3 = {
|
|
4959
|
+
title: "Servcraft API",
|
|
4960
|
+
description: "API documentation generated by Servcraft",
|
|
4961
|
+
version: "1.0.0",
|
|
4962
|
+
tags: [
|
|
4963
|
+
{ name: "Auth", description: "Authentication endpoints" },
|
|
4964
|
+
{ name: "Users", description: "User management endpoints" },
|
|
4965
|
+
{ name: "Health", description: "Health check endpoints" }
|
|
4966
|
+
]
|
|
4967
|
+
};
|
|
4968
|
+
async function registerSwagger(app, customConfig) {
|
|
4969
|
+
const swaggerConfig = { ...defaultConfig3, ...customConfig };
|
|
4970
|
+
await app.register(swagger, {
|
|
4971
|
+
openapi: {
|
|
4972
|
+
openapi: "3.0.3",
|
|
4973
|
+
info: {
|
|
4974
|
+
title: swaggerConfig.title,
|
|
4975
|
+
description: swaggerConfig.description,
|
|
4976
|
+
version: swaggerConfig.version,
|
|
4977
|
+
contact: swaggerConfig.contact,
|
|
4978
|
+
license: swaggerConfig.license
|
|
4979
|
+
},
|
|
4980
|
+
servers: swaggerConfig.servers || [
|
|
4981
|
+
{
|
|
4982
|
+
url: `http://localhost:${config.server.port}`,
|
|
4983
|
+
description: "Development server"
|
|
4984
|
+
}
|
|
4985
|
+
],
|
|
4986
|
+
tags: swaggerConfig.tags,
|
|
4987
|
+
components: {
|
|
4988
|
+
securitySchemes: {
|
|
4989
|
+
bearerAuth: {
|
|
4990
|
+
type: "http",
|
|
4991
|
+
scheme: "bearer",
|
|
4992
|
+
bearerFormat: "JWT",
|
|
4993
|
+
description: "Enter your JWT token"
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
});
|
|
4999
|
+
await app.register(swaggerUi, {
|
|
5000
|
+
routePrefix: "/docs",
|
|
5001
|
+
uiConfig: {
|
|
5002
|
+
docExpansion: "list",
|
|
5003
|
+
deepLinking: true,
|
|
5004
|
+
displayRequestDuration: true,
|
|
5005
|
+
filter: true,
|
|
5006
|
+
showExtensions: true,
|
|
5007
|
+
showCommonExtensions: true
|
|
5008
|
+
},
|
|
5009
|
+
staticCSP: true,
|
|
5010
|
+
transformStaticCSP: (header) => header
|
|
5011
|
+
});
|
|
5012
|
+
logger.info("Swagger documentation registered at /docs");
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
// src/modules/swagger/schema-builder.ts
|
|
5016
|
+
init_esm_shims();
|
|
5017
|
+
|
|
5018
|
+
// src/modules/auth/index.ts
|
|
5019
|
+
init_esm_shims();
|
|
5020
|
+
import jwt from "@fastify/jwt";
|
|
5021
|
+
import cookie from "@fastify/cookie";
|
|
5022
|
+
|
|
5023
|
+
// src/modules/auth/auth.service.ts
|
|
5024
|
+
init_esm_shims();
|
|
5025
|
+
import bcrypt from "bcryptjs";
|
|
5026
|
+
import { Redis } from "ioredis";
|
|
5027
|
+
var AuthService = class {
|
|
5028
|
+
app;
|
|
5029
|
+
SALT_ROUNDS = 12;
|
|
5030
|
+
redis = null;
|
|
5031
|
+
BLACKLIST_PREFIX = "auth:blacklist:";
|
|
5032
|
+
BLACKLIST_TTL = 7 * 24 * 60 * 60;
|
|
5033
|
+
// 7 days in seconds
|
|
5034
|
+
constructor(app, redisUrl) {
|
|
5035
|
+
this.app = app;
|
|
5036
|
+
if (redisUrl || process.env.REDIS_URL) {
|
|
5037
|
+
try {
|
|
5038
|
+
this.redis = new Redis(redisUrl || process.env.REDIS_URL || "redis://localhost:6379");
|
|
5039
|
+
this.redis.on("connect", () => {
|
|
5040
|
+
logger.info("Auth service connected to Redis for token blacklist");
|
|
5041
|
+
});
|
|
5042
|
+
this.redis.on("error", (error2) => {
|
|
5043
|
+
logger.error({ err: error2 }, "Redis connection error in Auth service");
|
|
5044
|
+
});
|
|
5045
|
+
} catch (error2) {
|
|
5046
|
+
logger.warn({ err: error2 }, "Failed to connect to Redis, using in-memory blacklist");
|
|
5047
|
+
this.redis = null;
|
|
5048
|
+
}
|
|
5049
|
+
} else {
|
|
5050
|
+
logger.warn(
|
|
5051
|
+
"No REDIS_URL provided, using in-memory token blacklist (not recommended for production)"
|
|
5052
|
+
);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
async hashPassword(password) {
|
|
5056
|
+
return bcrypt.hash(password, this.SALT_ROUNDS);
|
|
5057
|
+
}
|
|
5058
|
+
async verifyPassword(password, hash) {
|
|
5059
|
+
return bcrypt.compare(password, hash);
|
|
5060
|
+
}
|
|
5061
|
+
generateTokenPair(user) {
|
|
5062
|
+
const accessPayload = {
|
|
5063
|
+
sub: user.id,
|
|
5064
|
+
email: user.email,
|
|
5065
|
+
role: user.role,
|
|
5066
|
+
type: "access"
|
|
5067
|
+
};
|
|
5068
|
+
const refreshPayload = {
|
|
5069
|
+
sub: user.id,
|
|
5070
|
+
email: user.email,
|
|
5071
|
+
role: user.role,
|
|
5072
|
+
type: "refresh"
|
|
5073
|
+
};
|
|
5074
|
+
const accessToken = this.app.jwt.sign(accessPayload, {
|
|
5075
|
+
expiresIn: config.jwt.accessExpiresIn
|
|
5076
|
+
});
|
|
5077
|
+
const refreshToken = this.app.jwt.sign(refreshPayload, {
|
|
5078
|
+
expiresIn: config.jwt.refreshExpiresIn
|
|
5079
|
+
});
|
|
5080
|
+
const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
|
|
5081
|
+
return { accessToken, refreshToken, expiresIn };
|
|
5082
|
+
}
|
|
5083
|
+
parseExpiration(expiration) {
|
|
5084
|
+
const match = expiration.match(/^(\d+)([smhd])$/);
|
|
5085
|
+
if (!match) return 900;
|
|
5086
|
+
const value = parseInt(match[1] || "0", 10);
|
|
5087
|
+
const unit = match[2];
|
|
5088
|
+
switch (unit) {
|
|
5089
|
+
case "s":
|
|
5090
|
+
return value;
|
|
5091
|
+
case "m":
|
|
5092
|
+
return value * 60;
|
|
5093
|
+
case "h":
|
|
5094
|
+
return value * 3600;
|
|
5095
|
+
case "d":
|
|
5096
|
+
return value * 86400;
|
|
5097
|
+
default:
|
|
5098
|
+
return 900;
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
async verifyAccessToken(token) {
|
|
5102
|
+
try {
|
|
5103
|
+
if (await this.isTokenBlacklisted(token)) {
|
|
5104
|
+
throw new UnauthorizedError("Token has been revoked");
|
|
5105
|
+
}
|
|
5106
|
+
const payload = this.app.jwt.verify(token);
|
|
5107
|
+
if (payload.type !== "access") {
|
|
5108
|
+
throw new UnauthorizedError("Invalid token type");
|
|
5109
|
+
}
|
|
5110
|
+
return payload;
|
|
5111
|
+
} catch (error2) {
|
|
5112
|
+
if (error2 instanceof UnauthorizedError) throw error2;
|
|
5113
|
+
logger.debug({ err: error2 }, "Token verification failed");
|
|
5114
|
+
throw new UnauthorizedError("Invalid or expired token");
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
async verifyRefreshToken(token) {
|
|
5118
|
+
try {
|
|
5119
|
+
if (await this.isTokenBlacklisted(token)) {
|
|
5120
|
+
throw new UnauthorizedError("Token has been revoked");
|
|
5121
|
+
}
|
|
5122
|
+
const payload = this.app.jwt.verify(token);
|
|
5123
|
+
if (payload.type !== "refresh") {
|
|
5124
|
+
throw new UnauthorizedError("Invalid token type");
|
|
5125
|
+
}
|
|
5126
|
+
return payload;
|
|
5127
|
+
} catch (error2) {
|
|
5128
|
+
if (error2 instanceof UnauthorizedError) throw error2;
|
|
5129
|
+
logger.debug({ err: error2 }, "Refresh token verification failed");
|
|
5130
|
+
throw new UnauthorizedError("Invalid or expired refresh token");
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
/**
|
|
5134
|
+
* Blacklist a token (JWT revocation)
|
|
5135
|
+
* Uses Redis if available, falls back to in-memory Set
|
|
5136
|
+
*/
|
|
5137
|
+
async blacklistToken(token) {
|
|
5138
|
+
if (this.redis) {
|
|
5139
|
+
try {
|
|
5140
|
+
const key = `${this.BLACKLIST_PREFIX}${token}`;
|
|
5141
|
+
await this.redis.setex(key, this.BLACKLIST_TTL, "1");
|
|
5142
|
+
logger.debug("Token blacklisted in Redis");
|
|
5143
|
+
} catch (error2) {
|
|
5144
|
+
logger.error({ err: error2 }, "Failed to blacklist token in Redis");
|
|
5145
|
+
throw new Error("Failed to revoke token");
|
|
5146
|
+
}
|
|
5147
|
+
} else {
|
|
5148
|
+
logger.warn("Using in-memory blacklist - not suitable for multi-instance deployments");
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
/**
|
|
5152
|
+
* Check if a token is blacklisted
|
|
5153
|
+
* Uses Redis if available, falls back to always returning false
|
|
5154
|
+
*/
|
|
5155
|
+
async isTokenBlacklisted(token) {
|
|
5156
|
+
if (this.redis) {
|
|
5157
|
+
try {
|
|
5158
|
+
const key = `${this.BLACKLIST_PREFIX}${token}`;
|
|
5159
|
+
const result = await this.redis.exists(key);
|
|
5160
|
+
return result === 1;
|
|
5161
|
+
} catch (error2) {
|
|
5162
|
+
logger.error({ err: error2 }, "Failed to check token blacklist in Redis");
|
|
5163
|
+
return false;
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
return false;
|
|
5167
|
+
}
|
|
5168
|
+
/**
|
|
5169
|
+
* Get count of blacklisted tokens (Redis only)
|
|
5170
|
+
*/
|
|
5171
|
+
async getBlacklistCount() {
|
|
5172
|
+
if (this.redis) {
|
|
5173
|
+
try {
|
|
5174
|
+
const keys = await this.redis.keys(`${this.BLACKLIST_PREFIX}*`);
|
|
5175
|
+
return keys.length;
|
|
5176
|
+
} catch (error2) {
|
|
5177
|
+
logger.error({ err: error2 }, "Failed to get blacklist count from Redis");
|
|
5178
|
+
return 0;
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
return 0;
|
|
5182
|
+
}
|
|
5183
|
+
/**
|
|
5184
|
+
* Close Redis connection
|
|
5185
|
+
*/
|
|
5186
|
+
async close() {
|
|
5187
|
+
if (this.redis) {
|
|
5188
|
+
await this.redis.quit();
|
|
5189
|
+
logger.info("Auth service Redis connection closed");
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
// OAuth support methods - to be implemented with user repository
|
|
5193
|
+
async findUserByEmail(_email) {
|
|
5194
|
+
return null;
|
|
5195
|
+
}
|
|
5196
|
+
async createUserFromOAuth(data) {
|
|
5197
|
+
const user = {
|
|
5198
|
+
id: `oauth_${Date.now()}`,
|
|
5199
|
+
email: data.email,
|
|
5200
|
+
role: "user"
|
|
5201
|
+
};
|
|
5202
|
+
logger.info({ email: data.email }, "Created user from OAuth");
|
|
5203
|
+
return user;
|
|
5204
|
+
}
|
|
5205
|
+
async generateTokensForUser(userId) {
|
|
5206
|
+
const user = {
|
|
5207
|
+
id: userId,
|
|
5208
|
+
email: "",
|
|
5209
|
+
// Would be fetched from database in production
|
|
5210
|
+
role: "user"
|
|
5211
|
+
};
|
|
5212
|
+
return this.generateTokenPair(user);
|
|
5213
|
+
}
|
|
5214
|
+
async verifyPasswordById(userId, _password) {
|
|
5215
|
+
logger.debug({ userId }, "Password verification requested");
|
|
5216
|
+
return false;
|
|
5217
|
+
}
|
|
5218
|
+
};
|
|
5219
|
+
function createAuthService(app) {
|
|
5220
|
+
return new AuthService(app);
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5223
|
+
// src/modules/auth/auth.controller.ts
|
|
5224
|
+
init_esm_shims();
|
|
5225
|
+
|
|
5226
|
+
// src/modules/auth/schemas.ts
|
|
5227
|
+
init_esm_shims();
|
|
5228
|
+
import { z as z2 } from "zod";
|
|
5229
|
+
var loginSchema = z2.object({
|
|
5230
|
+
email: z2.string().email("Invalid email address"),
|
|
5231
|
+
password: z2.string().min(1, "Password is required")
|
|
5232
|
+
});
|
|
5233
|
+
var registerSchema = z2.object({
|
|
5234
|
+
email: z2.string().email("Invalid email address"),
|
|
5235
|
+
password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
|
|
5236
|
+
name: z2.string().min(2, "Name must be at least 2 characters").optional()
|
|
5237
|
+
});
|
|
5238
|
+
var refreshTokenSchema = z2.object({
|
|
5239
|
+
refreshToken: z2.string().min(1, "Refresh token is required")
|
|
5240
|
+
});
|
|
5241
|
+
var passwordResetRequestSchema = z2.object({
|
|
5242
|
+
email: z2.string().email("Invalid email address")
|
|
5243
|
+
});
|
|
5244
|
+
var passwordResetConfirmSchema = z2.object({
|
|
5245
|
+
token: z2.string().min(1, "Token is required"),
|
|
5246
|
+
password: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
|
|
5247
|
+
});
|
|
5248
|
+
var changePasswordSchema = z2.object({
|
|
5249
|
+
currentPassword: z2.string().min(1, "Current password is required"),
|
|
5250
|
+
newPassword: z2.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number")
|
|
5251
|
+
});
|
|
5252
|
+
|
|
5253
|
+
// src/utils/response.ts
|
|
5254
|
+
init_esm_shims();
|
|
5255
|
+
function success2(reply, data, statusCode = 200) {
|
|
5256
|
+
const response = {
|
|
5257
|
+
success: true,
|
|
5258
|
+
data
|
|
5259
|
+
};
|
|
5260
|
+
return reply.status(statusCode).send(response);
|
|
5261
|
+
}
|
|
5262
|
+
function created(reply, data) {
|
|
5263
|
+
return success2(reply, data, 201);
|
|
5264
|
+
}
|
|
5265
|
+
function noContent(reply) {
|
|
5266
|
+
return reply.status(204).send();
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
// src/modules/validation/validator.ts
|
|
5270
|
+
init_esm_shims();
|
|
5271
|
+
import { z as z3 } from "zod";
|
|
5272
|
+
function validateBody(schema, data) {
|
|
5273
|
+
const result = schema.safeParse(data);
|
|
5274
|
+
if (!result.success) {
|
|
5275
|
+
throw new ValidationError(formatZodErrors(result.error));
|
|
5276
|
+
}
|
|
5277
|
+
return result.data;
|
|
5278
|
+
}
|
|
5279
|
+
function validateQuery(schema, data) {
|
|
5280
|
+
const result = schema.safeParse(data);
|
|
5281
|
+
if (!result.success) {
|
|
5282
|
+
throw new ValidationError(formatZodErrors(result.error));
|
|
5283
|
+
}
|
|
5284
|
+
return result.data;
|
|
5285
|
+
}
|
|
5286
|
+
function formatZodErrors(error2) {
|
|
5287
|
+
const errors = {};
|
|
5288
|
+
for (const issue of error2.issues) {
|
|
5289
|
+
const path12 = issue.path.join(".") || "root";
|
|
5290
|
+
if (!errors[path12]) {
|
|
5291
|
+
errors[path12] = [];
|
|
5292
|
+
}
|
|
5293
|
+
errors[path12].push(issue.message);
|
|
5294
|
+
}
|
|
5295
|
+
return errors;
|
|
5296
|
+
}
|
|
5297
|
+
var idParamSchema = z3.object({
|
|
5298
|
+
id: z3.string().uuid("Invalid ID format")
|
|
5299
|
+
});
|
|
5300
|
+
var paginationSchema = z3.object({
|
|
5301
|
+
page: z3.string().transform(Number).optional().default("1"),
|
|
5302
|
+
limit: z3.string().transform(Number).optional().default("20"),
|
|
5303
|
+
sortBy: z3.string().optional(),
|
|
5304
|
+
sortOrder: z3.enum(["asc", "desc"]).optional().default("asc")
|
|
5305
|
+
});
|
|
5306
|
+
var searchSchema = z3.object({
|
|
5307
|
+
q: z3.string().min(1, "Search query is required").optional(),
|
|
5308
|
+
search: z3.string().min(1).optional()
|
|
5309
|
+
});
|
|
5310
|
+
var emailSchema = z3.string().email("Invalid email address");
|
|
5311
|
+
var passwordSchema = z3.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number").regex(/[^A-Za-z0-9]/, "Password must contain at least one special character");
|
|
5312
|
+
var urlSchema = z3.string().url("Invalid URL format");
|
|
5313
|
+
var phoneSchema = z3.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format");
|
|
5314
|
+
var dateSchema = z3.coerce.date();
|
|
5315
|
+
var futureDateSchema = z3.coerce.date().refine((date) => date > /* @__PURE__ */ new Date(), "Date must be in the future");
|
|
5316
|
+
var pastDateSchema = z3.coerce.date().refine((date) => date < /* @__PURE__ */ new Date(), "Date must be in the past");
|
|
5317
|
+
|
|
5318
|
+
// src/modules/auth/auth.controller.ts
|
|
5319
|
+
var AuthController = class {
|
|
5320
|
+
constructor(authService, userService) {
|
|
5321
|
+
this.authService = authService;
|
|
5322
|
+
this.userService = userService;
|
|
5323
|
+
}
|
|
5324
|
+
async register(request, reply) {
|
|
5325
|
+
const data = validateBody(registerSchema, request.body);
|
|
5326
|
+
const existingUser = await this.userService.findByEmail(data.email);
|
|
5327
|
+
if (existingUser) {
|
|
5328
|
+
throw new BadRequestError("Email already registered");
|
|
5329
|
+
}
|
|
5330
|
+
const hashedPassword = await this.authService.hashPassword(data.password);
|
|
5331
|
+
const user = await this.userService.create({
|
|
5332
|
+
email: data.email,
|
|
5333
|
+
password: hashedPassword,
|
|
5334
|
+
name: data.name
|
|
5335
|
+
});
|
|
5336
|
+
const tokens = this.authService.generateTokenPair({
|
|
5337
|
+
id: user.id,
|
|
5338
|
+
email: user.email,
|
|
5339
|
+
role: user.role
|
|
5340
|
+
});
|
|
5341
|
+
created(reply, {
|
|
5342
|
+
user: {
|
|
5343
|
+
id: user.id,
|
|
5344
|
+
email: user.email,
|
|
5345
|
+
name: user.name,
|
|
5346
|
+
role: user.role
|
|
5347
|
+
},
|
|
5348
|
+
...tokens
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
async login(request, reply) {
|
|
5352
|
+
const data = validateBody(loginSchema, request.body);
|
|
5353
|
+
const user = await this.userService.findByEmail(data.email);
|
|
5354
|
+
if (!user) {
|
|
5355
|
+
throw new UnauthorizedError("Invalid credentials");
|
|
5356
|
+
}
|
|
5357
|
+
if (user.status !== "active") {
|
|
5358
|
+
throw new UnauthorizedError("Account is not active");
|
|
5359
|
+
}
|
|
5360
|
+
const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
|
|
5361
|
+
if (!isValidPassword) {
|
|
5362
|
+
throw new UnauthorizedError("Invalid credentials");
|
|
5363
|
+
}
|
|
5364
|
+
await this.userService.updateLastLogin(user.id);
|
|
5365
|
+
const tokens = this.authService.generateTokenPair({
|
|
5366
|
+
id: user.id,
|
|
5367
|
+
email: user.email,
|
|
5368
|
+
role: user.role
|
|
5369
|
+
});
|
|
5370
|
+
success2(reply, {
|
|
5371
|
+
user: {
|
|
5372
|
+
id: user.id,
|
|
5373
|
+
email: user.email,
|
|
5374
|
+
name: user.name,
|
|
5375
|
+
role: user.role
|
|
5376
|
+
},
|
|
5377
|
+
...tokens
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5380
|
+
async refresh(request, reply) {
|
|
5381
|
+
const data = validateBody(refreshTokenSchema, request.body);
|
|
5382
|
+
const payload = await this.authService.verifyRefreshToken(data.refreshToken);
|
|
5383
|
+
const user = await this.userService.findById(payload.sub);
|
|
5384
|
+
if (!user || user.status !== "active") {
|
|
5385
|
+
throw new UnauthorizedError("User not found or inactive");
|
|
5386
|
+
}
|
|
5387
|
+
await this.authService.blacklistToken(data.refreshToken);
|
|
5388
|
+
const tokens = this.authService.generateTokenPair({
|
|
5389
|
+
id: user.id,
|
|
5390
|
+
email: user.email,
|
|
5391
|
+
role: user.role
|
|
5392
|
+
});
|
|
5393
|
+
success2(reply, tokens);
|
|
5394
|
+
}
|
|
5395
|
+
async logout(request, reply) {
|
|
5396
|
+
const authHeader = request.headers.authorization;
|
|
5397
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
5398
|
+
const token = authHeader.substring(7);
|
|
5399
|
+
await this.authService.blacklistToken(token);
|
|
5400
|
+
}
|
|
5401
|
+
success2(reply, { message: "Logged out successfully" });
|
|
5402
|
+
}
|
|
5403
|
+
async me(request, reply) {
|
|
5404
|
+
const authRequest = request;
|
|
5405
|
+
const user = await this.userService.findById(authRequest.user.id);
|
|
5406
|
+
if (!user) {
|
|
5407
|
+
throw new UnauthorizedError("User not found");
|
|
5408
|
+
}
|
|
5409
|
+
success2(reply, {
|
|
5410
|
+
id: user.id,
|
|
5411
|
+
email: user.email,
|
|
5412
|
+
name: user.name,
|
|
5413
|
+
role: user.role,
|
|
5414
|
+
status: user.status,
|
|
5415
|
+
createdAt: user.createdAt
|
|
5416
|
+
});
|
|
5417
|
+
}
|
|
5418
|
+
async changePassword(request, reply) {
|
|
5419
|
+
const authRequest = request;
|
|
5420
|
+
const data = validateBody(changePasswordSchema, request.body);
|
|
5421
|
+
const user = await this.userService.findById(authRequest.user.id);
|
|
5422
|
+
if (!user) {
|
|
5423
|
+
throw new UnauthorizedError("User not found");
|
|
5424
|
+
}
|
|
5425
|
+
const isValidPassword = await this.authService.verifyPassword(
|
|
5426
|
+
data.currentPassword,
|
|
5427
|
+
user.password
|
|
5428
|
+
);
|
|
5429
|
+
if (!isValidPassword) {
|
|
5430
|
+
throw new BadRequestError("Current password is incorrect");
|
|
5431
|
+
}
|
|
5432
|
+
const hashedPassword = await this.authService.hashPassword(data.newPassword);
|
|
5433
|
+
await this.userService.updatePassword(user.id, hashedPassword);
|
|
5434
|
+
success2(reply, { message: "Password changed successfully" });
|
|
5435
|
+
}
|
|
5436
|
+
};
|
|
5437
|
+
function createAuthController(authService, userService) {
|
|
5438
|
+
return new AuthController(authService, userService);
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
// src/modules/auth/auth.routes.ts
|
|
5442
|
+
init_esm_shims();
|
|
5443
|
+
|
|
5444
|
+
// src/modules/auth/auth.middleware.ts
|
|
5445
|
+
init_esm_shims();
|
|
5446
|
+
function createAuthMiddleware(authService) {
|
|
5447
|
+
return async function authenticate(request, _reply) {
|
|
5448
|
+
const authHeader = request.headers.authorization;
|
|
5449
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
5450
|
+
throw new UnauthorizedError("Missing or invalid authorization header");
|
|
5451
|
+
}
|
|
5452
|
+
const token = authHeader.substring(7);
|
|
5453
|
+
const payload = await authService.verifyAccessToken(token);
|
|
5454
|
+
request.user = {
|
|
5455
|
+
id: payload.sub,
|
|
5456
|
+
email: payload.email,
|
|
5457
|
+
role: payload.role
|
|
5458
|
+
};
|
|
5459
|
+
};
|
|
5460
|
+
}
|
|
5461
|
+
function createRoleMiddleware(allowedRoles) {
|
|
5462
|
+
return async function authorize(request, _reply) {
|
|
5463
|
+
const user = request.user;
|
|
5464
|
+
if (!user) {
|
|
5465
|
+
throw new UnauthorizedError("Authentication required");
|
|
5466
|
+
}
|
|
5467
|
+
if (!allowedRoles.includes(user.role)) {
|
|
5468
|
+
throw new ForbiddenError("Insufficient permissions");
|
|
5469
|
+
}
|
|
5470
|
+
};
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5473
|
+
// src/modules/auth/auth.routes.ts
|
|
5474
|
+
function registerAuthRoutes(app, controller, authService) {
|
|
5475
|
+
const authenticate = createAuthMiddleware(authService);
|
|
5476
|
+
app.post("/auth/register", controller.register.bind(controller));
|
|
5477
|
+
app.post("/auth/login", controller.login.bind(controller));
|
|
5478
|
+
app.post("/auth/refresh", controller.refresh.bind(controller));
|
|
5479
|
+
app.post("/auth/logout", { preHandler: [authenticate] }, controller.logout.bind(controller));
|
|
5480
|
+
app.get("/auth/me", { preHandler: [authenticate] }, controller.me.bind(controller));
|
|
5481
|
+
app.post(
|
|
5482
|
+
"/auth/change-password",
|
|
5483
|
+
{ preHandler: [authenticate] },
|
|
5484
|
+
controller.changePassword.bind(controller)
|
|
5485
|
+
);
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
// src/modules/user/user.service.ts
|
|
5489
|
+
init_esm_shims();
|
|
5490
|
+
|
|
5491
|
+
// src/modules/user/user.repository.ts
|
|
5492
|
+
init_esm_shims();
|
|
5493
|
+
|
|
5494
|
+
// src/database/prisma.ts
|
|
5495
|
+
init_esm_shims();
|
|
5496
|
+
import { PrismaClient } from "@prisma/client";
|
|
5497
|
+
var prismaClientSingleton = () => {
|
|
5498
|
+
return new PrismaClient({
|
|
5499
|
+
log: isProduction() ? ["error"] : ["query", "info", "warn", "error"],
|
|
5500
|
+
errorFormat: isProduction() ? "minimal" : "pretty"
|
|
5501
|
+
});
|
|
5502
|
+
};
|
|
5503
|
+
var prisma = globalThis.__prisma ?? prismaClientSingleton();
|
|
5504
|
+
if (!isProduction()) {
|
|
5505
|
+
globalThis.__prisma = prisma;
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
// src/utils/pagination.ts
|
|
5509
|
+
init_esm_shims();
|
|
5510
|
+
var DEFAULT_PAGE = 1;
|
|
5511
|
+
var DEFAULT_LIMIT = 20;
|
|
5512
|
+
var MAX_LIMIT = 100;
|
|
5513
|
+
function parsePaginationParams(query) {
|
|
5514
|
+
const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
|
|
5515
|
+
const limit = Math.min(
|
|
5516
|
+
MAX_LIMIT,
|
|
5517
|
+
Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
|
|
5518
|
+
);
|
|
5519
|
+
const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
|
|
5520
|
+
const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
|
|
5521
|
+
return { page, limit, sortBy, sortOrder };
|
|
5522
|
+
}
|
|
5523
|
+
function createPaginatedResult(data, total, params) {
|
|
5524
|
+
const totalPages = Math.ceil(total / params.limit);
|
|
5525
|
+
return {
|
|
5526
|
+
data,
|
|
5527
|
+
meta: {
|
|
5528
|
+
total,
|
|
5529
|
+
page: params.page,
|
|
5530
|
+
limit: params.limit,
|
|
5531
|
+
totalPages,
|
|
5532
|
+
hasNextPage: params.page < totalPages,
|
|
5533
|
+
hasPrevPage: params.page > 1
|
|
5534
|
+
}
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
function getSkip(params) {
|
|
5538
|
+
return (params.page - 1) * params.limit;
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5541
|
+
// src/modules/user/user.repository.ts
|
|
5542
|
+
import { UserRole, UserStatus } from "@prisma/client";
|
|
5543
|
+
var UserRepository = class {
|
|
5544
|
+
/**
|
|
5545
|
+
* Find user by ID
|
|
5546
|
+
*/
|
|
5547
|
+
async findById(id) {
|
|
5548
|
+
const user = await prisma.user.findUnique({
|
|
5549
|
+
where: { id }
|
|
5550
|
+
});
|
|
5551
|
+
if (!user) return null;
|
|
5552
|
+
return this.mapPrismaUserToUser(user);
|
|
5553
|
+
}
|
|
5554
|
+
/**
|
|
5555
|
+
* Find user by email (case-insensitive)
|
|
5556
|
+
*/
|
|
5557
|
+
async findByEmail(email) {
|
|
5558
|
+
const user = await prisma.user.findUnique({
|
|
5559
|
+
where: { email: email.toLowerCase() }
|
|
5560
|
+
});
|
|
5561
|
+
if (!user) return null;
|
|
5562
|
+
return this.mapPrismaUserToUser(user);
|
|
5563
|
+
}
|
|
5564
|
+
/**
|
|
5565
|
+
* Find multiple users with pagination and filters
|
|
5566
|
+
*/
|
|
5567
|
+
async findMany(params, filters) {
|
|
5568
|
+
const where = this.buildWhereClause(filters);
|
|
5569
|
+
const orderBy = this.buildOrderBy(params);
|
|
5570
|
+
const [data, total] = await Promise.all([
|
|
5571
|
+
prisma.user.findMany({
|
|
5572
|
+
where,
|
|
5573
|
+
orderBy,
|
|
5574
|
+
skip: getSkip(params),
|
|
5575
|
+
take: params.limit
|
|
5576
|
+
}),
|
|
5577
|
+
prisma.user.count({ where })
|
|
5578
|
+
]);
|
|
5579
|
+
const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
|
|
5580
|
+
return createPaginatedResult(mappedUsers, total, params);
|
|
5581
|
+
}
|
|
5582
|
+
/**
|
|
5583
|
+
* Create a new user
|
|
5584
|
+
*/
|
|
5585
|
+
async create(data) {
|
|
5586
|
+
const user = await prisma.user.create({
|
|
5587
|
+
data: {
|
|
5588
|
+
email: data.email.toLowerCase(),
|
|
5589
|
+
password: data.password,
|
|
5590
|
+
name: data.name,
|
|
5591
|
+
role: this.mapRoleToEnum(data.role || "user"),
|
|
5592
|
+
status: UserStatus.ACTIVE,
|
|
5593
|
+
emailVerified: false
|
|
5594
|
+
}
|
|
5595
|
+
});
|
|
5596
|
+
return this.mapPrismaUserToUser(user);
|
|
5597
|
+
}
|
|
5598
|
+
/**
|
|
5599
|
+
* Update user data
|
|
5600
|
+
*/
|
|
5601
|
+
async update(id, data) {
|
|
5602
|
+
try {
|
|
5603
|
+
const user = await prisma.user.update({
|
|
5604
|
+
where: { id },
|
|
5605
|
+
data: {
|
|
5606
|
+
...data.email && { email: data.email.toLowerCase() },
|
|
5607
|
+
...data.name !== void 0 && { name: data.name },
|
|
5608
|
+
...data.role && { role: this.mapRoleToEnum(data.role) },
|
|
5609
|
+
...data.status && { status: this.mapStatusToEnum(data.status) },
|
|
5610
|
+
...data.emailVerified !== void 0 && { emailVerified: data.emailVerified },
|
|
5611
|
+
...data.metadata && { metadata: data.metadata }
|
|
5612
|
+
}
|
|
5613
|
+
});
|
|
5614
|
+
return this.mapPrismaUserToUser(user);
|
|
5615
|
+
} catch {
|
|
5616
|
+
return null;
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
/**
|
|
5620
|
+
* Update user password
|
|
5621
|
+
*/
|
|
5622
|
+
async updatePassword(id, password) {
|
|
5623
|
+
try {
|
|
5624
|
+
const user = await prisma.user.update({
|
|
5625
|
+
where: { id },
|
|
5626
|
+
data: { password }
|
|
5627
|
+
});
|
|
5628
|
+
return this.mapPrismaUserToUser(user);
|
|
5629
|
+
} catch {
|
|
5630
|
+
return null;
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
/**
|
|
5634
|
+
* Update last login timestamp
|
|
5635
|
+
*/
|
|
5636
|
+
async updateLastLogin(id) {
|
|
5637
|
+
try {
|
|
5638
|
+
const user = await prisma.user.update({
|
|
5639
|
+
where: { id },
|
|
5640
|
+
data: { lastLoginAt: /* @__PURE__ */ new Date() }
|
|
5641
|
+
});
|
|
5642
|
+
return this.mapPrismaUserToUser(user);
|
|
5643
|
+
} catch {
|
|
5644
|
+
return null;
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
/**
|
|
5648
|
+
* Delete user by ID
|
|
5649
|
+
*/
|
|
5650
|
+
async delete(id) {
|
|
5651
|
+
try {
|
|
5652
|
+
await prisma.user.delete({
|
|
5653
|
+
where: { id }
|
|
5654
|
+
});
|
|
5655
|
+
return true;
|
|
5656
|
+
} catch {
|
|
5657
|
+
return false;
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
/**
|
|
5661
|
+
* Count users with optional filters
|
|
5662
|
+
*/
|
|
5663
|
+
async count(filters) {
|
|
5664
|
+
const where = this.buildWhereClause(filters);
|
|
5665
|
+
return prisma.user.count({ where });
|
|
5666
|
+
}
|
|
5667
|
+
/**
|
|
5668
|
+
* Helper to clear all users (for testing only)
|
|
5669
|
+
* WARNING: This deletes all users from the database
|
|
5670
|
+
*/
|
|
5671
|
+
async clear() {
|
|
5672
|
+
await prisma.user.deleteMany();
|
|
5673
|
+
}
|
|
5674
|
+
/**
|
|
5675
|
+
* Build Prisma where clause from filters
|
|
5676
|
+
*/
|
|
5677
|
+
buildWhereClause(filters) {
|
|
5678
|
+
if (!filters) return {};
|
|
5679
|
+
return {
|
|
5680
|
+
...filters.status && { status: this.mapStatusToEnum(filters.status) },
|
|
5681
|
+
...filters.role && { role: this.mapRoleToEnum(filters.role) },
|
|
5682
|
+
...filters.emailVerified !== void 0 && { emailVerified: filters.emailVerified },
|
|
5683
|
+
...filters.search && {
|
|
5684
|
+
OR: [
|
|
5685
|
+
{ email: { contains: filters.search, mode: "insensitive" } },
|
|
5686
|
+
{ name: { contains: filters.search, mode: "insensitive" } }
|
|
5687
|
+
]
|
|
5688
|
+
}
|
|
5689
|
+
};
|
|
5690
|
+
}
|
|
5691
|
+
/**
|
|
5692
|
+
* Build Prisma orderBy clause from pagination params
|
|
5693
|
+
*/
|
|
5694
|
+
buildOrderBy(params) {
|
|
5695
|
+
if (!params.sortBy) {
|
|
5696
|
+
return { createdAt: "desc" };
|
|
5697
|
+
}
|
|
5698
|
+
return {
|
|
5699
|
+
[params.sortBy]: params.sortOrder || "asc"
|
|
5700
|
+
};
|
|
5701
|
+
}
|
|
5702
|
+
/**
|
|
5703
|
+
* Map Prisma User to application User type
|
|
5704
|
+
*/
|
|
5705
|
+
mapPrismaUserToUser(prismaUser) {
|
|
5706
|
+
return {
|
|
5707
|
+
id: prismaUser.id,
|
|
5708
|
+
email: prismaUser.email,
|
|
5709
|
+
password: prismaUser.password,
|
|
5710
|
+
name: prismaUser.name ?? void 0,
|
|
5711
|
+
role: this.mapEnumToRole(prismaUser.role),
|
|
5712
|
+
status: this.mapEnumToStatus(prismaUser.status),
|
|
5713
|
+
emailVerified: prismaUser.emailVerified,
|
|
5714
|
+
lastLoginAt: prismaUser.lastLoginAt ?? void 0,
|
|
5715
|
+
metadata: prismaUser.metadata,
|
|
5716
|
+
createdAt: prismaUser.createdAt,
|
|
5717
|
+
updatedAt: prismaUser.updatedAt
|
|
5718
|
+
};
|
|
5719
|
+
}
|
|
5720
|
+
/**
|
|
5721
|
+
* Map application role to Prisma enum
|
|
5722
|
+
*/
|
|
5723
|
+
mapRoleToEnum(role) {
|
|
5724
|
+
const roleMap = {
|
|
5725
|
+
user: UserRole.USER,
|
|
5726
|
+
moderator: UserRole.MODERATOR,
|
|
5727
|
+
admin: UserRole.ADMIN,
|
|
5728
|
+
super_admin: UserRole.SUPER_ADMIN
|
|
5729
|
+
};
|
|
5730
|
+
return roleMap[role] || UserRole.USER;
|
|
5731
|
+
}
|
|
5732
|
+
/**
|
|
5733
|
+
* Map Prisma enum to application role
|
|
5734
|
+
*/
|
|
5735
|
+
mapEnumToRole(role) {
|
|
5736
|
+
const roleMap = {
|
|
5737
|
+
[UserRole.USER]: "user",
|
|
5738
|
+
[UserRole.MODERATOR]: "moderator",
|
|
5739
|
+
[UserRole.ADMIN]: "admin",
|
|
5740
|
+
[UserRole.SUPER_ADMIN]: "super_admin"
|
|
5741
|
+
};
|
|
5742
|
+
return roleMap[role];
|
|
5743
|
+
}
|
|
5744
|
+
/**
|
|
5745
|
+
* Map application status to Prisma enum
|
|
5746
|
+
*/
|
|
5747
|
+
mapStatusToEnum(status) {
|
|
5748
|
+
const statusMap = {
|
|
5749
|
+
active: UserStatus.ACTIVE,
|
|
5750
|
+
inactive: UserStatus.INACTIVE,
|
|
5751
|
+
suspended: UserStatus.SUSPENDED,
|
|
5752
|
+
banned: UserStatus.BANNED
|
|
5753
|
+
};
|
|
5754
|
+
return statusMap[status] || UserStatus.ACTIVE;
|
|
5755
|
+
}
|
|
5756
|
+
/**
|
|
5757
|
+
* Map Prisma enum to application status
|
|
5758
|
+
*/
|
|
5759
|
+
mapEnumToStatus(status) {
|
|
5760
|
+
const statusMap = {
|
|
5761
|
+
[UserStatus.ACTIVE]: "active",
|
|
5762
|
+
[UserStatus.INACTIVE]: "inactive",
|
|
5763
|
+
[UserStatus.SUSPENDED]: "suspended",
|
|
5764
|
+
[UserStatus.BANNED]: "banned"
|
|
5765
|
+
};
|
|
5766
|
+
return statusMap[status];
|
|
5767
|
+
}
|
|
5768
|
+
};
|
|
5769
|
+
function createUserRepository() {
|
|
5770
|
+
return new UserRepository();
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
// src/modules/user/types.ts
|
|
5774
|
+
init_esm_shims();
|
|
5775
|
+
var DEFAULT_ROLE_PERMISSIONS = {
|
|
5776
|
+
user: ["profile:read", "profile:update"],
|
|
5777
|
+
moderator: [
|
|
5778
|
+
"profile:read",
|
|
5779
|
+
"profile:update",
|
|
5780
|
+
"users:read",
|
|
5781
|
+
"content:read",
|
|
5782
|
+
"content:update",
|
|
5783
|
+
"content:delete"
|
|
5784
|
+
],
|
|
5785
|
+
admin: [
|
|
5786
|
+
"profile:read",
|
|
5787
|
+
"profile:update",
|
|
5788
|
+
"users:read",
|
|
5789
|
+
"users:update",
|
|
5790
|
+
"users:delete",
|
|
5791
|
+
"content:manage",
|
|
5792
|
+
"settings:read"
|
|
5793
|
+
],
|
|
5794
|
+
super_admin: ["*:manage"]
|
|
5795
|
+
// All permissions
|
|
5796
|
+
};
|
|
5797
|
+
|
|
5798
|
+
// src/modules/user/user.service.ts
|
|
5799
|
+
var UserService = class {
|
|
5800
|
+
constructor(repository) {
|
|
5801
|
+
this.repository = repository;
|
|
5802
|
+
}
|
|
5803
|
+
async findById(id) {
|
|
5804
|
+
return this.repository.findById(id);
|
|
5805
|
+
}
|
|
5806
|
+
async findByEmail(email) {
|
|
5807
|
+
return this.repository.findByEmail(email);
|
|
5808
|
+
}
|
|
5809
|
+
async findMany(params, filters) {
|
|
5810
|
+
const result = await this.repository.findMany(params, filters);
|
|
5811
|
+
return {
|
|
5812
|
+
...result,
|
|
5813
|
+
data: result.data.map(({ password: _password, ...user }) => user)
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5816
|
+
async create(data) {
|
|
5817
|
+
const existing = await this.repository.findByEmail(data.email);
|
|
5818
|
+
if (existing) {
|
|
5819
|
+
throw new ConflictError("User with this email already exists");
|
|
5820
|
+
}
|
|
5821
|
+
const user = await this.repository.create(data);
|
|
5822
|
+
logger.info({ userId: user.id, email: user.email }, "User created");
|
|
5823
|
+
return user;
|
|
5824
|
+
}
|
|
5825
|
+
async update(id, data) {
|
|
5826
|
+
const user = await this.repository.findById(id);
|
|
5827
|
+
if (!user) {
|
|
5828
|
+
throw new NotFoundError("User");
|
|
5829
|
+
}
|
|
5830
|
+
if (data.email && data.email !== user.email) {
|
|
5831
|
+
const existing = await this.repository.findByEmail(data.email);
|
|
5832
|
+
if (existing) {
|
|
5833
|
+
throw new ConflictError("Email already in use");
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
const updatedUser = await this.repository.update(id, data);
|
|
5837
|
+
if (!updatedUser) {
|
|
5838
|
+
throw new NotFoundError("User");
|
|
5839
|
+
}
|
|
5840
|
+
logger.info({ userId: id }, "User updated");
|
|
5841
|
+
return updatedUser;
|
|
5842
|
+
}
|
|
5843
|
+
async updatePassword(id, hashedPassword) {
|
|
5844
|
+
const user = await this.repository.updatePassword(id, hashedPassword);
|
|
5845
|
+
if (!user) {
|
|
5846
|
+
throw new NotFoundError("User");
|
|
5847
|
+
}
|
|
5848
|
+
logger.info({ userId: id }, "User password updated");
|
|
5849
|
+
return user;
|
|
5850
|
+
}
|
|
5851
|
+
async updateLastLogin(id) {
|
|
5852
|
+
const user = await this.repository.updateLastLogin(id);
|
|
5853
|
+
if (!user) {
|
|
5854
|
+
throw new NotFoundError("User");
|
|
5855
|
+
}
|
|
5856
|
+
return user;
|
|
5857
|
+
}
|
|
5858
|
+
async delete(id) {
|
|
5859
|
+
const user = await this.repository.findById(id);
|
|
5860
|
+
if (!user) {
|
|
5861
|
+
throw new NotFoundError("User");
|
|
5862
|
+
}
|
|
5863
|
+
await this.repository.delete(id);
|
|
5864
|
+
logger.info({ userId: id }, "User deleted");
|
|
5865
|
+
}
|
|
5866
|
+
async suspend(id) {
|
|
5867
|
+
return this.update(id, { status: "suspended" });
|
|
5868
|
+
}
|
|
5869
|
+
async ban(id) {
|
|
5870
|
+
return this.update(id, { status: "banned" });
|
|
5871
|
+
}
|
|
5872
|
+
async activate(id) {
|
|
5873
|
+
return this.update(id, { status: "active" });
|
|
5874
|
+
}
|
|
5875
|
+
async verifyEmail(id) {
|
|
5876
|
+
return this.update(id, { emailVerified: true });
|
|
5877
|
+
}
|
|
5878
|
+
async changeRole(id, role) {
|
|
5879
|
+
return this.update(id, { role });
|
|
5880
|
+
}
|
|
5881
|
+
// RBAC helpers
|
|
5882
|
+
hasPermission(role, permission) {
|
|
5883
|
+
const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
|
|
5884
|
+
if (permissions.includes("*:manage")) {
|
|
5885
|
+
return true;
|
|
5886
|
+
}
|
|
5887
|
+
if (permissions.includes(permission)) {
|
|
5888
|
+
return true;
|
|
5889
|
+
}
|
|
5890
|
+
const [resource] = permission.split(":");
|
|
5891
|
+
const managePermission = `${resource}:manage`;
|
|
5892
|
+
if (permissions.includes(managePermission)) {
|
|
5893
|
+
return true;
|
|
5894
|
+
}
|
|
5895
|
+
return false;
|
|
5896
|
+
}
|
|
5897
|
+
getPermissions(role) {
|
|
5898
|
+
return DEFAULT_ROLE_PERMISSIONS[role] || [];
|
|
5899
|
+
}
|
|
5900
|
+
};
|
|
5901
|
+
function createUserService(repository) {
|
|
5902
|
+
return new UserService(repository || createUserRepository());
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
// src/modules/auth/types.ts
|
|
5906
|
+
init_esm_shims();
|
|
5907
|
+
|
|
5908
|
+
// src/modules/auth/index.ts
|
|
5909
|
+
async function registerAuthModule(app) {
|
|
5910
|
+
await app.register(jwt, {
|
|
5911
|
+
secret: config.jwt.secret,
|
|
5912
|
+
sign: {
|
|
5913
|
+
algorithm: "HS256"
|
|
5914
|
+
}
|
|
5915
|
+
});
|
|
5916
|
+
await app.register(cookie, {
|
|
5917
|
+
secret: config.jwt.secret,
|
|
5918
|
+
hook: "onRequest"
|
|
5919
|
+
});
|
|
5920
|
+
const authService = createAuthService(app);
|
|
5921
|
+
const userService = createUserService();
|
|
5922
|
+
const authController = createAuthController(authService, userService);
|
|
5923
|
+
registerAuthRoutes(app, authController, authService);
|
|
5924
|
+
logger.info("Auth module registered");
|
|
5925
|
+
}
|
|
5926
|
+
|
|
5927
|
+
// src/modules/user/index.ts
|
|
5928
|
+
init_esm_shims();
|
|
5929
|
+
|
|
5930
|
+
// src/modules/user/user.controller.ts
|
|
5931
|
+
init_esm_shims();
|
|
5932
|
+
|
|
5933
|
+
// src/modules/user/schemas.ts
|
|
5934
|
+
init_esm_shims();
|
|
5935
|
+
import { z as z4 } from "zod";
|
|
5936
|
+
var userStatusEnum = z4.enum(["active", "inactive", "suspended", "banned"]);
|
|
5937
|
+
var userRoleEnum = z4.enum(["user", "admin", "moderator", "super_admin"]);
|
|
5938
|
+
var createUserSchema = z4.object({
|
|
5939
|
+
email: z4.string().email("Invalid email address"),
|
|
5940
|
+
password: z4.string().min(8, "Password must be at least 8 characters").regex(/[A-Z]/, "Password must contain at least one uppercase letter").regex(/[a-z]/, "Password must contain at least one lowercase letter").regex(/[0-9]/, "Password must contain at least one number"),
|
|
5941
|
+
name: z4.string().min(2, "Name must be at least 2 characters").optional(),
|
|
5942
|
+
role: userRoleEnum.optional().default("user")
|
|
5943
|
+
});
|
|
5944
|
+
var updateUserSchema = z4.object({
|
|
5945
|
+
email: z4.string().email("Invalid email address").optional(),
|
|
5946
|
+
name: z4.string().min(2, "Name must be at least 2 characters").optional(),
|
|
5947
|
+
role: userRoleEnum.optional(),
|
|
5948
|
+
status: userStatusEnum.optional(),
|
|
5949
|
+
emailVerified: z4.boolean().optional(),
|
|
5950
|
+
metadata: z4.record(z4.unknown()).optional()
|
|
5951
|
+
});
|
|
5952
|
+
var updateProfileSchema = z4.object({
|
|
5953
|
+
name: z4.string().min(2, "Name must be at least 2 characters").optional(),
|
|
5954
|
+
metadata: z4.record(z4.unknown()).optional()
|
|
5955
|
+
});
|
|
5956
|
+
var userQuerySchema = z4.object({
|
|
5957
|
+
page: z4.string().transform(Number).optional(),
|
|
5958
|
+
limit: z4.string().transform(Number).optional(),
|
|
5959
|
+
sortBy: z4.string().optional(),
|
|
5960
|
+
sortOrder: z4.enum(["asc", "desc"]).optional(),
|
|
5961
|
+
status: userStatusEnum.optional(),
|
|
5962
|
+
role: userRoleEnum.optional(),
|
|
5963
|
+
search: z4.string().optional(),
|
|
5964
|
+
emailVerified: z4.string().transform((val) => val === "true").optional()
|
|
5965
|
+
});
|
|
5966
|
+
|
|
5967
|
+
// src/modules/user/user.controller.ts
|
|
5968
|
+
function omitPassword(user) {
|
|
5969
|
+
const { password, ...userData } = user;
|
|
5970
|
+
void password;
|
|
5971
|
+
return userData;
|
|
5972
|
+
}
|
|
5973
|
+
var UserController = class {
|
|
5974
|
+
constructor(userService) {
|
|
5975
|
+
this.userService = userService;
|
|
5976
|
+
}
|
|
5977
|
+
async list(request, reply) {
|
|
5978
|
+
const query = validateQuery(userQuerySchema, request.query);
|
|
5979
|
+
const pagination = parsePaginationParams(query);
|
|
5980
|
+
const filters = {
|
|
5981
|
+
status: query.status,
|
|
5982
|
+
role: query.role,
|
|
5983
|
+
search: query.search,
|
|
5984
|
+
emailVerified: query.emailVerified
|
|
5985
|
+
};
|
|
5986
|
+
const result = await this.userService.findMany(pagination, filters);
|
|
5987
|
+
success2(reply, result);
|
|
5988
|
+
}
|
|
5989
|
+
async getById(request, reply) {
|
|
5990
|
+
const user = await this.userService.findById(request.params.id);
|
|
5991
|
+
if (!user) {
|
|
5992
|
+
return reply.status(404).send({
|
|
5993
|
+
success: false,
|
|
5994
|
+
message: "User not found"
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
success2(reply, omitPassword(user));
|
|
5998
|
+
}
|
|
5999
|
+
async update(request, reply) {
|
|
6000
|
+
const data = validateBody(updateUserSchema, request.body);
|
|
6001
|
+
const user = await this.userService.update(request.params.id, data);
|
|
6002
|
+
success2(reply, omitPassword(user));
|
|
6003
|
+
}
|
|
6004
|
+
async delete(request, reply) {
|
|
6005
|
+
const authRequest = request;
|
|
6006
|
+
if (authRequest.user.id === request.params.id) {
|
|
6007
|
+
throw new ForbiddenError("Cannot delete your own account");
|
|
6008
|
+
}
|
|
6009
|
+
await this.userService.delete(request.params.id);
|
|
6010
|
+
noContent(reply);
|
|
6011
|
+
}
|
|
6012
|
+
async suspend(request, reply) {
|
|
6013
|
+
const authRequest = request;
|
|
6014
|
+
if (authRequest.user.id === request.params.id) {
|
|
6015
|
+
throw new ForbiddenError("Cannot suspend your own account");
|
|
6016
|
+
}
|
|
6017
|
+
const user = await this.userService.suspend(request.params.id);
|
|
6018
|
+
const userData = omitPassword(user);
|
|
6019
|
+
success2(reply, userData);
|
|
6020
|
+
}
|
|
6021
|
+
async ban(request, reply) {
|
|
6022
|
+
const authRequest = request;
|
|
6023
|
+
if (authRequest.user.id === request.params.id) {
|
|
6024
|
+
throw new ForbiddenError("Cannot ban your own account");
|
|
6025
|
+
}
|
|
6026
|
+
const user = await this.userService.ban(request.params.id);
|
|
6027
|
+
const userData = omitPassword(user);
|
|
6028
|
+
success2(reply, userData);
|
|
6029
|
+
}
|
|
6030
|
+
async activate(request, reply) {
|
|
6031
|
+
const user = await this.userService.activate(request.params.id);
|
|
6032
|
+
const userData = omitPassword(user);
|
|
6033
|
+
success2(reply, userData);
|
|
6034
|
+
}
|
|
6035
|
+
// Profile routes (for authenticated user)
|
|
6036
|
+
async getProfile(request, reply) {
|
|
6037
|
+
const authRequest = request;
|
|
6038
|
+
const user = await this.userService.findById(authRequest.user.id);
|
|
6039
|
+
if (!user) {
|
|
6040
|
+
return reply.status(404).send({
|
|
6041
|
+
success: false,
|
|
6042
|
+
message: "User not found"
|
|
6043
|
+
});
|
|
6044
|
+
}
|
|
6045
|
+
const userData = omitPassword(user);
|
|
6046
|
+
success2(reply, userData);
|
|
6047
|
+
}
|
|
6048
|
+
async updateProfile(request, reply) {
|
|
6049
|
+
const authRequest = request;
|
|
6050
|
+
const data = validateBody(updateProfileSchema, request.body);
|
|
6051
|
+
const user = await this.userService.update(authRequest.user.id, data);
|
|
6052
|
+
const userData = omitPassword(user);
|
|
6053
|
+
success2(reply, userData);
|
|
6054
|
+
}
|
|
6055
|
+
};
|
|
6056
|
+
function createUserController(userService) {
|
|
6057
|
+
return new UserController(userService);
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
// src/modules/user/user.routes.ts
|
|
6061
|
+
init_esm_shims();
|
|
6062
|
+
var idParamsSchema = {
|
|
6063
|
+
type: "object",
|
|
6064
|
+
properties: {
|
|
6065
|
+
id: { type: "string" }
|
|
6066
|
+
},
|
|
6067
|
+
required: ["id"]
|
|
6068
|
+
};
|
|
6069
|
+
function registerUserRoutes(app, controller, authService) {
|
|
6070
|
+
const authenticate = createAuthMiddleware(authService);
|
|
6071
|
+
const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
|
|
6072
|
+
const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
|
|
6073
|
+
app.get("/profile", { preHandler: [authenticate] }, controller.getProfile.bind(controller));
|
|
6074
|
+
app.patch("/profile", { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
|
|
6075
|
+
app.get("/users", { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
|
|
6076
|
+
app.get(
|
|
6077
|
+
"/users/:id",
|
|
6078
|
+
{ preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
|
|
6079
|
+
async (request, reply) => {
|
|
6080
|
+
return controller.getById(request, reply);
|
|
6081
|
+
}
|
|
6082
|
+
);
|
|
6083
|
+
app.patch(
|
|
6084
|
+
"/users/:id",
|
|
6085
|
+
{ preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
|
|
6086
|
+
async (request, reply) => {
|
|
6087
|
+
return controller.update(request, reply);
|
|
6088
|
+
}
|
|
6089
|
+
);
|
|
6090
|
+
app.delete(
|
|
6091
|
+
"/users/:id",
|
|
6092
|
+
{ preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
|
|
6093
|
+
async (request, reply) => {
|
|
6094
|
+
return controller.delete(request, reply);
|
|
6095
|
+
}
|
|
6096
|
+
);
|
|
6097
|
+
app.post(
|
|
6098
|
+
"/users/:id/suspend",
|
|
6099
|
+
{ preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
|
|
6100
|
+
async (request, reply) => {
|
|
6101
|
+
return controller.suspend(request, reply);
|
|
6102
|
+
}
|
|
6103
|
+
);
|
|
6104
|
+
app.post(
|
|
6105
|
+
"/users/:id/ban",
|
|
6106
|
+
{ preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
|
|
6107
|
+
async (request, reply) => {
|
|
6108
|
+
return controller.ban(request, reply);
|
|
6109
|
+
}
|
|
6110
|
+
);
|
|
6111
|
+
app.post(
|
|
6112
|
+
"/users/:id/activate",
|
|
6113
|
+
{ preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
|
|
6114
|
+
async (request, reply) => {
|
|
6115
|
+
return controller.activate(request, reply);
|
|
6116
|
+
}
|
|
6117
|
+
);
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6120
|
+
// src/modules/user/index.ts
|
|
6121
|
+
async function registerUserModule(app, authService) {
|
|
6122
|
+
const repository = createUserRepository();
|
|
6123
|
+
const userService = createUserService(repository);
|
|
6124
|
+
const userController = createUserController(userService);
|
|
6125
|
+
registerUserRoutes(app, userController, authService);
|
|
6126
|
+
logger.info("User module registered");
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
// src/cli/utils/docs-generator.ts
|
|
6130
|
+
async function generateDocs(outputPath = "openapi.json", silent = false) {
|
|
6131
|
+
const spinner = silent ? null : ora5("Generating OpenAPI documentation...").start();
|
|
6132
|
+
try {
|
|
6133
|
+
const server = createServer({
|
|
6134
|
+
port: config.server.port,
|
|
6135
|
+
host: config.server.host
|
|
6136
|
+
});
|
|
6137
|
+
const app = server.instance;
|
|
6138
|
+
registerErrorHandler(app);
|
|
6139
|
+
await registerSecurity(app);
|
|
6140
|
+
await registerSwagger(app, {
|
|
6141
|
+
title: "Servcraft API",
|
|
6142
|
+
description: "API documentation generated by Servcraft",
|
|
6143
|
+
version: "1.0.0"
|
|
6144
|
+
});
|
|
6145
|
+
await registerAuthModule(app);
|
|
6146
|
+
const authService = createAuthService(app);
|
|
6147
|
+
await registerUserModule(app, authService);
|
|
6148
|
+
await app.ready();
|
|
6149
|
+
const spec = app.swagger();
|
|
6150
|
+
const absoluteOutput = path9.resolve(outputPath);
|
|
6151
|
+
await fs6.mkdir(path9.dirname(absoluteOutput), { recursive: true });
|
|
6152
|
+
await fs6.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
|
|
6153
|
+
spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
|
|
6154
|
+
await app.close();
|
|
6155
|
+
return absoluteOutput;
|
|
6156
|
+
} catch (error2) {
|
|
6157
|
+
spinner?.fail("Failed to generate OpenAPI documentation");
|
|
6158
|
+
throw error2;
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
// src/cli/commands/docs.ts
|
|
6163
|
+
var docsCommand = new Command5("docs").description("API documentation commands");
|
|
6164
|
+
docsCommand.command("generate").alias("gen").description("Generate OpenAPI/Swagger documentation").option("-o, --output <file>", "Output file path", "openapi.json").option("-f, --format <format>", "Output format: json, yaml", "json").action(async (options) => {
|
|
6165
|
+
try {
|
|
6166
|
+
const outputPath = await generateDocs(options.output, false);
|
|
6167
|
+
if (options.format === "yaml") {
|
|
6168
|
+
const jsonContent = await fs7.readFile(outputPath, "utf-8");
|
|
6169
|
+
const spec = JSON.parse(jsonContent);
|
|
6170
|
+
const yamlPath = outputPath.replace(".json", ".yaml");
|
|
6171
|
+
await fs7.writeFile(yamlPath, jsonToYaml(spec));
|
|
6172
|
+
success(`YAML documentation generated: ${yamlPath}`);
|
|
6173
|
+
}
|
|
6174
|
+
console.log("\n\u{1F4DA} Documentation URLs:");
|
|
6175
|
+
info(" Swagger UI: http://localhost:3000/docs");
|
|
6176
|
+
info(" OpenAPI JSON: http://localhost:3000/docs/json");
|
|
6177
|
+
} catch (err) {
|
|
6178
|
+
error(err instanceof Error ? err.message : String(err));
|
|
6179
|
+
}
|
|
6180
|
+
});
|
|
6181
|
+
docsCommand.option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
|
|
6182
|
+
if (options.output) {
|
|
6183
|
+
try {
|
|
6184
|
+
const outputPath = await generateDocs(options.output);
|
|
6185
|
+
success(`Documentation written to ${outputPath}`);
|
|
6186
|
+
} catch (err) {
|
|
6187
|
+
error(err instanceof Error ? err.message : String(err));
|
|
6188
|
+
process.exitCode = 1;
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
});
|
|
6192
|
+
docsCommand.command("export").description("Export documentation to Postman, Insomnia, or YAML").option("-f, --format <format>", "Export format: postman, insomnia, yaml", "postman").option("-o, --output <file>", "Output file path").action(async (options) => {
|
|
6193
|
+
const spinner = ora6("Exporting documentation...").start();
|
|
6194
|
+
try {
|
|
6195
|
+
const projectRoot = getProjectRoot();
|
|
6196
|
+
const specPath = path10.join(projectRoot, "openapi.json");
|
|
6197
|
+
try {
|
|
6198
|
+
await fs7.access(specPath);
|
|
6199
|
+
} catch {
|
|
6200
|
+
spinner.text = "Generating OpenAPI spec first...";
|
|
6201
|
+
await generateDocs("openapi.json", true);
|
|
6202
|
+
}
|
|
6203
|
+
const specContent = await fs7.readFile(specPath, "utf-8");
|
|
6204
|
+
const spec = JSON.parse(specContent);
|
|
6205
|
+
let output;
|
|
6206
|
+
let defaultName;
|
|
6207
|
+
switch (options.format) {
|
|
6208
|
+
case "postman":
|
|
6209
|
+
output = JSON.stringify(convertToPostman(spec), null, 2);
|
|
6210
|
+
defaultName = "postman_collection.json";
|
|
6211
|
+
break;
|
|
6212
|
+
case "insomnia":
|
|
6213
|
+
output = JSON.stringify(convertToInsomnia(spec), null, 2);
|
|
6214
|
+
defaultName = "insomnia_collection.json";
|
|
6215
|
+
break;
|
|
6216
|
+
case "yaml":
|
|
6217
|
+
output = jsonToYaml(spec);
|
|
6218
|
+
defaultName = "openapi.yaml";
|
|
6219
|
+
break;
|
|
6220
|
+
default:
|
|
6221
|
+
throw new Error(`Unknown format: ${options.format}`);
|
|
6222
|
+
}
|
|
6223
|
+
const outPath = path10.join(projectRoot, options.output || defaultName);
|
|
6224
|
+
await fs7.writeFile(outPath, output);
|
|
6225
|
+
spinner.succeed(`Exported to: ${options.output || defaultName}`);
|
|
6226
|
+
if (options.format === "postman") {
|
|
6227
|
+
info("\n Import in Postman: File > Import > Select file");
|
|
6228
|
+
}
|
|
6229
|
+
} catch (err) {
|
|
6230
|
+
spinner.fail("Export failed");
|
|
6231
|
+
error(err instanceof Error ? err.message : String(err));
|
|
6232
|
+
}
|
|
6233
|
+
});
|
|
6234
|
+
docsCommand.command("status").description("Show documentation status").action(async () => {
|
|
6235
|
+
const projectRoot = getProjectRoot();
|
|
6236
|
+
console.log(chalk9.bold("\n\u{1F4CA} Documentation Status\n"));
|
|
6237
|
+
const specPath = path10.join(projectRoot, "openapi.json");
|
|
6238
|
+
try {
|
|
6239
|
+
const stat2 = await fs7.stat(specPath);
|
|
6240
|
+
success(
|
|
6241
|
+
`openapi.json exists (${formatBytes(stat2.size)}, modified ${formatDate(stat2.mtime)})`
|
|
6242
|
+
);
|
|
6243
|
+
const content = await fs7.readFile(specPath, "utf-8");
|
|
6244
|
+
const spec = JSON.parse(content);
|
|
6245
|
+
const pathCount = Object.keys(spec.paths || {}).length;
|
|
6246
|
+
info(` ${pathCount} endpoints documented`);
|
|
6247
|
+
} catch {
|
|
6248
|
+
warn('openapi.json not found - run "servcraft docs generate"');
|
|
6249
|
+
}
|
|
6250
|
+
console.log("\n\u{1F4CC} Commands:");
|
|
6251
|
+
info(" servcraft docs generate Generate OpenAPI spec");
|
|
6252
|
+
info(" servcraft docs export Export to Postman/Insomnia");
|
|
6253
|
+
});
|
|
6254
|
+
function jsonToYaml(obj, indent = 0) {
|
|
6255
|
+
const spaces = " ".repeat(indent);
|
|
6256
|
+
if (obj === null || obj === void 0) return "null";
|
|
6257
|
+
if (typeof obj === "string") {
|
|
6258
|
+
if (obj.includes("\n") || obj.includes(":") || obj.includes("#")) {
|
|
6259
|
+
return `"${obj.replace(/"/g, '\\"')}"`;
|
|
6260
|
+
}
|
|
6261
|
+
return obj || '""';
|
|
6262
|
+
}
|
|
6263
|
+
if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
|
|
6264
|
+
if (Array.isArray(obj)) {
|
|
6265
|
+
if (obj.length === 0) return "[]";
|
|
6266
|
+
return obj.map((item) => `${spaces}- ${jsonToYaml(item, indent + 1).trimStart()}`).join("\n");
|
|
6267
|
+
}
|
|
6268
|
+
if (typeof obj === "object") {
|
|
6269
|
+
const entries = Object.entries(obj);
|
|
6270
|
+
if (entries.length === 0) return "{}";
|
|
6271
|
+
return entries.map(([key, value]) => {
|
|
6272
|
+
const valueStr = jsonToYaml(value, indent + 1);
|
|
6273
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0) {
|
|
6274
|
+
return `${spaces}${key}:
|
|
6275
|
+
${valueStr}`;
|
|
6276
|
+
}
|
|
6277
|
+
return `${spaces}${key}: ${valueStr}`;
|
|
6278
|
+
}).join("\n");
|
|
6279
|
+
}
|
|
6280
|
+
return String(obj);
|
|
6281
|
+
}
|
|
6282
|
+
function convertToPostman(spec) {
|
|
6283
|
+
const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
|
|
6284
|
+
const items = [];
|
|
6285
|
+
for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
|
|
6286
|
+
for (const [method, details] of Object.entries(methods)) {
|
|
6287
|
+
items.push({
|
|
6288
|
+
name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
|
|
6289
|
+
request: {
|
|
6290
|
+
method: method.toUpperCase(),
|
|
6291
|
+
header: [
|
|
6292
|
+
{ key: "Content-Type", value: "application/json" },
|
|
6293
|
+
{ key: "Authorization", value: "Bearer {{token}}" }
|
|
6294
|
+
],
|
|
6295
|
+
url: {
|
|
6296
|
+
raw: `{{baseUrl}}${pathUrl}`,
|
|
6297
|
+
host: ["{{baseUrl}}"],
|
|
6298
|
+
path: pathUrl.split("/").filter(Boolean)
|
|
6299
|
+
},
|
|
6300
|
+
...details.requestBody ? { body: { mode: "raw", raw: "{}" } } : {}
|
|
6301
|
+
}
|
|
6302
|
+
});
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6305
|
+
return {
|
|
6306
|
+
info: {
|
|
6307
|
+
name: spec.info.title,
|
|
6308
|
+
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
6309
|
+
},
|
|
6310
|
+
item: items,
|
|
6311
|
+
variable: [
|
|
6312
|
+
{ key: "baseUrl", value: baseUrl },
|
|
6313
|
+
{ key: "token", value: "" }
|
|
6314
|
+
]
|
|
6315
|
+
};
|
|
6316
|
+
}
|
|
6317
|
+
function convertToInsomnia(spec) {
|
|
6318
|
+
const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
|
|
6319
|
+
const resources = [
|
|
6320
|
+
{ _type: "environment", name: "Base Environment", data: { baseUrl, token: "" } }
|
|
6321
|
+
];
|
|
6322
|
+
for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
|
|
6323
|
+
for (const [method, details] of Object.entries(methods)) {
|
|
6324
|
+
resources.push({
|
|
6325
|
+
_type: "request",
|
|
6326
|
+
name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
|
|
6327
|
+
method: method.toUpperCase(),
|
|
6328
|
+
url: `{{ baseUrl }}${pathUrl}`,
|
|
6329
|
+
headers: [
|
|
6330
|
+
{ name: "Content-Type", value: "application/json" },
|
|
6331
|
+
{ name: "Authorization", value: "Bearer {{ token }}" }
|
|
6332
|
+
]
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
return { _type: "export", __export_format: 4, resources };
|
|
6337
|
+
}
|
|
6338
|
+
function formatBytes(bytes) {
|
|
6339
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6340
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6341
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6342
|
+
}
|
|
6343
|
+
function formatDate(date) {
|
|
6344
|
+
return date.toLocaleDateString("en-US", {
|
|
6345
|
+
month: "short",
|
|
6346
|
+
day: "numeric",
|
|
6347
|
+
hour: "2-digit",
|
|
6348
|
+
minute: "2-digit"
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
|
|
6352
|
+
// src/cli/commands/list.ts
|
|
6353
|
+
init_esm_shims();
|
|
6354
|
+
import { Command as Command6 } from "commander";
|
|
6355
|
+
import chalk10 from "chalk";
|
|
6356
|
+
import fs8 from "fs/promises";
|
|
6357
|
+
var AVAILABLE_MODULES2 = {
|
|
6358
|
+
// Core
|
|
6359
|
+
auth: {
|
|
6360
|
+
name: "Authentication",
|
|
6361
|
+
description: "JWT authentication with access/refresh tokens",
|
|
6362
|
+
category: "Core"
|
|
6363
|
+
},
|
|
6364
|
+
users: {
|
|
6365
|
+
name: "User Management",
|
|
6366
|
+
description: "User CRUD with RBAC (roles & permissions)",
|
|
6367
|
+
category: "Core"
|
|
6368
|
+
},
|
|
6369
|
+
email: {
|
|
6370
|
+
name: "Email Service",
|
|
6371
|
+
description: "SMTP email with templates (Handlebars)",
|
|
6372
|
+
category: "Core"
|
|
6373
|
+
},
|
|
6374
|
+
// Security
|
|
6375
|
+
mfa: {
|
|
6376
|
+
name: "MFA/TOTP",
|
|
6377
|
+
description: "Two-factor authentication with QR codes",
|
|
6378
|
+
category: "Security"
|
|
6379
|
+
},
|
|
6380
|
+
oauth: {
|
|
6381
|
+
name: "OAuth",
|
|
6382
|
+
description: "Social login (Google, GitHub, Facebook, Twitter, Apple)",
|
|
6383
|
+
category: "Security"
|
|
6384
|
+
},
|
|
6385
|
+
"rate-limit": {
|
|
6386
|
+
name: "Rate Limiting",
|
|
6387
|
+
description: "Advanced rate limiting with multiple algorithms",
|
|
6388
|
+
category: "Security"
|
|
6389
|
+
},
|
|
6390
|
+
// Data & Storage
|
|
6391
|
+
cache: {
|
|
6392
|
+
name: "Redis Cache",
|
|
6393
|
+
description: "Redis caching with TTL & invalidation",
|
|
6394
|
+
category: "Data & Storage"
|
|
6395
|
+
},
|
|
6396
|
+
upload: {
|
|
6397
|
+
name: "File Upload",
|
|
6398
|
+
description: "File upload with local/S3/Cloudinary storage",
|
|
6399
|
+
category: "Data & Storage"
|
|
6400
|
+
},
|
|
6401
|
+
search: {
|
|
6402
|
+
name: "Search",
|
|
6403
|
+
description: "Full-text search with Elasticsearch/Meilisearch",
|
|
6404
|
+
category: "Data & Storage"
|
|
6405
|
+
},
|
|
6406
|
+
// Communication
|
|
6407
|
+
notification: {
|
|
6408
|
+
name: "Notifications",
|
|
6409
|
+
description: "Email, SMS, Push notifications",
|
|
6410
|
+
category: "Communication"
|
|
6411
|
+
},
|
|
6412
|
+
webhook: {
|
|
6413
|
+
name: "Webhooks",
|
|
6414
|
+
description: "Outgoing webhooks with HMAC signatures & retry",
|
|
6415
|
+
category: "Communication"
|
|
6416
|
+
},
|
|
6417
|
+
websocket: {
|
|
6418
|
+
name: "WebSockets",
|
|
6419
|
+
description: "Real-time communication with Socket.io",
|
|
6420
|
+
category: "Communication"
|
|
6421
|
+
},
|
|
6422
|
+
// Background Processing
|
|
6423
|
+
queue: {
|
|
6424
|
+
name: "Queue/Jobs",
|
|
6425
|
+
description: "Background jobs with Bull/BullMQ & cron scheduling",
|
|
6426
|
+
category: "Background Processing"
|
|
6427
|
+
},
|
|
6428
|
+
"media-processing": {
|
|
6429
|
+
name: "Media Processing",
|
|
6430
|
+
description: "Image/video processing with FFmpeg",
|
|
6431
|
+
category: "Background Processing"
|
|
6432
|
+
},
|
|
6433
|
+
// Monitoring & Analytics
|
|
6434
|
+
audit: {
|
|
6435
|
+
name: "Audit Logs",
|
|
6436
|
+
description: "Activity logging and audit trail",
|
|
6437
|
+
category: "Monitoring & Analytics"
|
|
6438
|
+
},
|
|
6439
|
+
analytics: {
|
|
6440
|
+
name: "Analytics/Metrics",
|
|
6441
|
+
description: "Prometheus metrics & event tracking",
|
|
6442
|
+
category: "Monitoring & Analytics"
|
|
6443
|
+
},
|
|
6444
|
+
// Internationalization
|
|
6445
|
+
i18n: {
|
|
6446
|
+
name: "i18n/Localization",
|
|
6447
|
+
description: "Multi-language support with 7+ locales",
|
|
6448
|
+
category: "Internationalization"
|
|
6449
|
+
},
|
|
6450
|
+
// API Management
|
|
6451
|
+
"feature-flag": {
|
|
6452
|
+
name: "Feature Flags",
|
|
6453
|
+
description: "A/B testing & progressive rollout",
|
|
6454
|
+
category: "API Management"
|
|
6455
|
+
},
|
|
6456
|
+
"api-versioning": {
|
|
6457
|
+
name: "API Versioning",
|
|
6458
|
+
description: "Multiple API versions support",
|
|
6459
|
+
category: "API Management"
|
|
6460
|
+
},
|
|
6461
|
+
// Payments
|
|
6462
|
+
payment: {
|
|
6463
|
+
name: "Payments",
|
|
6464
|
+
description: "Payment processing (Stripe, PayPal, Mobile Money)",
|
|
6465
|
+
category: "Payments"
|
|
6466
|
+
}
|
|
6467
|
+
};
|
|
6468
|
+
async function getInstalledModules() {
|
|
6469
|
+
try {
|
|
6470
|
+
const modulesDir = getModulesDir();
|
|
6471
|
+
const entries = await fs8.readdir(modulesDir, { withFileTypes: true });
|
|
6472
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6473
|
+
} catch {
|
|
6474
|
+
return [];
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
function isServercraftProject() {
|
|
6478
|
+
try {
|
|
6479
|
+
getProjectRoot();
|
|
6480
|
+
return true;
|
|
6481
|
+
} catch {
|
|
6482
|
+
return false;
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
var listCommand = new Command6("list").alias("ls").description("List available and installed modules").option("-a, --available", "Show only available modules").option("-i, --installed", "Show only installed modules").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(
|
|
6486
|
+
async (options) => {
|
|
6487
|
+
const installedModules = await getInstalledModules();
|
|
6488
|
+
const isProject = isServercraftProject();
|
|
6489
|
+
if (options.json) {
|
|
6490
|
+
const output = {
|
|
6491
|
+
available: Object.entries(AVAILABLE_MODULES2).map(([key, mod]) => ({
|
|
6492
|
+
id: key,
|
|
6493
|
+
...mod,
|
|
6494
|
+
installed: installedModules.includes(key)
|
|
6495
|
+
}))
|
|
6496
|
+
};
|
|
6497
|
+
if (isProject) {
|
|
6498
|
+
output.installed = installedModules;
|
|
6499
|
+
}
|
|
6500
|
+
console.log(JSON.stringify(output, null, 2));
|
|
6501
|
+
return;
|
|
6502
|
+
}
|
|
6503
|
+
const byCategory = {};
|
|
6504
|
+
for (const [key, mod] of Object.entries(AVAILABLE_MODULES2)) {
|
|
6505
|
+
if (options.category && mod.category.toLowerCase() !== options.category.toLowerCase()) {
|
|
6506
|
+
continue;
|
|
6507
|
+
}
|
|
6508
|
+
if (!byCategory[mod.category]) {
|
|
6509
|
+
byCategory[mod.category] = [];
|
|
6510
|
+
}
|
|
6511
|
+
byCategory[mod.category].push({
|
|
6512
|
+
id: key,
|
|
6513
|
+
name: mod.name,
|
|
6514
|
+
description: mod.description,
|
|
6515
|
+
installed: installedModules.includes(key)
|
|
6516
|
+
});
|
|
6517
|
+
}
|
|
6518
|
+
if (options.installed) {
|
|
6519
|
+
if (!isProject) {
|
|
6520
|
+
console.log(chalk10.yellow("\n\u26A0 Not in a Servcraft project directory\n"));
|
|
6521
|
+
return;
|
|
6522
|
+
}
|
|
6523
|
+
console.log(chalk10.bold("\n\u{1F4E6} Installed Modules:\n"));
|
|
6524
|
+
if (installedModules.length === 0) {
|
|
6525
|
+
console.log(chalk10.gray(" No modules installed yet.\n"));
|
|
6526
|
+
console.log(` Run ${chalk10.cyan("servcraft add <module>")} to add a module.
|
|
6527
|
+
`);
|
|
6528
|
+
return;
|
|
6529
|
+
}
|
|
6530
|
+
for (const modId of installedModules) {
|
|
6531
|
+
const mod = AVAILABLE_MODULES2[modId];
|
|
6532
|
+
if (mod) {
|
|
6533
|
+
console.log(` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${mod.name}`);
|
|
6534
|
+
} else {
|
|
6535
|
+
console.log(
|
|
6536
|
+
` ${chalk10.green("\u2713")} ${chalk10.cyan(modId.padEnd(18))} ${chalk10.gray("(custom module)")}`
|
|
6537
|
+
);
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
console.log(`
|
|
6541
|
+
Total: ${chalk10.bold(installedModules.length)} module(s) installed
|
|
6542
|
+
`);
|
|
6543
|
+
return;
|
|
6544
|
+
}
|
|
6545
|
+
console.log(chalk10.bold("\n\u{1F4E6} Available Modules\n"));
|
|
6546
|
+
if (isProject) {
|
|
6547
|
+
console.log(
|
|
6548
|
+
chalk10.gray(` ${chalk10.green("\u2713")} = installed ${chalk10.dim("\u25CB")} = not installed
|
|
6549
|
+
`)
|
|
6550
|
+
);
|
|
6551
|
+
}
|
|
6552
|
+
for (const [category, modules] of Object.entries(byCategory)) {
|
|
6553
|
+
console.log(chalk10.bold.blue(` ${category}`));
|
|
6554
|
+
console.log(chalk10.gray(" " + "\u2500".repeat(40)));
|
|
6555
|
+
for (const mod of modules) {
|
|
6556
|
+
const status = isProject ? mod.installed ? chalk10.green("\u2713") : chalk10.dim("\u25CB") : " ";
|
|
6557
|
+
const nameColor = mod.installed ? chalk10.green : chalk10.cyan;
|
|
6558
|
+
console.log(` ${status} ${nameColor(mod.id.padEnd(18))} ${mod.name}`);
|
|
6559
|
+
console.log(` ${chalk10.gray(mod.description)}`);
|
|
6560
|
+
}
|
|
6561
|
+
console.log();
|
|
6562
|
+
}
|
|
6563
|
+
const totalAvailable = Object.keys(AVAILABLE_MODULES2).length;
|
|
6564
|
+
const totalInstalled = installedModules.filter((m) => AVAILABLE_MODULES2[m]).length;
|
|
6565
|
+
console.log(chalk10.gray("\u2500".repeat(50)));
|
|
6566
|
+
console.log(
|
|
6567
|
+
` ${chalk10.bold(totalAvailable)} modules available` + (isProject ? ` | ${chalk10.green.bold(totalInstalled)} installed` : "")
|
|
6568
|
+
);
|
|
6569
|
+
console.log();
|
|
6570
|
+
console.log(chalk10.bold(" Usage:"));
|
|
6571
|
+
console.log(` ${chalk10.yellow("servcraft add <module>")} Add a module`);
|
|
6572
|
+
console.log(` ${chalk10.yellow("servcraft list --installed")} Show installed only`);
|
|
6573
|
+
console.log(` ${chalk10.yellow("servcraft list --category Security")} Filter by category`);
|
|
6574
|
+
console.log();
|
|
6575
|
+
}
|
|
6576
|
+
);
|
|
6577
|
+
|
|
6578
|
+
// src/cli/commands/remove.ts
|
|
6579
|
+
init_esm_shims();
|
|
6580
|
+
import { Command as Command7 } from "commander";
|
|
6581
|
+
import path11 from "path";
|
|
6582
|
+
import ora7 from "ora";
|
|
6583
|
+
import chalk11 from "chalk";
|
|
6584
|
+
import fs9 from "fs/promises";
|
|
6585
|
+
import inquirer4 from "inquirer";
|
|
6586
|
+
init_error_handler();
|
|
6587
|
+
var removeCommand = new Command7("remove").alias("rm").description("Remove an installed module from your project").argument("<module>", "Module to remove").option("-y, --yes", "Skip confirmation prompt").option("--keep-env", "Keep environment variables").action(async (moduleName, options) => {
|
|
6588
|
+
const projectError = validateProject();
|
|
6589
|
+
if (projectError) {
|
|
6590
|
+
displayError(projectError);
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6593
|
+
console.log(chalk11.bold.cyan("\n\u{1F5D1}\uFE0F ServCraft Module Removal\n"));
|
|
6594
|
+
const moduleDir = path11.join(getModulesDir(), moduleName);
|
|
6595
|
+
try {
|
|
6596
|
+
const exists = await fs9.access(moduleDir).then(() => true).catch(() => false);
|
|
6597
|
+
if (!exists) {
|
|
6598
|
+
displayError(
|
|
6599
|
+
new (init_error_handler(), __toCommonJS(error_handler_exports)).ServCraftError(
|
|
6600
|
+
`Module "${moduleName}" is not installed`,
|
|
6601
|
+
[
|
|
6602
|
+
`Run ${chalk11.cyan("servcraft list --installed")} to see installed modules`,
|
|
6603
|
+
`Check the spelling of the module name`
|
|
6604
|
+
]
|
|
6605
|
+
)
|
|
6606
|
+
);
|
|
6607
|
+
return;
|
|
6608
|
+
}
|
|
6609
|
+
const files = await fs9.readdir(moduleDir);
|
|
6610
|
+
const fileCount = files.length;
|
|
6611
|
+
if (!options?.yes) {
|
|
6612
|
+
console.log(chalk11.yellow(`\u26A0 This will remove the "${moduleName}" module:`));
|
|
6613
|
+
console.log(chalk11.gray(` Directory: ${moduleDir}`));
|
|
6614
|
+
console.log(chalk11.gray(` Files: ${fileCount} file(s)`));
|
|
6615
|
+
console.log();
|
|
6616
|
+
const { confirm } = await inquirer4.prompt([
|
|
6617
|
+
{
|
|
6618
|
+
type: "confirm",
|
|
6619
|
+
name: "confirm",
|
|
6620
|
+
message: "Are you sure you want to remove this module?",
|
|
6621
|
+
default: false
|
|
6622
|
+
}
|
|
6623
|
+
]);
|
|
6624
|
+
if (!confirm) {
|
|
6625
|
+
console.log(chalk11.yellow("\n\u2716 Removal cancelled\n"));
|
|
6626
|
+
return;
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
const spinner = ora7("Removing module...").start();
|
|
6630
|
+
await fs9.rm(moduleDir, { recursive: true, force: true });
|
|
6631
|
+
spinner.succeed(`Module "${moduleName}" removed successfully!`);
|
|
6632
|
+
console.log("\n" + chalk11.bold("\u2713 Removed:"));
|
|
6633
|
+
success(` src/modules/${moduleName}/ (${fileCount} files)`);
|
|
6634
|
+
if (!options?.keepEnv) {
|
|
6635
|
+
console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
|
|
6636
|
+
info(" 1. Remove environment variables related to this module from .env");
|
|
6637
|
+
info(" 2. Remove module imports from your main app file");
|
|
6638
|
+
info(" 3. Remove related database migrations if any");
|
|
6639
|
+
info(" 4. Update your routes if they reference this module");
|
|
6640
|
+
} else {
|
|
6641
|
+
console.log("\n" + chalk11.bold("\u{1F4CC} Manual cleanup needed:"));
|
|
6642
|
+
info(" 1. Environment variables were kept (--keep-env flag)");
|
|
6643
|
+
info(" 2. Remove module imports from your main app file");
|
|
6644
|
+
info(" 3. Update your routes if they reference this module");
|
|
6645
|
+
}
|
|
6646
|
+
console.log();
|
|
6647
|
+
} catch (err) {
|
|
6648
|
+
error(err instanceof Error ? err.message : String(err));
|
|
6649
|
+
console.log();
|
|
6650
|
+
}
|
|
6651
|
+
});
|
|
6652
|
+
|
|
6653
|
+
// src/cli/commands/doctor.ts
|
|
6654
|
+
init_esm_shims();
|
|
6655
|
+
import { Command as Command8 } from "commander";
|
|
6656
|
+
import chalk12 from "chalk";
|
|
6657
|
+
var doctorCommand = new Command8("doctor").description("Diagnose project configuration and dependencies").action(async () => {
|
|
6658
|
+
console.log(chalk12.bold.cyan("\nServCraft Doctor - Coming soon!\n"));
|
|
6659
|
+
});
|
|
6660
|
+
|
|
3827
6661
|
// src/cli/index.ts
|
|
3828
|
-
var program = new
|
|
6662
|
+
var program = new Command9();
|
|
3829
6663
|
program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
|
|
3830
6664
|
program.addCommand(initCommand);
|
|
3831
6665
|
program.addCommand(generateCommand);
|
|
3832
6666
|
program.addCommand(addModuleCommand);
|
|
3833
6667
|
program.addCommand(dbCommand);
|
|
6668
|
+
program.addCommand(docsCommand);
|
|
6669
|
+
program.addCommand(listCommand);
|
|
6670
|
+
program.addCommand(removeCommand);
|
|
6671
|
+
program.addCommand(doctorCommand);
|
|
3834
6672
|
program.parse();
|
|
3835
6673
|
//# sourceMappingURL=index.js.map
|