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