servcraft 0.1.5 → 0.1.7

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.
@@ -23,8 +23,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
+ // node_modules/tsup/assets/cjs_shims.js
27
+ var 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;
28
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
+
26
30
  // src/cli/index.ts
27
- var import_commander5 = require("commander");
31
+ var import_commander6 = require("commander");
28
32
 
29
33
  // src/cli/commands/init.ts
30
34
  var import_commander = require("commander");
@@ -96,7 +100,7 @@ function getModulesDir() {
96
100
  }
97
101
 
98
102
  // src/cli/commands/init.ts
99
- var initCommand = new import_commander.Command("init").alias("new").description("Initialize a new Servcraft project").argument("[name]", "Project name").option("-y, --yes", "Skip prompts and use defaults").option("--ts, --typescript", "Use TypeScript (default)").option("--js, --javascript", "Use JavaScript").option("--db <database>", "Database type (postgresql, mysql, sqlite, mongodb, none)").action(
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("--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)").action(
100
104
  async (name, cmdOptions) => {
101
105
  console.log(
102
106
  import_chalk2.default.blue(`
@@ -113,6 +117,7 @@ var initCommand = new import_commander.Command("init").alias("new").description(
113
117
  options = {
114
118
  name: name || "my-servcraft-app",
115
119
  language: cmdOptions.javascript ? "javascript" : "typescript",
120
+ moduleSystem: cmdOptions.commonjs ? "commonjs" : "esm",
116
121
  database: db,
117
122
  orm: db === "mongodb" ? "mongoose" : db === "none" ? "none" : "prisma",
118
123
  validator: "zod",
@@ -142,6 +147,16 @@ var initCommand = new import_commander.Command("init").alias("new").description(
142
147
  ],
143
148
  default: "typescript"
144
149
  },
150
+ {
151
+ type: "list",
152
+ name: "moduleSystem",
153
+ message: "Select module system:",
154
+ choices: [
155
+ { name: "ESM (import/export) - Recommended", value: "esm" },
156
+ { name: "CommonJS (require/module.exports)", value: "commonjs" }
157
+ ],
158
+ default: "esm"
159
+ },
145
160
  {
146
161
  type: "list",
147
162
  name: "database",
@@ -204,10 +219,10 @@ var initCommand = new import_commander.Command("init").alias("new").description(
204
219
  JSON.stringify(packageJson, null, 2)
205
220
  );
206
221
  if (options.language === "typescript") {
207
- await writeFile(import_path2.default.join(projectDir, "tsconfig.json"), generateTsConfig());
208
- await writeFile(import_path2.default.join(projectDir, "tsup.config.ts"), generateTsupConfig());
222
+ await writeFile(import_path2.default.join(projectDir, "tsconfig.json"), generateTsConfig(options));
223
+ await writeFile(import_path2.default.join(projectDir, "tsup.config.ts"), generateTsupConfig(options));
209
224
  } else {
210
- await writeFile(import_path2.default.join(projectDir, "jsconfig.json"), generateJsConfig());
225
+ await writeFile(import_path2.default.join(projectDir, "jsconfig.json"), generateJsConfig(options));
211
226
  }
212
227
  await writeFile(import_path2.default.join(projectDir, ".env.example"), generateEnvExample(options));
213
228
  await writeFile(import_path2.default.join(projectDir, ".env"), generateEnvExample(options));
@@ -217,7 +232,7 @@ var initCommand = new import_commander.Command("init").alias("new").description(
217
232
  import_path2.default.join(projectDir, "docker-compose.yml"),
218
233
  generateDockerCompose(options)
219
234
  );
220
- const ext = options.language === "typescript" ? "ts" : "js";
235
+ const ext = options.language === "typescript" ? "ts" : options.moduleSystem === "esm" ? "js" : "cjs";
221
236
  const dirs = [
222
237
  "src/core",
223
238
  "src/config",
@@ -246,6 +261,22 @@ var initCommand = new import_commander.Command("init").alias("new").description(
246
261
  import_path2.default.join(projectDir, `src/core/logger.${ext}`),
247
262
  generateLoggerFile(options)
248
263
  );
264
+ await writeFile(
265
+ import_path2.default.join(projectDir, `src/config/index.${ext}`),
266
+ generateConfigFile(options)
267
+ );
268
+ await writeFile(
269
+ import_path2.default.join(projectDir, `src/middleware/index.${ext}`),
270
+ generateMiddlewareFile(options)
271
+ );
272
+ await writeFile(
273
+ import_path2.default.join(projectDir, `src/utils/index.${ext}`),
274
+ generateUtilsFile(options)
275
+ );
276
+ await writeFile(
277
+ import_path2.default.join(projectDir, `src/types/index.${ext}`),
278
+ generateTypesFile(options)
279
+ );
249
280
  if (options.orm === "prisma") {
250
281
  await writeFile(
251
282
  import_path2.default.join(projectDir, "prisma/schema.prisma"),
@@ -307,18 +338,35 @@ var initCommand = new import_commander.Command("init").alias("new").description(
307
338
  );
308
339
  function generatePackageJson(options) {
309
340
  const isTS = options.language === "typescript";
341
+ const isESM = options.moduleSystem === "esm";
342
+ let devCommand;
343
+ if (isTS) {
344
+ devCommand = "tsx watch src/index.ts";
345
+ } else if (isESM) {
346
+ devCommand = "node --watch src/index.js";
347
+ } else {
348
+ devCommand = "node --watch src/index.cjs";
349
+ }
350
+ let startCommand;
351
+ if (isTS) {
352
+ startCommand = isESM ? "node dist/index.js" : "node dist/index.cjs";
353
+ } else if (isESM) {
354
+ startCommand = "node src/index.js";
355
+ } else {
356
+ startCommand = "node src/index.cjs";
357
+ }
310
358
  const pkg = {
311
359
  name: options.name,
312
360
  version: "0.1.0",
313
361
  description: "A Servcraft application",
314
- main: isTS ? "dist/index.js" : "src/index.js",
315
- type: "module",
362
+ main: isTS ? isESM ? "dist/index.js" : "dist/index.cjs" : isESM ? "src/index.js" : "src/index.cjs",
363
+ ...isESM && { type: "module" },
316
364
  scripts: {
317
- dev: isTS ? "tsx watch src/index.ts" : "node --watch src/index.js",
365
+ dev: devCommand,
318
366
  build: isTS ? "tsup" : 'echo "No build needed for JS"',
319
- start: isTS ? "node dist/index.js" : "node src/index.js",
367
+ start: startCommand,
320
368
  test: "vitest",
321
- lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js"
369
+ lint: isTS ? "eslint src --ext .ts" : "eslint src --ext .js,.cjs"
322
370
  },
323
371
  dependencies: {
324
372
  fastify: "^4.28.1",
@@ -380,13 +428,14 @@ function generatePackageJson(options) {
380
428
  }
381
429
  return pkg;
382
430
  }
383
- function generateTsConfig() {
431
+ function generateTsConfig(options) {
432
+ const isESM = options.moduleSystem === "esm";
384
433
  return JSON.stringify(
385
434
  {
386
435
  compilerOptions: {
387
436
  target: "ES2022",
388
- module: "NodeNext",
389
- moduleResolution: "NodeNext",
437
+ module: isESM ? "NodeNext" : "CommonJS",
438
+ moduleResolution: isESM ? "NodeNext" : "Node",
390
439
  lib: ["ES2022"],
391
440
  outDir: "./dist",
392
441
  rootDir: "./src",
@@ -405,12 +454,13 @@ function generateTsConfig() {
405
454
  2
406
455
  );
407
456
  }
408
- function generateJsConfig() {
457
+ function generateJsConfig(options) {
458
+ const isESM = options.moduleSystem === "esm";
409
459
  return JSON.stringify(
410
460
  {
411
461
  compilerOptions: {
412
- module: "NodeNext",
413
- moduleResolution: "NodeNext",
462
+ module: isESM ? "NodeNext" : "CommonJS",
463
+ moduleResolution: isESM ? "NodeNext" : "Node",
414
464
  target: "ES2022",
415
465
  checkJs: true
416
466
  },
@@ -421,12 +471,13 @@ function generateJsConfig() {
421
471
  2
422
472
  );
423
473
  }
424
- function generateTsupConfig() {
474
+ function generateTsupConfig(options) {
475
+ const isESM = options.moduleSystem === "esm";
425
476
  return `import { defineConfig } from 'tsup';
426
477
 
427
478
  export default defineConfig({
428
479
  entry: ['src/index.ts'],
429
- format: ['esm'],
480
+ format: ['${isESM ? "esm" : "cjs"}'],
430
481
  dts: true,
431
482
  clean: true,
432
483
  sourcemap: true,
@@ -435,7 +486,7 @@ export default defineConfig({
435
486
  `;
436
487
  }
437
488
  function generateEnvExample(options) {
438
- let env = `# Server
489
+ let env2 = `# Server
439
490
  NODE_ENV=development
440
491
  PORT=3000
441
492
  HOST=0.0.0.0
@@ -453,31 +504,31 @@ RATE_LIMIT_MAX=100
453
504
  LOG_LEVEL=info
454
505
  `;
455
506
  if (options.database === "postgresql") {
456
- env += `
507
+ env2 += `
457
508
  # Database (PostgreSQL)
458
509
  DATABASE_PROVIDER=postgresql
459
510
  DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
460
511
  `;
461
512
  } else if (options.database === "mysql") {
462
- env += `
513
+ env2 += `
463
514
  # Database (MySQL)
464
515
  DATABASE_PROVIDER=mysql
465
516
  DATABASE_URL="mysql://user:password@localhost:3306/mydb"
466
517
  `;
467
518
  } else if (options.database === "sqlite") {
468
- env += `
519
+ env2 += `
469
520
  # Database (SQLite)
470
521
  DATABASE_PROVIDER=sqlite
471
522
  DATABASE_URL="file:./dev.db"
472
523
  `;
473
524
  } else if (options.database === "mongodb") {
474
- env += `
525
+ env2 += `
475
526
  # Database (MongoDB)
476
527
  MONGODB_URI="mongodb://localhost:27017/mydb"
477
528
  `;
478
529
  }
479
530
  if (options.features.includes("email")) {
480
- env += `
531
+ env2 += `
481
532
  # Email
482
533
  SMTP_HOST=smtp.example.com
483
534
  SMTP_PORT=587
@@ -487,12 +538,12 @@ SMTP_FROM="App <noreply@example.com>"
487
538
  `;
488
539
  }
489
540
  if (options.features.includes("redis")) {
490
- env += `
541
+ env2 += `
491
542
  # Redis
492
543
  REDIS_URL=redis://localhost:6379
493
544
  `;
494
545
  }
495
- return env;
546
+ return env2;
496
547
  }
497
548
  function generateGitignore() {
498
549
  return `node_modules/
@@ -621,7 +672,12 @@ model User {
621
672
  }
622
673
  function generateEntryFile(options) {
623
674
  const isTS = options.language === "typescript";
624
- return `${isTS ? "import 'dotenv/config';\nimport { createServer } from './core/server.js';\nimport { logger } from './core/logger.js';" : "require('dotenv').config();\nconst { createServer } = require('./core/server.js');\nconst { logger } = require('./core/logger.js');"}
675
+ const isESM = options.moduleSystem === "esm";
676
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
677
+ if (isESM || isTS) {
678
+ return `import 'dotenv/config';
679
+ import { createServer } from './core/server.${fileExt}';
680
+ import { logger } from './core/logger.${fileExt}';
625
681
 
626
682
  async function main()${isTS ? ": Promise<void>" : ""} {
627
683
  const server = createServer();
@@ -636,16 +692,31 @@ async function main()${isTS ? ": Promise<void>" : ""} {
636
692
 
637
693
  main();
638
694
  `;
695
+ } else {
696
+ return `require('dotenv').config();
697
+ const { createServer } = require('./core/server.cjs');
698
+ const { logger } = require('./core/logger.cjs');
699
+
700
+ async function main() {
701
+ const server = createServer();
702
+
703
+ try {
704
+ await server.start();
705
+ } catch (error) {
706
+ logger.error({ err: error }, 'Failed to start server');
707
+ process.exit(1);
708
+ }
709
+ }
710
+
711
+ main();
712
+ `;
713
+ }
639
714
  }
640
715
  function generateServerFile(options) {
641
716
  const isTS = options.language === "typescript";
642
- return `${isTS ? `import Fastify from 'fastify';
643
- import type { FastifyInstance } from 'fastify';
644
- import { logger } from './logger.js';` : `const Fastify = require('fastify');
645
- const { logger } = require('./logger.js');`}
646
-
647
- ${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "function createServer()"} {
648
- const app = Fastify({ logger });
717
+ const isESM = options.moduleSystem === "esm";
718
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
719
+ const serverBody = ` const app = Fastify({ logger });
649
720
 
650
721
  // Health check
651
722
  app.get('/health', async () => ({
@@ -672,33 +743,58 @@ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: ()
672
743
  logger.info(\`Server listening on \${host}:\${port}\`);
673
744
  },
674
745
  };
675
- }
746
+ }`;
747
+ if (isESM || isTS) {
748
+ return `import Fastify from 'fastify';
749
+ ${isTS ? "import type { FastifyInstance } from 'fastify';" : ""}
750
+ import { logger } from './logger.${fileExt}';
751
+
752
+ ${isTS ? "export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }" : "export function createServer()"} {
753
+ ${serverBody}
754
+ `;
755
+ } else {
756
+ return `const Fastify = require('fastify');
757
+ const { logger } = require('./logger.cjs');
676
758
 
677
- ${isTS ? "" : "module.exports = { createServer };"}
759
+ function createServer() {
760
+ ${serverBody}
761
+
762
+ module.exports = { createServer };
678
763
  `;
764
+ }
679
765
  }
680
766
  function generateLoggerFile(options) {
681
767
  const isTS = options.language === "typescript";
682
- return `${isTS ? "import pino from 'pino';\nimport type { Logger } from 'pino';" : "const pino = require('pino');"}
683
-
684
- ${isTS ? "export const logger: Logger" : "const logger"} = pino({
768
+ const isESM = options.moduleSystem === "esm";
769
+ const loggerBody = `pino({
685
770
  level: process.env.LOG_LEVEL || 'info',
686
771
  transport: process.env.NODE_ENV !== 'production' ? {
687
772
  target: 'pino-pretty',
688
773
  options: { colorize: true },
689
774
  } : undefined,
690
- });
775
+ })`;
776
+ if (isESM || isTS) {
777
+ return `import pino from 'pino';
778
+ ${isTS ? "import type { Logger } from 'pino';" : ""}
691
779
 
692
- ${isTS ? "" : "module.exports = { logger };"}
780
+ export const logger${isTS ? ": Logger" : ""} = ${loggerBody};
693
781
  `;
782
+ } else {
783
+ return `const pino = require('pino');
784
+
785
+ const logger = ${loggerBody};
786
+
787
+ module.exports = { logger };
788
+ `;
789
+ }
694
790
  }
695
791
  function generateMongooseConnection(options) {
696
792
  const isTS = options.language === "typescript";
697
- return `${isTS ? "import mongoose from 'mongoose';\nimport { logger } from '../core/logger.js';" : "const mongoose = require('mongoose');\nconst { logger } = require('../core/logger.js');"}
793
+ const isESM = options.moduleSystem === "esm";
794
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
795
+ const connectionBody = `const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
698
796
 
699
- const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mydb';
700
-
701
- ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "async function connectDatabase()"} {
797
+ async function connectDatabase()${isTS ? ": Promise<typeof mongoose>" : ""} {
702
798
  try {
703
799
  const conn = await mongoose.connect(MONGODB_URI);
704
800
  logger.info(\`MongoDB connected: \${conn.connection.host}\`);
@@ -709,35 +805,36 @@ ${isTS ? "export async function connectDatabase(): Promise<typeof mongoose>" : "
709
805
  }
710
806
  }
711
807
 
712
- ${isTS ? "export async function disconnectDatabase(): Promise<void>" : "async function disconnectDatabase()"} {
808
+ async function disconnectDatabase()${isTS ? ": Promise<void>" : ""} {
713
809
  try {
714
810
  await mongoose.disconnect();
715
811
  logger.info('MongoDB disconnected');
716
812
  } catch (error) {
717
813
  logger.error({ err: error }, 'MongoDB disconnect failed');
718
814
  }
719
- }
815
+ }`;
816
+ if (isESM || isTS) {
817
+ return `import mongoose from 'mongoose';
818
+ import { logger } from '../core/logger.${fileExt}';
819
+
820
+ ${connectionBody}
821
+
822
+ export { connectDatabase, disconnectDatabase, mongoose };
823
+ `;
824
+ } else {
825
+ return `const mongoose = require('mongoose');
826
+ const { logger } = require('../core/logger.cjs');
827
+
828
+ ${connectionBody}
720
829
 
721
- ${isTS ? "export { mongoose };" : "module.exports = { connectDatabase, disconnectDatabase, mongoose };"}
830
+ module.exports = { connectDatabase, disconnectDatabase, mongoose };
722
831
  `;
832
+ }
723
833
  }
724
834
  function generateMongooseUserModel(options) {
725
835
  const isTS = options.language === "typescript";
726
- return `${isTS ? "import mongoose, { Schema, Document } from 'mongoose';\nimport bcrypt from 'bcryptjs';" : "const mongoose = require('mongoose');\nconst bcrypt = require('bcryptjs');\nconst { Schema } = mongoose;"}
727
-
728
- ${isTS ? `export interface IUser extends Document {
729
- email: string;
730
- password: string;
731
- name?: string;
732
- role: 'user' | 'admin';
733
- status: 'active' | 'inactive' | 'suspended';
734
- emailVerified: boolean;
735
- createdAt: Date;
736
- updatedAt: Date;
737
- comparePassword(candidatePassword: string): Promise<boolean>;
738
- }` : ""}
739
-
740
- const userSchema = new Schema${isTS ? "<IUser>" : ""}({
836
+ const isESM = options.moduleSystem === "esm";
837
+ const schemaBody = `const userSchema = new Schema${isTS ? "<IUser>" : ""}({
741
838
  email: {
742
839
  type: String,
743
840
  required: true,
@@ -788,9 +885,237 @@ userSchema.pre('save', async function(next) {
788
885
  // Compare password method
789
886
  userSchema.methods.comparePassword = async function(candidatePassword${isTS ? ": string" : ""})${isTS ? ": Promise<boolean>" : ""} {
790
887
  return bcrypt.compare(candidatePassword, this.password);
791
- };
888
+ };`;
889
+ const tsInterface = isTS ? `
890
+ export interface IUser extends Document {
891
+ email: string;
892
+ password: string;
893
+ name?: string;
894
+ role: 'user' | 'admin';
895
+ status: 'active' | 'inactive' | 'suspended';
896
+ emailVerified: boolean;
897
+ createdAt: Date;
898
+ updatedAt: Date;
899
+ comparePassword(candidatePassword: string): Promise<boolean>;
900
+ }
901
+ ` : "";
902
+ if (isESM || isTS) {
903
+ return `import mongoose${isTS ? ", { Schema, Document }" : ""} from 'mongoose';
904
+ import bcrypt from 'bcryptjs';
905
+ ${!isTS ? "const { Schema } = mongoose;" : ""}
906
+ ${tsInterface}
907
+ ${schemaBody}
908
+
909
+ export const User = mongoose.model${isTS ? "<IUser>" : ""}('User', userSchema);
910
+ `;
911
+ } else {
912
+ return `const mongoose = require('mongoose');
913
+ const bcrypt = require('bcryptjs');
914
+ const { Schema } = mongoose;
915
+
916
+ ${schemaBody}
917
+
918
+ const User = mongoose.model('User', userSchema);
919
+
920
+ module.exports = { User };
921
+ `;
922
+ }
923
+ }
924
+ function generateConfigFile(options) {
925
+ const isTS = options.language === "typescript";
926
+ const isESM = options.moduleSystem === "esm";
927
+ const configBody = `{
928
+ env: process.env.NODE_ENV || 'development',
929
+ port: parseInt(process.env.PORT || '3000', 10),
930
+ host: process.env.HOST || '0.0.0.0',
931
+
932
+ jwt: {
933
+ secret: process.env.JWT_SECRET || 'your-secret-key',
934
+ accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || '15m',
935
+ refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
936
+ },
937
+
938
+ cors: {
939
+ origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
940
+ },
941
+
942
+ rateLimit: {
943
+ max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
944
+ },
945
+
946
+ log: {
947
+ level: process.env.LOG_LEVEL || 'info',
948
+ },
949
+ }${isTS ? " as const" : ""}`;
950
+ if (isESM || isTS) {
951
+ return `import 'dotenv/config';
952
+
953
+ export const config = ${configBody};
954
+ `;
955
+ } else {
956
+ return `require('dotenv').config();
957
+
958
+ const config = ${configBody};
959
+
960
+ module.exports = { config };
961
+ `;
962
+ }
963
+ }
964
+ function generateMiddlewareFile(options) {
965
+ const isTS = options.language === "typescript";
966
+ const isESM = options.moduleSystem === "esm";
967
+ const fileExt = isTS ? "js" : isESM ? "js" : "cjs";
968
+ const middlewareBody = `/**
969
+ * Error handler middleware
970
+ */
971
+ function errorHandler(error${isTS ? ": Error" : ""}, request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""})${isTS ? ": void" : ""} {
972
+ logger.error({ err: error, url: request.url, method: request.method }, 'Request error');
973
+
974
+ const statusCode = (error${isTS ? " as any" : ""}).statusCode || 500;
975
+ const message = statusCode === 500 ? 'Internal Server Error' : error.message;
976
+
977
+ reply.status(statusCode).send({
978
+ success: false,
979
+ error: message,
980
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Request logging middleware
986
+ */
987
+ function requestLogger(request${isTS ? ": FastifyRequest" : ""}, reply${isTS ? ": FastifyReply" : ""}, done${isTS ? ": () => void" : ""})${isTS ? ": void" : ""} {
988
+ logger.info({ url: request.url, method: request.method, ip: request.ip }, 'Incoming request');
989
+ done();
990
+ }`;
991
+ if (isESM || isTS) {
992
+ return `${isTS ? "import type { FastifyRequest, FastifyReply } from 'fastify';" : ""}
993
+ import { logger } from '../core/logger.${fileExt}';
994
+
995
+ ${middlewareBody}
996
+
997
+ export { errorHandler, requestLogger };
998
+ `;
999
+ } else {
1000
+ return `const { logger } = require('../core/logger.cjs');
1001
+
1002
+ ${middlewareBody}
1003
+
1004
+ module.exports = { errorHandler, requestLogger };
1005
+ `;
1006
+ }
1007
+ }
1008
+ function generateUtilsFile(options) {
1009
+ const isTS = options.language === "typescript";
1010
+ const isESM = options.moduleSystem === "esm";
1011
+ const utilsBody = `/**
1012
+ * Standard API response helper
1013
+ */
1014
+ function apiResponse${isTS ? "<T>" : ""}(data${isTS ? ": T" : ""}, message = "Success")${isTS ? ": { success: boolean; message: string; data: T }" : ""} {
1015
+ return {
1016
+ success: true,
1017
+ message,
1018
+ data,
1019
+ };
1020
+ }
1021
+
1022
+ /**
1023
+ * Error response helper
1024
+ */
1025
+ function errorResponse(message${isTS ? ": string" : ""}, code${isTS ? "?: string" : ""})${isTS ? ": { success: boolean; error: string; code?: string }" : ""} {
1026
+ return {
1027
+ success: false,
1028
+ error: message,
1029
+ ...(code && { code }),
1030
+ };
1031
+ }
1032
+
1033
+ /**
1034
+ * Pagination helper
1035
+ */
1036
+ function paginate${isTS ? "<T>" : ""}(data${isTS ? ": T[]" : ""}, page${isTS ? ": number" : ""}, limit${isTS ? ": number" : ""}, total${isTS ? ": number" : ""})${isTS ? ": PaginationResult<T>" : ""} {
1037
+ const totalPages = Math.ceil(total / limit);
1038
+
1039
+ return {
1040
+ data,
1041
+ pagination: {
1042
+ page,
1043
+ limit,
1044
+ total,
1045
+ totalPages,
1046
+ hasNextPage: page < totalPages,
1047
+ hasPrevPage: page > 1,
1048
+ },
1049
+ };
1050
+ }`;
1051
+ const tsInterface = isTS ? `
1052
+ /**
1053
+ * Pagination result type
1054
+ */
1055
+ export interface PaginationResult<T> {
1056
+ data: T[];
1057
+ pagination: {
1058
+ page: number;
1059
+ limit: number;
1060
+ total: number;
1061
+ totalPages: number;
1062
+ hasNextPage: boolean;
1063
+ hasPrevPage: boolean;
1064
+ };
1065
+ }
1066
+ ` : "";
1067
+ if (isESM || isTS) {
1068
+ return `${tsInterface}
1069
+ ${utilsBody}
1070
+
1071
+ export { apiResponse, errorResponse, paginate };
1072
+ `;
1073
+ } else {
1074
+ return `${utilsBody}
1075
+
1076
+ module.exports = { apiResponse, errorResponse, paginate };
1077
+ `;
1078
+ }
1079
+ }
1080
+ function generateTypesFile(options) {
1081
+ if (options.language !== "typescript") {
1082
+ return "// Types file - not needed for JavaScript\n";
1083
+ }
1084
+ return `/**
1085
+ * Common type definitions
1086
+ */
1087
+
1088
+ export interface ApiResponse<T = unknown> {
1089
+ success: boolean;
1090
+ message?: string;
1091
+ data?: T;
1092
+ error?: string;
1093
+ code?: string;
1094
+ }
1095
+
1096
+ export interface PaginatedResponse<T> {
1097
+ data: T[];
1098
+ pagination: {
1099
+ page: number;
1100
+ limit: number;
1101
+ total: number;
1102
+ totalPages: number;
1103
+ hasNextPage: boolean;
1104
+ hasPrevPage: boolean;
1105
+ };
1106
+ }
792
1107
 
793
- ${isTS ? "export const User = mongoose.model<IUser>('User', userSchema);" : "const User = mongoose.model('User', userSchema);\nmodule.exports = { User };"}
1108
+ export interface RequestUser {
1109
+ id: string;
1110
+ email: string;
1111
+ role: string;
1112
+ }
1113
+
1114
+ declare module 'fastify' {
1115
+ interface FastifyRequest {
1116
+ user?: RequestUser;
1117
+ }
1118
+ }
794
1119
  `;
795
1120
  }
796
1121
 
@@ -1959,12 +2284,12 @@ var EnvManager = class {
1959
2284
  async addVariables(sections) {
1960
2285
  const added = [];
1961
2286
  const skipped = [];
1962
- let created = false;
2287
+ let created2 = false;
1963
2288
  let envContent = "";
1964
2289
  if ((0, import_fs.existsSync)(this.envPath)) {
1965
2290
  envContent = await fs3.readFile(this.envPath, "utf-8");
1966
2291
  } else {
1967
- created = true;
2292
+ created2 = true;
1968
2293
  }
1969
2294
  const existingKeys = this.parseExistingKeys(envContent);
1970
2295
  let newContent = envContent;
@@ -1995,7 +2320,7 @@ var EnvManager = class {
1995
2320
  if ((0, import_fs.existsSync)(this.envExamplePath)) {
1996
2321
  await this.updateEnvExample(sections);
1997
2322
  }
1998
- return { added, skipped, created };
2323
+ return { added, skipped, created: created2 };
1999
2324
  }
2000
2325
  /**
2001
2326
  * Update .env.example file
@@ -3555,11 +3880,43 @@ export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
3555
3880
  await writeFile(import_path4.default.join(dir, fileName), content);
3556
3881
  }
3557
3882
  }
3883
+ async function findServercraftModules() {
3884
+ const scriptDir = import_path4.default.dirname(new URL(importMetaUrl).pathname);
3885
+ const possiblePaths = [
3886
+ // Local node_modules (when servcraft is a dependency)
3887
+ import_path4.default.join(process.cwd(), "node_modules", "servcraft", "src", "modules"),
3888
+ // From dist/cli/index.js -> src/modules (npx or global install)
3889
+ import_path4.default.resolve(scriptDir, "..", "..", "src", "modules"),
3890
+ // From src/cli/commands/add-module.ts -> src/modules (development)
3891
+ import_path4.default.resolve(scriptDir, "..", "..", "modules")
3892
+ ];
3893
+ for (const p of possiblePaths) {
3894
+ try {
3895
+ const stats = await fs5.stat(p);
3896
+ if (stats.isDirectory()) {
3897
+ return p;
3898
+ }
3899
+ } catch {
3900
+ }
3901
+ }
3902
+ return null;
3903
+ }
3558
3904
  async function generateModuleFiles(moduleName, moduleDir) {
3559
- const sourceModuleDir = import_path4.default.join(process.cwd(), "src", "modules", moduleName);
3560
- if (await fileExists(sourceModuleDir)) {
3561
- await copyModuleFromSource(sourceModuleDir, moduleDir);
3562
- return;
3905
+ const moduleNameMap = {
3906
+ users: "user",
3907
+ "rate-limit": "rate-limit",
3908
+ "feature-flag": "feature-flag",
3909
+ "api-versioning": "api-versioning",
3910
+ "media-processing": "media-processing"
3911
+ };
3912
+ const sourceDirName = moduleNameMap[moduleName] || moduleName;
3913
+ const servercraftModulesDir = await findServercraftModules();
3914
+ if (servercraftModulesDir) {
3915
+ const sourceModuleDir = import_path4.default.join(servercraftModulesDir, sourceDirName);
3916
+ if (await fileExists(sourceModuleDir)) {
3917
+ await copyModuleFromSource(sourceModuleDir, moduleDir);
3918
+ return;
3919
+ }
3563
3920
  }
3564
3921
  switch (moduleName) {
3565
3922
  case "auth":
@@ -3815,12 +4172,1782 @@ dbCommand.command("status").description("Show migration status").action(async ()
3815
4172
  }
3816
4173
  });
3817
4174
 
4175
+ // src/cli/commands/docs.ts
4176
+ var import_commander5 = require("commander");
4177
+ var import_path6 = __toESM(require("path"), 1);
4178
+ var import_promises4 = __toESM(require("fs/promises"), 1);
4179
+ var import_ora6 = __toESM(require("ora"), 1);
4180
+ var import_chalk6 = __toESM(require("chalk"), 1);
4181
+
4182
+ // src/cli/utils/docs-generator.ts
4183
+ var import_promises3 = __toESM(require("fs/promises"), 1);
4184
+ var import_path5 = __toESM(require("path"), 1);
4185
+ var import_ora5 = __toESM(require("ora"), 1);
4186
+
4187
+ // src/core/server.ts
4188
+ var import_fastify = __toESM(require("fastify"), 1);
4189
+
4190
+ // src/core/logger.ts
4191
+ var import_pino = __toESM(require("pino"), 1);
4192
+ var defaultConfig = {
4193
+ level: process.env.LOG_LEVEL || "info",
4194
+ pretty: process.env.NODE_ENV !== "production",
4195
+ name: "servcraft"
4196
+ };
4197
+ function createLogger(config2 = {}) {
4198
+ const mergedConfig = { ...defaultConfig, ...config2 };
4199
+ const transport = mergedConfig.pretty ? {
4200
+ target: "pino-pretty",
4201
+ options: {
4202
+ colorize: true,
4203
+ translateTime: "SYS:standard",
4204
+ ignore: "pid,hostname"
4205
+ }
4206
+ } : void 0;
4207
+ return (0, import_pino.default)({
4208
+ name: mergedConfig.name,
4209
+ level: mergedConfig.level,
4210
+ transport,
4211
+ formatters: {
4212
+ level: (label) => ({ level: label })
4213
+ },
4214
+ timestamp: import_pino.default.stdTimeFunctions.isoTime
4215
+ });
4216
+ }
4217
+ var logger = createLogger();
4218
+
4219
+ // src/core/server.ts
4220
+ var defaultConfig2 = {
4221
+ port: parseInt(process.env.PORT || "3000", 10),
4222
+ host: process.env.HOST || "0.0.0.0",
4223
+ trustProxy: true,
4224
+ bodyLimit: 1048576,
4225
+ // 1MB
4226
+ requestTimeout: 3e4
4227
+ // 30s
4228
+ };
4229
+ var Server = class {
4230
+ app;
4231
+ config;
4232
+ logger;
4233
+ isShuttingDown = false;
4234
+ constructor(config2 = {}) {
4235
+ this.config = { ...defaultConfig2, ...config2 };
4236
+ this.logger = this.config.logger || logger;
4237
+ const fastifyOptions = {
4238
+ logger: this.logger,
4239
+ trustProxy: this.config.trustProxy,
4240
+ bodyLimit: this.config.bodyLimit,
4241
+ requestTimeout: this.config.requestTimeout
4242
+ };
4243
+ this.app = (0, import_fastify.default)(fastifyOptions);
4244
+ this.setupHealthCheck();
4245
+ this.setupGracefulShutdown();
4246
+ }
4247
+ get instance() {
4248
+ return this.app;
4249
+ }
4250
+ setupHealthCheck() {
4251
+ this.app.get("/health", async (_request, reply) => {
4252
+ const healthcheck = {
4253
+ status: "ok",
4254
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4255
+ uptime: process.uptime(),
4256
+ memory: process.memoryUsage(),
4257
+ version: process.env.npm_package_version || "0.1.0"
4258
+ };
4259
+ return reply.status(200).send(healthcheck);
4260
+ });
4261
+ this.app.get("/ready", async (_request, reply) => {
4262
+ if (this.isShuttingDown) {
4263
+ return reply.status(503).send({ status: "shutting_down" });
4264
+ }
4265
+ return reply.status(200).send({ status: "ready" });
4266
+ });
4267
+ }
4268
+ setupGracefulShutdown() {
4269
+ const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
4270
+ signals.forEach((signal) => {
4271
+ process.on(signal, async () => {
4272
+ this.logger.info(`Received ${signal}, starting graceful shutdown...`);
4273
+ await this.shutdown();
4274
+ });
4275
+ });
4276
+ process.on("uncaughtException", async (error2) => {
4277
+ this.logger.error({ err: error2 }, "Uncaught exception");
4278
+ await this.shutdown(1);
4279
+ });
4280
+ process.on("unhandledRejection", async (reason) => {
4281
+ this.logger.error({ err: reason }, "Unhandled rejection");
4282
+ await this.shutdown(1);
4283
+ });
4284
+ }
4285
+ async shutdown(exitCode = 0) {
4286
+ if (this.isShuttingDown) {
4287
+ return;
4288
+ }
4289
+ this.isShuttingDown = true;
4290
+ this.logger.info("Graceful shutdown initiated...");
4291
+ const shutdownTimeout = setTimeout(() => {
4292
+ this.logger.error("Graceful shutdown timeout, forcing exit");
4293
+ process.exit(1);
4294
+ }, 3e4);
4295
+ try {
4296
+ await this.app.close();
4297
+ this.logger.info("Server closed successfully");
4298
+ clearTimeout(shutdownTimeout);
4299
+ process.exit(exitCode);
4300
+ } catch (error2) {
4301
+ this.logger.error({ err: error2 }, "Error during shutdown");
4302
+ clearTimeout(shutdownTimeout);
4303
+ process.exit(1);
4304
+ }
4305
+ }
4306
+ async start() {
4307
+ try {
4308
+ await this.app.listen({
4309
+ port: this.config.port,
4310
+ host: this.config.host
4311
+ });
4312
+ this.logger.info(`Server listening on ${this.config.host}:${this.config.port}`);
4313
+ } catch (error2) {
4314
+ this.logger.error({ err: error2 }, "Failed to start server");
4315
+ throw error2;
4316
+ }
4317
+ }
4318
+ };
4319
+ function createServer(config2 = {}) {
4320
+ return new Server(config2);
4321
+ }
4322
+
4323
+ // src/utils/errors.ts
4324
+ var AppError = class _AppError extends Error {
4325
+ statusCode;
4326
+ isOperational;
4327
+ errors;
4328
+ constructor(message, statusCode = 500, isOperational = true, errors) {
4329
+ super(message);
4330
+ this.statusCode = statusCode;
4331
+ this.isOperational = isOperational;
4332
+ this.errors = errors;
4333
+ Object.setPrototypeOf(this, _AppError.prototype);
4334
+ Error.captureStackTrace(this, this.constructor);
4335
+ }
4336
+ };
4337
+ var NotFoundError = class _NotFoundError extends AppError {
4338
+ constructor(resource = "Resource") {
4339
+ super(`${resource} not found`, 404);
4340
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
4341
+ }
4342
+ };
4343
+ var UnauthorizedError = class _UnauthorizedError extends AppError {
4344
+ constructor(message = "Unauthorized") {
4345
+ super(message, 401);
4346
+ Object.setPrototypeOf(this, _UnauthorizedError.prototype);
4347
+ }
4348
+ };
4349
+ var ForbiddenError = class _ForbiddenError extends AppError {
4350
+ constructor(message = "Forbidden") {
4351
+ super(message, 403);
4352
+ Object.setPrototypeOf(this, _ForbiddenError.prototype);
4353
+ }
4354
+ };
4355
+ var BadRequestError = class _BadRequestError extends AppError {
4356
+ constructor(message = "Bad request", errors) {
4357
+ super(message, 400, true, errors);
4358
+ Object.setPrototypeOf(this, _BadRequestError.prototype);
4359
+ }
4360
+ };
4361
+ var ConflictError = class _ConflictError extends AppError {
4362
+ constructor(message = "Resource already exists") {
4363
+ super(message, 409);
4364
+ Object.setPrototypeOf(this, _ConflictError.prototype);
4365
+ }
4366
+ };
4367
+ var ValidationError = class _ValidationError extends AppError {
4368
+ constructor(errors) {
4369
+ super("Validation failed", 422, true, errors);
4370
+ Object.setPrototypeOf(this, _ValidationError.prototype);
4371
+ }
4372
+ };
4373
+ function isAppError(error2) {
4374
+ return error2 instanceof AppError;
4375
+ }
4376
+
4377
+ // src/config/env.ts
4378
+ var import_zod = require("zod");
4379
+ var import_dotenv = __toESM(require("dotenv"), 1);
4380
+ import_dotenv.default.config();
4381
+ var envSchema = import_zod.z.object({
4382
+ // Server
4383
+ NODE_ENV: import_zod.z.enum(["development", "staging", "production", "test"]).default("development"),
4384
+ PORT: import_zod.z.string().transform(Number).default("3000"),
4385
+ HOST: import_zod.z.string().default("0.0.0.0"),
4386
+ // Database
4387
+ DATABASE_URL: import_zod.z.string().optional(),
4388
+ // JWT
4389
+ JWT_SECRET: import_zod.z.string().min(32).optional(),
4390
+ JWT_ACCESS_EXPIRES_IN: import_zod.z.string().default("15m"),
4391
+ JWT_REFRESH_EXPIRES_IN: import_zod.z.string().default("7d"),
4392
+ // Security
4393
+ CORS_ORIGIN: import_zod.z.string().default("*"),
4394
+ RATE_LIMIT_MAX: import_zod.z.string().transform(Number).default("100"),
4395
+ RATE_LIMIT_WINDOW_MS: import_zod.z.string().transform(Number).default("60000"),
4396
+ // Email
4397
+ SMTP_HOST: import_zod.z.string().optional(),
4398
+ SMTP_PORT: import_zod.z.string().transform(Number).optional(),
4399
+ SMTP_USER: import_zod.z.string().optional(),
4400
+ SMTP_PASS: import_zod.z.string().optional(),
4401
+ SMTP_FROM: import_zod.z.string().optional(),
4402
+ // Redis (optional)
4403
+ REDIS_URL: import_zod.z.string().optional(),
4404
+ // Logging
4405
+ LOG_LEVEL: import_zod.z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info")
4406
+ });
4407
+ function validateEnv() {
4408
+ const parsed = envSchema.safeParse(process.env);
4409
+ if (!parsed.success) {
4410
+ logger.error({ errors: parsed.error.flatten().fieldErrors }, "Invalid environment variables");
4411
+ throw new Error("Invalid environment variables");
4412
+ }
4413
+ return parsed.data;
4414
+ }
4415
+ var env = validateEnv();
4416
+ function isProduction() {
4417
+ return env.NODE_ENV === "production";
4418
+ }
4419
+
4420
+ // src/config/index.ts
4421
+ function parseCorsOrigin(origin) {
4422
+ if (origin === "*") return "*";
4423
+ if (origin.includes(",")) {
4424
+ return origin.split(",").map((o) => o.trim());
4425
+ }
4426
+ return origin;
4427
+ }
4428
+ function createConfig() {
4429
+ return {
4430
+ env,
4431
+ server: {
4432
+ port: env.PORT,
4433
+ host: env.HOST
4434
+ },
4435
+ jwt: {
4436
+ secret: env.JWT_SECRET || "change-me-in-production-please-32chars",
4437
+ accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
4438
+ refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN
4439
+ },
4440
+ security: {
4441
+ corsOrigin: parseCorsOrigin(env.CORS_ORIGIN),
4442
+ rateLimit: {
4443
+ max: env.RATE_LIMIT_MAX,
4444
+ windowMs: env.RATE_LIMIT_WINDOW_MS
4445
+ }
4446
+ },
4447
+ email: {
4448
+ host: env.SMTP_HOST,
4449
+ port: env.SMTP_PORT,
4450
+ user: env.SMTP_USER,
4451
+ pass: env.SMTP_PASS,
4452
+ from: env.SMTP_FROM
4453
+ },
4454
+ database: {
4455
+ url: env.DATABASE_URL
4456
+ },
4457
+ redis: {
4458
+ url: env.REDIS_URL
4459
+ }
4460
+ };
4461
+ }
4462
+ var config = createConfig();
4463
+
4464
+ // src/middleware/error-handler.ts
4465
+ function registerErrorHandler(app) {
4466
+ app.setErrorHandler(
4467
+ (error2, request, reply) => {
4468
+ logger.error(
4469
+ {
4470
+ err: error2,
4471
+ requestId: request.id,
4472
+ method: request.method,
4473
+ url: request.url
4474
+ },
4475
+ "Request error"
4476
+ );
4477
+ if (isAppError(error2)) {
4478
+ return reply.status(error2.statusCode).send({
4479
+ success: false,
4480
+ message: error2.message,
4481
+ errors: error2.errors,
4482
+ ...isProduction() ? {} : { stack: error2.stack }
4483
+ });
4484
+ }
4485
+ if ("validation" in error2 && error2.validation) {
4486
+ const errors = {};
4487
+ for (const err of error2.validation) {
4488
+ const field = err.instancePath?.replace("/", "") || "body";
4489
+ if (!errors[field]) {
4490
+ errors[field] = [];
4491
+ }
4492
+ errors[field].push(err.message || "Invalid value");
4493
+ }
4494
+ return reply.status(400).send({
4495
+ success: false,
4496
+ message: "Validation failed",
4497
+ errors
4498
+ });
4499
+ }
4500
+ if ("statusCode" in error2 && typeof error2.statusCode === "number") {
4501
+ return reply.status(error2.statusCode).send({
4502
+ success: false,
4503
+ message: error2.message,
4504
+ ...isProduction() ? {} : { stack: error2.stack }
4505
+ });
4506
+ }
4507
+ return reply.status(500).send({
4508
+ success: false,
4509
+ message: isProduction() ? "Internal server error" : error2.message,
4510
+ ...isProduction() ? {} : { stack: error2.stack }
4511
+ });
4512
+ }
4513
+ );
4514
+ app.setNotFoundHandler((request, reply) => {
4515
+ return reply.status(404).send({
4516
+ success: false,
4517
+ message: `Route ${request.method} ${request.url} not found`
4518
+ });
4519
+ });
4520
+ }
4521
+
4522
+ // src/middleware/security.ts
4523
+ var import_helmet = __toESM(require("@fastify/helmet"), 1);
4524
+ var import_cors = __toESM(require("@fastify/cors"), 1);
4525
+ var import_rate_limit = __toESM(require("@fastify/rate-limit"), 1);
4526
+ var defaultOptions = {
4527
+ helmet: true,
4528
+ cors: true,
4529
+ rateLimit: true
4530
+ };
4531
+ async function registerSecurity(app, options = {}) {
4532
+ const opts = { ...defaultOptions, ...options };
4533
+ if (opts.helmet) {
4534
+ await app.register(import_helmet.default, {
4535
+ contentSecurityPolicy: {
4536
+ directives: {
4537
+ defaultSrc: ["'self'"],
4538
+ styleSrc: ["'self'", "'unsafe-inline'"],
4539
+ scriptSrc: ["'self'"],
4540
+ imgSrc: ["'self'", "data:", "https:"]
4541
+ }
4542
+ },
4543
+ crossOriginEmbedderPolicy: false
4544
+ });
4545
+ logger.debug("Helmet security headers enabled");
4546
+ }
4547
+ if (opts.cors) {
4548
+ await app.register(import_cors.default, {
4549
+ origin: config.security.corsOrigin,
4550
+ credentials: true,
4551
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
4552
+ allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
4553
+ exposedHeaders: ["X-Total-Count", "X-Page", "X-Limit"],
4554
+ maxAge: 86400
4555
+ // 24 hours
4556
+ });
4557
+ logger.debug({ origin: config.security.corsOrigin }, "CORS enabled");
4558
+ }
4559
+ if (opts.rateLimit) {
4560
+ await app.register(import_rate_limit.default, {
4561
+ max: config.security.rateLimit.max,
4562
+ timeWindow: config.security.rateLimit.windowMs,
4563
+ errorResponseBuilder: (_request, context) => ({
4564
+ success: false,
4565
+ message: "Too many requests, please try again later",
4566
+ retryAfter: context.after
4567
+ }),
4568
+ keyGenerator: (request) => {
4569
+ return request.headers["x-forwarded-for"]?.toString().split(",")[0] || request.ip || "unknown";
4570
+ }
4571
+ });
4572
+ logger.debug(
4573
+ {
4574
+ max: config.security.rateLimit.max,
4575
+ windowMs: config.security.rateLimit.windowMs
4576
+ },
4577
+ "Rate limiting enabled"
4578
+ );
4579
+ }
4580
+ }
4581
+
4582
+ // src/modules/swagger/swagger.service.ts
4583
+ var import_swagger = __toESM(require("@fastify/swagger"), 1);
4584
+ var import_swagger_ui = __toESM(require("@fastify/swagger-ui"), 1);
4585
+ var defaultConfig3 = {
4586
+ title: "Servcraft API",
4587
+ description: "API documentation generated by Servcraft",
4588
+ version: "1.0.0",
4589
+ tags: [
4590
+ { name: "Auth", description: "Authentication endpoints" },
4591
+ { name: "Users", description: "User management endpoints" },
4592
+ { name: "Health", description: "Health check endpoints" }
4593
+ ]
4594
+ };
4595
+ async function registerSwagger(app, customConfig) {
4596
+ const swaggerConfig = { ...defaultConfig3, ...customConfig };
4597
+ await app.register(import_swagger.default, {
4598
+ openapi: {
4599
+ openapi: "3.0.3",
4600
+ info: {
4601
+ title: swaggerConfig.title,
4602
+ description: swaggerConfig.description,
4603
+ version: swaggerConfig.version,
4604
+ contact: swaggerConfig.contact,
4605
+ license: swaggerConfig.license
4606
+ },
4607
+ servers: swaggerConfig.servers || [
4608
+ {
4609
+ url: `http://localhost:${config.server.port}`,
4610
+ description: "Development server"
4611
+ }
4612
+ ],
4613
+ tags: swaggerConfig.tags,
4614
+ components: {
4615
+ securitySchemes: {
4616
+ bearerAuth: {
4617
+ type: "http",
4618
+ scheme: "bearer",
4619
+ bearerFormat: "JWT",
4620
+ description: "Enter your JWT token"
4621
+ }
4622
+ }
4623
+ }
4624
+ }
4625
+ });
4626
+ await app.register(import_swagger_ui.default, {
4627
+ routePrefix: "/docs",
4628
+ uiConfig: {
4629
+ docExpansion: "list",
4630
+ deepLinking: true,
4631
+ displayRequestDuration: true,
4632
+ filter: true,
4633
+ showExtensions: true,
4634
+ showCommonExtensions: true
4635
+ },
4636
+ staticCSP: true,
4637
+ transformStaticCSP: (header) => header
4638
+ });
4639
+ logger.info("Swagger documentation registered at /docs");
4640
+ }
4641
+
4642
+ // src/modules/auth/index.ts
4643
+ var import_jwt = __toESM(require("@fastify/jwt"), 1);
4644
+ var import_cookie = __toESM(require("@fastify/cookie"), 1);
4645
+
4646
+ // src/modules/auth/auth.service.ts
4647
+ var import_bcryptjs = __toESM(require("bcryptjs"), 1);
4648
+ var import_ioredis = require("ioredis");
4649
+ var AuthService = class {
4650
+ app;
4651
+ SALT_ROUNDS = 12;
4652
+ redis = null;
4653
+ BLACKLIST_PREFIX = "auth:blacklist:";
4654
+ BLACKLIST_TTL = 7 * 24 * 60 * 60;
4655
+ // 7 days in seconds
4656
+ constructor(app, redisUrl) {
4657
+ this.app = app;
4658
+ if (redisUrl || process.env.REDIS_URL) {
4659
+ try {
4660
+ this.redis = new import_ioredis.Redis(redisUrl || process.env.REDIS_URL || "redis://localhost:6379");
4661
+ this.redis.on("connect", () => {
4662
+ logger.info("Auth service connected to Redis for token blacklist");
4663
+ });
4664
+ this.redis.on("error", (error2) => {
4665
+ logger.error({ err: error2 }, "Redis connection error in Auth service");
4666
+ });
4667
+ } catch (error2) {
4668
+ logger.warn({ err: error2 }, "Failed to connect to Redis, using in-memory blacklist");
4669
+ this.redis = null;
4670
+ }
4671
+ } else {
4672
+ logger.warn(
4673
+ "No REDIS_URL provided, using in-memory token blacklist (not recommended for production)"
4674
+ );
4675
+ }
4676
+ }
4677
+ async hashPassword(password) {
4678
+ return import_bcryptjs.default.hash(password, this.SALT_ROUNDS);
4679
+ }
4680
+ async verifyPassword(password, hash) {
4681
+ return import_bcryptjs.default.compare(password, hash);
4682
+ }
4683
+ generateTokenPair(user) {
4684
+ const accessPayload = {
4685
+ sub: user.id,
4686
+ email: user.email,
4687
+ role: user.role,
4688
+ type: "access"
4689
+ };
4690
+ const refreshPayload = {
4691
+ sub: user.id,
4692
+ email: user.email,
4693
+ role: user.role,
4694
+ type: "refresh"
4695
+ };
4696
+ const accessToken = this.app.jwt.sign(accessPayload, {
4697
+ expiresIn: config.jwt.accessExpiresIn
4698
+ });
4699
+ const refreshToken = this.app.jwt.sign(refreshPayload, {
4700
+ expiresIn: config.jwt.refreshExpiresIn
4701
+ });
4702
+ const expiresIn = this.parseExpiration(config.jwt.accessExpiresIn);
4703
+ return { accessToken, refreshToken, expiresIn };
4704
+ }
4705
+ parseExpiration(expiration) {
4706
+ const match = expiration.match(/^(\d+)([smhd])$/);
4707
+ if (!match) return 900;
4708
+ const value = parseInt(match[1] || "0", 10);
4709
+ const unit = match[2];
4710
+ switch (unit) {
4711
+ case "s":
4712
+ return value;
4713
+ case "m":
4714
+ return value * 60;
4715
+ case "h":
4716
+ return value * 3600;
4717
+ case "d":
4718
+ return value * 86400;
4719
+ default:
4720
+ return 900;
4721
+ }
4722
+ }
4723
+ async verifyAccessToken(token) {
4724
+ try {
4725
+ if (await this.isTokenBlacklisted(token)) {
4726
+ throw new UnauthorizedError("Token has been revoked");
4727
+ }
4728
+ const payload = this.app.jwt.verify(token);
4729
+ if (payload.type !== "access") {
4730
+ throw new UnauthorizedError("Invalid token type");
4731
+ }
4732
+ return payload;
4733
+ } catch (error2) {
4734
+ if (error2 instanceof UnauthorizedError) throw error2;
4735
+ logger.debug({ err: error2 }, "Token verification failed");
4736
+ throw new UnauthorizedError("Invalid or expired token");
4737
+ }
4738
+ }
4739
+ async verifyRefreshToken(token) {
4740
+ try {
4741
+ if (await this.isTokenBlacklisted(token)) {
4742
+ throw new UnauthorizedError("Token has been revoked");
4743
+ }
4744
+ const payload = this.app.jwt.verify(token);
4745
+ if (payload.type !== "refresh") {
4746
+ throw new UnauthorizedError("Invalid token type");
4747
+ }
4748
+ return payload;
4749
+ } catch (error2) {
4750
+ if (error2 instanceof UnauthorizedError) throw error2;
4751
+ logger.debug({ err: error2 }, "Refresh token verification failed");
4752
+ throw new UnauthorizedError("Invalid or expired refresh token");
4753
+ }
4754
+ }
4755
+ /**
4756
+ * Blacklist a token (JWT revocation)
4757
+ * Uses Redis if available, falls back to in-memory Set
4758
+ */
4759
+ async blacklistToken(token) {
4760
+ if (this.redis) {
4761
+ try {
4762
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
4763
+ await this.redis.setex(key, this.BLACKLIST_TTL, "1");
4764
+ logger.debug("Token blacklisted in Redis");
4765
+ } catch (error2) {
4766
+ logger.error({ err: error2 }, "Failed to blacklist token in Redis");
4767
+ throw new Error("Failed to revoke token");
4768
+ }
4769
+ } else {
4770
+ logger.warn("Using in-memory blacklist - not suitable for multi-instance deployments");
4771
+ }
4772
+ }
4773
+ /**
4774
+ * Check if a token is blacklisted
4775
+ * Uses Redis if available, falls back to always returning false
4776
+ */
4777
+ async isTokenBlacklisted(token) {
4778
+ if (this.redis) {
4779
+ try {
4780
+ const key = `${this.BLACKLIST_PREFIX}${token}`;
4781
+ const result = await this.redis.exists(key);
4782
+ return result === 1;
4783
+ } catch (error2) {
4784
+ logger.error({ err: error2 }, "Failed to check token blacklist in Redis");
4785
+ return false;
4786
+ }
4787
+ }
4788
+ return false;
4789
+ }
4790
+ /**
4791
+ * Get count of blacklisted tokens (Redis only)
4792
+ */
4793
+ async getBlacklistCount() {
4794
+ if (this.redis) {
4795
+ try {
4796
+ const keys = await this.redis.keys(`${this.BLACKLIST_PREFIX}*`);
4797
+ return keys.length;
4798
+ } catch (error2) {
4799
+ logger.error({ err: error2 }, "Failed to get blacklist count from Redis");
4800
+ return 0;
4801
+ }
4802
+ }
4803
+ return 0;
4804
+ }
4805
+ /**
4806
+ * Close Redis connection
4807
+ */
4808
+ async close() {
4809
+ if (this.redis) {
4810
+ await this.redis.quit();
4811
+ logger.info("Auth service Redis connection closed");
4812
+ }
4813
+ }
4814
+ // OAuth support methods - to be implemented with user repository
4815
+ async findUserByEmail(_email) {
4816
+ return null;
4817
+ }
4818
+ async createUserFromOAuth(data) {
4819
+ const user = {
4820
+ id: `oauth_${Date.now()}`,
4821
+ email: data.email,
4822
+ role: "user"
4823
+ };
4824
+ logger.info({ email: data.email }, "Created user from OAuth");
4825
+ return user;
4826
+ }
4827
+ async generateTokensForUser(userId) {
4828
+ const user = {
4829
+ id: userId,
4830
+ email: "",
4831
+ // Would be fetched from database in production
4832
+ role: "user"
4833
+ };
4834
+ return this.generateTokenPair(user);
4835
+ }
4836
+ async verifyPasswordById(userId, _password) {
4837
+ logger.debug({ userId }, "Password verification requested");
4838
+ return false;
4839
+ }
4840
+ };
4841
+ function createAuthService(app) {
4842
+ return new AuthService(app);
4843
+ }
4844
+
4845
+ // src/modules/auth/schemas.ts
4846
+ var import_zod2 = require("zod");
4847
+ var loginSchema = import_zod2.z.object({
4848
+ email: import_zod2.z.string().email("Invalid email address"),
4849
+ password: import_zod2.z.string().min(1, "Password is required")
4850
+ });
4851
+ var registerSchema = import_zod2.z.object({
4852
+ email: import_zod2.z.string().email("Invalid email address"),
4853
+ 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"),
4854
+ name: import_zod2.z.string().min(2, "Name must be at least 2 characters").optional()
4855
+ });
4856
+ var refreshTokenSchema = import_zod2.z.object({
4857
+ refreshToken: import_zod2.z.string().min(1, "Refresh token is required")
4858
+ });
4859
+ var passwordResetRequestSchema = import_zod2.z.object({
4860
+ email: import_zod2.z.string().email("Invalid email address")
4861
+ });
4862
+ var passwordResetConfirmSchema = import_zod2.z.object({
4863
+ token: import_zod2.z.string().min(1, "Token is required"),
4864
+ 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")
4865
+ });
4866
+ var changePasswordSchema = import_zod2.z.object({
4867
+ currentPassword: import_zod2.z.string().min(1, "Current password is required"),
4868
+ 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")
4869
+ });
4870
+
4871
+ // src/utils/response.ts
4872
+ function success2(reply, data, statusCode = 200) {
4873
+ const response = {
4874
+ success: true,
4875
+ data
4876
+ };
4877
+ return reply.status(statusCode).send(response);
4878
+ }
4879
+ function created(reply, data) {
4880
+ return success2(reply, data, 201);
4881
+ }
4882
+ function noContent(reply) {
4883
+ return reply.status(204).send();
4884
+ }
4885
+
4886
+ // src/modules/validation/validator.ts
4887
+ var import_zod3 = require("zod");
4888
+ function validateBody(schema, data) {
4889
+ const result = schema.safeParse(data);
4890
+ if (!result.success) {
4891
+ throw new ValidationError(formatZodErrors(result.error));
4892
+ }
4893
+ return result.data;
4894
+ }
4895
+ function validateQuery(schema, data) {
4896
+ const result = schema.safeParse(data);
4897
+ if (!result.success) {
4898
+ throw new ValidationError(formatZodErrors(result.error));
4899
+ }
4900
+ return result.data;
4901
+ }
4902
+ function formatZodErrors(error2) {
4903
+ const errors = {};
4904
+ for (const issue of error2.issues) {
4905
+ const path9 = issue.path.join(".") || "root";
4906
+ if (!errors[path9]) {
4907
+ errors[path9] = [];
4908
+ }
4909
+ errors[path9].push(issue.message);
4910
+ }
4911
+ return errors;
4912
+ }
4913
+ var idParamSchema = import_zod3.z.object({
4914
+ id: import_zod3.z.string().uuid("Invalid ID format")
4915
+ });
4916
+ var paginationSchema = import_zod3.z.object({
4917
+ page: import_zod3.z.string().transform(Number).optional().default("1"),
4918
+ limit: import_zod3.z.string().transform(Number).optional().default("20"),
4919
+ sortBy: import_zod3.z.string().optional(),
4920
+ sortOrder: import_zod3.z.enum(["asc", "desc"]).optional().default("asc")
4921
+ });
4922
+ var searchSchema = import_zod3.z.object({
4923
+ q: import_zod3.z.string().min(1, "Search query is required").optional(),
4924
+ search: import_zod3.z.string().min(1).optional()
4925
+ });
4926
+ var emailSchema = import_zod3.z.string().email("Invalid email address");
4927
+ 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");
4928
+ var urlSchema = import_zod3.z.string().url("Invalid URL format");
4929
+ var phoneSchema = import_zod3.z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format");
4930
+ var dateSchema = import_zod3.z.coerce.date();
4931
+ var futureDateSchema = import_zod3.z.coerce.date().refine((date) => date > /* @__PURE__ */ new Date(), "Date must be in the future");
4932
+ var pastDateSchema = import_zod3.z.coerce.date().refine((date) => date < /* @__PURE__ */ new Date(), "Date must be in the past");
4933
+
4934
+ // src/modules/auth/auth.controller.ts
4935
+ var AuthController = class {
4936
+ constructor(authService, userService) {
4937
+ this.authService = authService;
4938
+ this.userService = userService;
4939
+ }
4940
+ async register(request, reply) {
4941
+ const data = validateBody(registerSchema, request.body);
4942
+ const existingUser = await this.userService.findByEmail(data.email);
4943
+ if (existingUser) {
4944
+ throw new BadRequestError("Email already registered");
4945
+ }
4946
+ const hashedPassword = await this.authService.hashPassword(data.password);
4947
+ const user = await this.userService.create({
4948
+ email: data.email,
4949
+ password: hashedPassword,
4950
+ name: data.name
4951
+ });
4952
+ const tokens = this.authService.generateTokenPair({
4953
+ id: user.id,
4954
+ email: user.email,
4955
+ role: user.role
4956
+ });
4957
+ created(reply, {
4958
+ user: {
4959
+ id: user.id,
4960
+ email: user.email,
4961
+ name: user.name,
4962
+ role: user.role
4963
+ },
4964
+ ...tokens
4965
+ });
4966
+ }
4967
+ async login(request, reply) {
4968
+ const data = validateBody(loginSchema, request.body);
4969
+ const user = await this.userService.findByEmail(data.email);
4970
+ if (!user) {
4971
+ throw new UnauthorizedError("Invalid credentials");
4972
+ }
4973
+ if (user.status !== "active") {
4974
+ throw new UnauthorizedError("Account is not active");
4975
+ }
4976
+ const isValidPassword = await this.authService.verifyPassword(data.password, user.password);
4977
+ if (!isValidPassword) {
4978
+ throw new UnauthorizedError("Invalid credentials");
4979
+ }
4980
+ await this.userService.updateLastLogin(user.id);
4981
+ const tokens = this.authService.generateTokenPair({
4982
+ id: user.id,
4983
+ email: user.email,
4984
+ role: user.role
4985
+ });
4986
+ success2(reply, {
4987
+ user: {
4988
+ id: user.id,
4989
+ email: user.email,
4990
+ name: user.name,
4991
+ role: user.role
4992
+ },
4993
+ ...tokens
4994
+ });
4995
+ }
4996
+ async refresh(request, reply) {
4997
+ const data = validateBody(refreshTokenSchema, request.body);
4998
+ const payload = await this.authService.verifyRefreshToken(data.refreshToken);
4999
+ const user = await this.userService.findById(payload.sub);
5000
+ if (!user || user.status !== "active") {
5001
+ throw new UnauthorizedError("User not found or inactive");
5002
+ }
5003
+ await this.authService.blacklistToken(data.refreshToken);
5004
+ const tokens = this.authService.generateTokenPair({
5005
+ id: user.id,
5006
+ email: user.email,
5007
+ role: user.role
5008
+ });
5009
+ success2(reply, tokens);
5010
+ }
5011
+ async logout(request, reply) {
5012
+ const authHeader = request.headers.authorization;
5013
+ if (authHeader?.startsWith("Bearer ")) {
5014
+ const token = authHeader.substring(7);
5015
+ await this.authService.blacklistToken(token);
5016
+ }
5017
+ success2(reply, { message: "Logged out successfully" });
5018
+ }
5019
+ async me(request, reply) {
5020
+ const authRequest = request;
5021
+ const user = await this.userService.findById(authRequest.user.id);
5022
+ if (!user) {
5023
+ throw new UnauthorizedError("User not found");
5024
+ }
5025
+ success2(reply, {
5026
+ id: user.id,
5027
+ email: user.email,
5028
+ name: user.name,
5029
+ role: user.role,
5030
+ status: user.status,
5031
+ createdAt: user.createdAt
5032
+ });
5033
+ }
5034
+ async changePassword(request, reply) {
5035
+ const authRequest = request;
5036
+ const data = validateBody(changePasswordSchema, request.body);
5037
+ const user = await this.userService.findById(authRequest.user.id);
5038
+ if (!user) {
5039
+ throw new UnauthorizedError("User not found");
5040
+ }
5041
+ const isValidPassword = await this.authService.verifyPassword(
5042
+ data.currentPassword,
5043
+ user.password
5044
+ );
5045
+ if (!isValidPassword) {
5046
+ throw new BadRequestError("Current password is incorrect");
5047
+ }
5048
+ const hashedPassword = await this.authService.hashPassword(data.newPassword);
5049
+ await this.userService.updatePassword(user.id, hashedPassword);
5050
+ success2(reply, { message: "Password changed successfully" });
5051
+ }
5052
+ };
5053
+ function createAuthController(authService, userService) {
5054
+ return new AuthController(authService, userService);
5055
+ }
5056
+
5057
+ // src/modules/auth/auth.middleware.ts
5058
+ function createAuthMiddleware(authService) {
5059
+ return async function authenticate(request, _reply) {
5060
+ const authHeader = request.headers.authorization;
5061
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
5062
+ throw new UnauthorizedError("Missing or invalid authorization header");
5063
+ }
5064
+ const token = authHeader.substring(7);
5065
+ const payload = await authService.verifyAccessToken(token);
5066
+ request.user = {
5067
+ id: payload.sub,
5068
+ email: payload.email,
5069
+ role: payload.role
5070
+ };
5071
+ };
5072
+ }
5073
+ function createRoleMiddleware(allowedRoles) {
5074
+ return async function authorize(request, _reply) {
5075
+ const user = request.user;
5076
+ if (!user) {
5077
+ throw new UnauthorizedError("Authentication required");
5078
+ }
5079
+ if (!allowedRoles.includes(user.role)) {
5080
+ throw new ForbiddenError("Insufficient permissions");
5081
+ }
5082
+ };
5083
+ }
5084
+
5085
+ // src/modules/auth/auth.routes.ts
5086
+ function registerAuthRoutes(app, controller, authService) {
5087
+ const authenticate = createAuthMiddleware(authService);
5088
+ app.post("/auth/register", controller.register.bind(controller));
5089
+ app.post("/auth/login", controller.login.bind(controller));
5090
+ app.post("/auth/refresh", controller.refresh.bind(controller));
5091
+ app.post("/auth/logout", { preHandler: [authenticate] }, controller.logout.bind(controller));
5092
+ app.get("/auth/me", { preHandler: [authenticate] }, controller.me.bind(controller));
5093
+ app.post(
5094
+ "/auth/change-password",
5095
+ { preHandler: [authenticate] },
5096
+ controller.changePassword.bind(controller)
5097
+ );
5098
+ }
5099
+
5100
+ // src/database/prisma.ts
5101
+ var import_client = require("@prisma/client");
5102
+ var prismaClientSingleton = () => {
5103
+ return new import_client.PrismaClient({
5104
+ log: isProduction() ? ["error"] : ["query", "info", "warn", "error"],
5105
+ errorFormat: isProduction() ? "minimal" : "pretty"
5106
+ });
5107
+ };
5108
+ var prisma = globalThis.__prisma ?? prismaClientSingleton();
5109
+ if (!isProduction()) {
5110
+ globalThis.__prisma = prisma;
5111
+ }
5112
+
5113
+ // src/utils/pagination.ts
5114
+ var DEFAULT_PAGE = 1;
5115
+ var DEFAULT_LIMIT = 20;
5116
+ var MAX_LIMIT = 100;
5117
+ function parsePaginationParams(query) {
5118
+ const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
5119
+ const limit = Math.min(
5120
+ MAX_LIMIT,
5121
+ Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10))
5122
+ );
5123
+ const sortBy = typeof query.sortBy === "string" ? query.sortBy : void 0;
5124
+ const sortOrder = query.sortOrder === "desc" ? "desc" : "asc";
5125
+ return { page, limit, sortBy, sortOrder };
5126
+ }
5127
+ function createPaginatedResult(data, total, params) {
5128
+ const totalPages = Math.ceil(total / params.limit);
5129
+ return {
5130
+ data,
5131
+ meta: {
5132
+ total,
5133
+ page: params.page,
5134
+ limit: params.limit,
5135
+ totalPages,
5136
+ hasNextPage: params.page < totalPages,
5137
+ hasPrevPage: params.page > 1
5138
+ }
5139
+ };
5140
+ }
5141
+ function getSkip(params) {
5142
+ return (params.page - 1) * params.limit;
5143
+ }
5144
+
5145
+ // src/modules/user/user.repository.ts
5146
+ var import_client2 = require("@prisma/client");
5147
+ var UserRepository = class {
5148
+ /**
5149
+ * Find user by ID
5150
+ */
5151
+ async findById(id) {
5152
+ const user = await prisma.user.findUnique({
5153
+ where: { id }
5154
+ });
5155
+ if (!user) return null;
5156
+ return this.mapPrismaUserToUser(user);
5157
+ }
5158
+ /**
5159
+ * Find user by email (case-insensitive)
5160
+ */
5161
+ async findByEmail(email) {
5162
+ const user = await prisma.user.findUnique({
5163
+ where: { email: email.toLowerCase() }
5164
+ });
5165
+ if (!user) return null;
5166
+ return this.mapPrismaUserToUser(user);
5167
+ }
5168
+ /**
5169
+ * Find multiple users with pagination and filters
5170
+ */
5171
+ async findMany(params, filters) {
5172
+ const where = this.buildWhereClause(filters);
5173
+ const orderBy = this.buildOrderBy(params);
5174
+ const [data, total] = await Promise.all([
5175
+ prisma.user.findMany({
5176
+ where,
5177
+ orderBy,
5178
+ skip: getSkip(params),
5179
+ take: params.limit
5180
+ }),
5181
+ prisma.user.count({ where })
5182
+ ]);
5183
+ const mappedUsers = data.map((user) => this.mapPrismaUserToUser(user));
5184
+ return createPaginatedResult(mappedUsers, total, params);
5185
+ }
5186
+ /**
5187
+ * Create a new user
5188
+ */
5189
+ async create(data) {
5190
+ const user = await prisma.user.create({
5191
+ data: {
5192
+ email: data.email.toLowerCase(),
5193
+ password: data.password,
5194
+ name: data.name,
5195
+ role: this.mapRoleToEnum(data.role || "user"),
5196
+ status: import_client2.UserStatus.ACTIVE,
5197
+ emailVerified: false
5198
+ }
5199
+ });
5200
+ return this.mapPrismaUserToUser(user);
5201
+ }
5202
+ /**
5203
+ * Update user data
5204
+ */
5205
+ async update(id, data) {
5206
+ try {
5207
+ const user = await prisma.user.update({
5208
+ where: { id },
5209
+ data: {
5210
+ ...data.email && { email: data.email.toLowerCase() },
5211
+ ...data.name !== void 0 && { name: data.name },
5212
+ ...data.role && { role: this.mapRoleToEnum(data.role) },
5213
+ ...data.status && { status: this.mapStatusToEnum(data.status) },
5214
+ ...data.emailVerified !== void 0 && { emailVerified: data.emailVerified },
5215
+ ...data.metadata && { metadata: data.metadata }
5216
+ }
5217
+ });
5218
+ return this.mapPrismaUserToUser(user);
5219
+ } catch {
5220
+ return null;
5221
+ }
5222
+ }
5223
+ /**
5224
+ * Update user password
5225
+ */
5226
+ async updatePassword(id, password) {
5227
+ try {
5228
+ const user = await prisma.user.update({
5229
+ where: { id },
5230
+ data: { password }
5231
+ });
5232
+ return this.mapPrismaUserToUser(user);
5233
+ } catch {
5234
+ return null;
5235
+ }
5236
+ }
5237
+ /**
5238
+ * Update last login timestamp
5239
+ */
5240
+ async updateLastLogin(id) {
5241
+ try {
5242
+ const user = await prisma.user.update({
5243
+ where: { id },
5244
+ data: { lastLoginAt: /* @__PURE__ */ new Date() }
5245
+ });
5246
+ return this.mapPrismaUserToUser(user);
5247
+ } catch {
5248
+ return null;
5249
+ }
5250
+ }
5251
+ /**
5252
+ * Delete user by ID
5253
+ */
5254
+ async delete(id) {
5255
+ try {
5256
+ await prisma.user.delete({
5257
+ where: { id }
5258
+ });
5259
+ return true;
5260
+ } catch {
5261
+ return false;
5262
+ }
5263
+ }
5264
+ /**
5265
+ * Count users with optional filters
5266
+ */
5267
+ async count(filters) {
5268
+ const where = this.buildWhereClause(filters);
5269
+ return prisma.user.count({ where });
5270
+ }
5271
+ /**
5272
+ * Helper to clear all users (for testing only)
5273
+ * WARNING: This deletes all users from the database
5274
+ */
5275
+ async clear() {
5276
+ await prisma.user.deleteMany();
5277
+ }
5278
+ /**
5279
+ * Build Prisma where clause from filters
5280
+ */
5281
+ buildWhereClause(filters) {
5282
+ if (!filters) return {};
5283
+ return {
5284
+ ...filters.status && { status: this.mapStatusToEnum(filters.status) },
5285
+ ...filters.role && { role: this.mapRoleToEnum(filters.role) },
5286
+ ...filters.emailVerified !== void 0 && { emailVerified: filters.emailVerified },
5287
+ ...filters.search && {
5288
+ OR: [
5289
+ { email: { contains: filters.search, mode: "insensitive" } },
5290
+ { name: { contains: filters.search, mode: "insensitive" } }
5291
+ ]
5292
+ }
5293
+ };
5294
+ }
5295
+ /**
5296
+ * Build Prisma orderBy clause from pagination params
5297
+ */
5298
+ buildOrderBy(params) {
5299
+ if (!params.sortBy) {
5300
+ return { createdAt: "desc" };
5301
+ }
5302
+ return {
5303
+ [params.sortBy]: params.sortOrder || "asc"
5304
+ };
5305
+ }
5306
+ /**
5307
+ * Map Prisma User to application User type
5308
+ */
5309
+ mapPrismaUserToUser(prismaUser) {
5310
+ return {
5311
+ id: prismaUser.id,
5312
+ email: prismaUser.email,
5313
+ password: prismaUser.password,
5314
+ name: prismaUser.name ?? void 0,
5315
+ role: this.mapEnumToRole(prismaUser.role),
5316
+ status: this.mapEnumToStatus(prismaUser.status),
5317
+ emailVerified: prismaUser.emailVerified,
5318
+ lastLoginAt: prismaUser.lastLoginAt ?? void 0,
5319
+ metadata: prismaUser.metadata,
5320
+ createdAt: prismaUser.createdAt,
5321
+ updatedAt: prismaUser.updatedAt
5322
+ };
5323
+ }
5324
+ /**
5325
+ * Map application role to Prisma enum
5326
+ */
5327
+ mapRoleToEnum(role) {
5328
+ const roleMap = {
5329
+ user: import_client2.UserRole.USER,
5330
+ moderator: import_client2.UserRole.MODERATOR,
5331
+ admin: import_client2.UserRole.ADMIN,
5332
+ super_admin: import_client2.UserRole.SUPER_ADMIN
5333
+ };
5334
+ return roleMap[role] || import_client2.UserRole.USER;
5335
+ }
5336
+ /**
5337
+ * Map Prisma enum to application role
5338
+ */
5339
+ mapEnumToRole(role) {
5340
+ const roleMap = {
5341
+ [import_client2.UserRole.USER]: "user",
5342
+ [import_client2.UserRole.MODERATOR]: "moderator",
5343
+ [import_client2.UserRole.ADMIN]: "admin",
5344
+ [import_client2.UserRole.SUPER_ADMIN]: "super_admin"
5345
+ };
5346
+ return roleMap[role];
5347
+ }
5348
+ /**
5349
+ * Map application status to Prisma enum
5350
+ */
5351
+ mapStatusToEnum(status) {
5352
+ const statusMap = {
5353
+ active: import_client2.UserStatus.ACTIVE,
5354
+ inactive: import_client2.UserStatus.INACTIVE,
5355
+ suspended: import_client2.UserStatus.SUSPENDED,
5356
+ banned: import_client2.UserStatus.BANNED
5357
+ };
5358
+ return statusMap[status] || import_client2.UserStatus.ACTIVE;
5359
+ }
5360
+ /**
5361
+ * Map Prisma enum to application status
5362
+ */
5363
+ mapEnumToStatus(status) {
5364
+ const statusMap = {
5365
+ [import_client2.UserStatus.ACTIVE]: "active",
5366
+ [import_client2.UserStatus.INACTIVE]: "inactive",
5367
+ [import_client2.UserStatus.SUSPENDED]: "suspended",
5368
+ [import_client2.UserStatus.BANNED]: "banned"
5369
+ };
5370
+ return statusMap[status];
5371
+ }
5372
+ };
5373
+ function createUserRepository() {
5374
+ return new UserRepository();
5375
+ }
5376
+
5377
+ // src/modules/user/types.ts
5378
+ var DEFAULT_ROLE_PERMISSIONS = {
5379
+ user: ["profile:read", "profile:update"],
5380
+ moderator: [
5381
+ "profile:read",
5382
+ "profile:update",
5383
+ "users:read",
5384
+ "content:read",
5385
+ "content:update",
5386
+ "content:delete"
5387
+ ],
5388
+ admin: [
5389
+ "profile:read",
5390
+ "profile:update",
5391
+ "users:read",
5392
+ "users:update",
5393
+ "users:delete",
5394
+ "content:manage",
5395
+ "settings:read"
5396
+ ],
5397
+ super_admin: ["*:manage"]
5398
+ // All permissions
5399
+ };
5400
+
5401
+ // src/modules/user/user.service.ts
5402
+ var UserService = class {
5403
+ constructor(repository) {
5404
+ this.repository = repository;
5405
+ }
5406
+ async findById(id) {
5407
+ return this.repository.findById(id);
5408
+ }
5409
+ async findByEmail(email) {
5410
+ return this.repository.findByEmail(email);
5411
+ }
5412
+ async findMany(params, filters) {
5413
+ const result = await this.repository.findMany(params, filters);
5414
+ return {
5415
+ ...result,
5416
+ data: result.data.map(({ password: _password, ...user }) => user)
5417
+ };
5418
+ }
5419
+ async create(data) {
5420
+ const existing = await this.repository.findByEmail(data.email);
5421
+ if (existing) {
5422
+ throw new ConflictError("User with this email already exists");
5423
+ }
5424
+ const user = await this.repository.create(data);
5425
+ logger.info({ userId: user.id, email: user.email }, "User created");
5426
+ return user;
5427
+ }
5428
+ async update(id, data) {
5429
+ const user = await this.repository.findById(id);
5430
+ if (!user) {
5431
+ throw new NotFoundError("User");
5432
+ }
5433
+ if (data.email && data.email !== user.email) {
5434
+ const existing = await this.repository.findByEmail(data.email);
5435
+ if (existing) {
5436
+ throw new ConflictError("Email already in use");
5437
+ }
5438
+ }
5439
+ const updatedUser = await this.repository.update(id, data);
5440
+ if (!updatedUser) {
5441
+ throw new NotFoundError("User");
5442
+ }
5443
+ logger.info({ userId: id }, "User updated");
5444
+ return updatedUser;
5445
+ }
5446
+ async updatePassword(id, hashedPassword) {
5447
+ const user = await this.repository.updatePassword(id, hashedPassword);
5448
+ if (!user) {
5449
+ throw new NotFoundError("User");
5450
+ }
5451
+ logger.info({ userId: id }, "User password updated");
5452
+ return user;
5453
+ }
5454
+ async updateLastLogin(id) {
5455
+ const user = await this.repository.updateLastLogin(id);
5456
+ if (!user) {
5457
+ throw new NotFoundError("User");
5458
+ }
5459
+ return user;
5460
+ }
5461
+ async delete(id) {
5462
+ const user = await this.repository.findById(id);
5463
+ if (!user) {
5464
+ throw new NotFoundError("User");
5465
+ }
5466
+ await this.repository.delete(id);
5467
+ logger.info({ userId: id }, "User deleted");
5468
+ }
5469
+ async suspend(id) {
5470
+ return this.update(id, { status: "suspended" });
5471
+ }
5472
+ async ban(id) {
5473
+ return this.update(id, { status: "banned" });
5474
+ }
5475
+ async activate(id) {
5476
+ return this.update(id, { status: "active" });
5477
+ }
5478
+ async verifyEmail(id) {
5479
+ return this.update(id, { emailVerified: true });
5480
+ }
5481
+ async changeRole(id, role) {
5482
+ return this.update(id, { role });
5483
+ }
5484
+ // RBAC helpers
5485
+ hasPermission(role, permission) {
5486
+ const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
5487
+ if (permissions.includes("*:manage")) {
5488
+ return true;
5489
+ }
5490
+ if (permissions.includes(permission)) {
5491
+ return true;
5492
+ }
5493
+ const [resource] = permission.split(":");
5494
+ const managePermission = `${resource}:manage`;
5495
+ if (permissions.includes(managePermission)) {
5496
+ return true;
5497
+ }
5498
+ return false;
5499
+ }
5500
+ getPermissions(role) {
5501
+ return DEFAULT_ROLE_PERMISSIONS[role] || [];
5502
+ }
5503
+ };
5504
+ function createUserService(repository) {
5505
+ return new UserService(repository || createUserRepository());
5506
+ }
5507
+
5508
+ // src/modules/auth/index.ts
5509
+ async function registerAuthModule(app) {
5510
+ await app.register(import_jwt.default, {
5511
+ secret: config.jwt.secret,
5512
+ sign: {
5513
+ algorithm: "HS256"
5514
+ }
5515
+ });
5516
+ await app.register(import_cookie.default, {
5517
+ secret: config.jwt.secret,
5518
+ hook: "onRequest"
5519
+ });
5520
+ const authService = createAuthService(app);
5521
+ const userService = createUserService();
5522
+ const authController = createAuthController(authService, userService);
5523
+ registerAuthRoutes(app, authController, authService);
5524
+ logger.info("Auth module registered");
5525
+ }
5526
+
5527
+ // src/modules/user/schemas.ts
5528
+ var import_zod4 = require("zod");
5529
+ var userStatusEnum = import_zod4.z.enum(["active", "inactive", "suspended", "banned"]);
5530
+ var userRoleEnum = import_zod4.z.enum(["user", "admin", "moderator", "super_admin"]);
5531
+ var createUserSchema = import_zod4.z.object({
5532
+ email: import_zod4.z.string().email("Invalid email address"),
5533
+ 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"),
5534
+ name: import_zod4.z.string().min(2, "Name must be at least 2 characters").optional(),
5535
+ role: userRoleEnum.optional().default("user")
5536
+ });
5537
+ var updateUserSchema = import_zod4.z.object({
5538
+ email: import_zod4.z.string().email("Invalid email address").optional(),
5539
+ name: import_zod4.z.string().min(2, "Name must be at least 2 characters").optional(),
5540
+ role: userRoleEnum.optional(),
5541
+ status: userStatusEnum.optional(),
5542
+ emailVerified: import_zod4.z.boolean().optional(),
5543
+ metadata: import_zod4.z.record(import_zod4.z.unknown()).optional()
5544
+ });
5545
+ var updateProfileSchema = import_zod4.z.object({
5546
+ name: import_zod4.z.string().min(2, "Name must be at least 2 characters").optional(),
5547
+ metadata: import_zod4.z.record(import_zod4.z.unknown()).optional()
5548
+ });
5549
+ var userQuerySchema = import_zod4.z.object({
5550
+ page: import_zod4.z.string().transform(Number).optional(),
5551
+ limit: import_zod4.z.string().transform(Number).optional(),
5552
+ sortBy: import_zod4.z.string().optional(),
5553
+ sortOrder: import_zod4.z.enum(["asc", "desc"]).optional(),
5554
+ status: userStatusEnum.optional(),
5555
+ role: userRoleEnum.optional(),
5556
+ search: import_zod4.z.string().optional(),
5557
+ emailVerified: import_zod4.z.string().transform((val) => val === "true").optional()
5558
+ });
5559
+
5560
+ // src/modules/user/user.controller.ts
5561
+ function omitPassword(user) {
5562
+ const { password, ...userData } = user;
5563
+ void password;
5564
+ return userData;
5565
+ }
5566
+ var UserController = class {
5567
+ constructor(userService) {
5568
+ this.userService = userService;
5569
+ }
5570
+ async list(request, reply) {
5571
+ const query = validateQuery(userQuerySchema, request.query);
5572
+ const pagination = parsePaginationParams(query);
5573
+ const filters = {
5574
+ status: query.status,
5575
+ role: query.role,
5576
+ search: query.search,
5577
+ emailVerified: query.emailVerified
5578
+ };
5579
+ const result = await this.userService.findMany(pagination, filters);
5580
+ success2(reply, result);
5581
+ }
5582
+ async getById(request, reply) {
5583
+ const user = await this.userService.findById(request.params.id);
5584
+ if (!user) {
5585
+ return reply.status(404).send({
5586
+ success: false,
5587
+ message: "User not found"
5588
+ });
5589
+ }
5590
+ success2(reply, omitPassword(user));
5591
+ }
5592
+ async update(request, reply) {
5593
+ const data = validateBody(updateUserSchema, request.body);
5594
+ const user = await this.userService.update(request.params.id, data);
5595
+ success2(reply, omitPassword(user));
5596
+ }
5597
+ async delete(request, reply) {
5598
+ const authRequest = request;
5599
+ if (authRequest.user.id === request.params.id) {
5600
+ throw new ForbiddenError("Cannot delete your own account");
5601
+ }
5602
+ await this.userService.delete(request.params.id);
5603
+ noContent(reply);
5604
+ }
5605
+ async suspend(request, reply) {
5606
+ const authRequest = request;
5607
+ if (authRequest.user.id === request.params.id) {
5608
+ throw new ForbiddenError("Cannot suspend your own account");
5609
+ }
5610
+ const user = await this.userService.suspend(request.params.id);
5611
+ const userData = omitPassword(user);
5612
+ success2(reply, userData);
5613
+ }
5614
+ async ban(request, reply) {
5615
+ const authRequest = request;
5616
+ if (authRequest.user.id === request.params.id) {
5617
+ throw new ForbiddenError("Cannot ban your own account");
5618
+ }
5619
+ const user = await this.userService.ban(request.params.id);
5620
+ const userData = omitPassword(user);
5621
+ success2(reply, userData);
5622
+ }
5623
+ async activate(request, reply) {
5624
+ const user = await this.userService.activate(request.params.id);
5625
+ const userData = omitPassword(user);
5626
+ success2(reply, userData);
5627
+ }
5628
+ // Profile routes (for authenticated user)
5629
+ async getProfile(request, reply) {
5630
+ const authRequest = request;
5631
+ const user = await this.userService.findById(authRequest.user.id);
5632
+ if (!user) {
5633
+ return reply.status(404).send({
5634
+ success: false,
5635
+ message: "User not found"
5636
+ });
5637
+ }
5638
+ const userData = omitPassword(user);
5639
+ success2(reply, userData);
5640
+ }
5641
+ async updateProfile(request, reply) {
5642
+ const authRequest = request;
5643
+ const data = validateBody(updateProfileSchema, request.body);
5644
+ const user = await this.userService.update(authRequest.user.id, data);
5645
+ const userData = omitPassword(user);
5646
+ success2(reply, userData);
5647
+ }
5648
+ };
5649
+ function createUserController(userService) {
5650
+ return new UserController(userService);
5651
+ }
5652
+
5653
+ // src/modules/user/user.routes.ts
5654
+ var idParamsSchema = {
5655
+ type: "object",
5656
+ properties: {
5657
+ id: { type: "string" }
5658
+ },
5659
+ required: ["id"]
5660
+ };
5661
+ function registerUserRoutes(app, controller, authService) {
5662
+ const authenticate = createAuthMiddleware(authService);
5663
+ const isAdmin = createRoleMiddleware(["admin", "super_admin"]);
5664
+ const isModerator = createRoleMiddleware(["moderator", "admin", "super_admin"]);
5665
+ app.get("/profile", { preHandler: [authenticate] }, controller.getProfile.bind(controller));
5666
+ app.patch("/profile", { preHandler: [authenticate] }, controller.updateProfile.bind(controller));
5667
+ app.get("/users", { preHandler: [authenticate, isModerator] }, controller.list.bind(controller));
5668
+ app.get(
5669
+ "/users/:id",
5670
+ { preHandler: [authenticate, isModerator], schema: { params: idParamsSchema } },
5671
+ async (request, reply) => {
5672
+ return controller.getById(request, reply);
5673
+ }
5674
+ );
5675
+ app.patch(
5676
+ "/users/:id",
5677
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5678
+ async (request, reply) => {
5679
+ return controller.update(request, reply);
5680
+ }
5681
+ );
5682
+ app.delete(
5683
+ "/users/:id",
5684
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5685
+ async (request, reply) => {
5686
+ return controller.delete(request, reply);
5687
+ }
5688
+ );
5689
+ app.post(
5690
+ "/users/:id/suspend",
5691
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5692
+ async (request, reply) => {
5693
+ return controller.suspend(request, reply);
5694
+ }
5695
+ );
5696
+ app.post(
5697
+ "/users/:id/ban",
5698
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5699
+ async (request, reply) => {
5700
+ return controller.ban(request, reply);
5701
+ }
5702
+ );
5703
+ app.post(
5704
+ "/users/:id/activate",
5705
+ { preHandler: [authenticate, isAdmin], schema: { params: idParamsSchema } },
5706
+ async (request, reply) => {
5707
+ return controller.activate(request, reply);
5708
+ }
5709
+ );
5710
+ }
5711
+
5712
+ // src/modules/user/index.ts
5713
+ async function registerUserModule(app, authService) {
5714
+ const repository = createUserRepository();
5715
+ const userService = createUserService(repository);
5716
+ const userController = createUserController(userService);
5717
+ registerUserRoutes(app, userController, authService);
5718
+ logger.info("User module registered");
5719
+ }
5720
+
5721
+ // src/cli/utils/docs-generator.ts
5722
+ async function generateDocs(outputPath = "openapi.json", silent = false) {
5723
+ const spinner = silent ? null : (0, import_ora5.default)("Generating OpenAPI documentation...").start();
5724
+ try {
5725
+ const server = createServer({
5726
+ port: config.server.port,
5727
+ host: config.server.host
5728
+ });
5729
+ const app = server.instance;
5730
+ registerErrorHandler(app);
5731
+ await registerSecurity(app);
5732
+ await registerSwagger(app, {
5733
+ title: "Servcraft API",
5734
+ description: "API documentation generated by Servcraft",
5735
+ version: "1.0.0"
5736
+ });
5737
+ await registerAuthModule(app);
5738
+ const authService = createAuthService(app);
5739
+ await registerUserModule(app, authService);
5740
+ await app.ready();
5741
+ const spec = app.swagger();
5742
+ const absoluteOutput = import_path5.default.resolve(outputPath);
5743
+ await import_promises3.default.mkdir(import_path5.default.dirname(absoluteOutput), { recursive: true });
5744
+ await import_promises3.default.writeFile(absoluteOutput, JSON.stringify(spec, null, 2), "utf8");
5745
+ spinner?.succeed(`OpenAPI spec generated at ${absoluteOutput}`);
5746
+ await app.close();
5747
+ return absoluteOutput;
5748
+ } catch (error2) {
5749
+ spinner?.fail("Failed to generate OpenAPI documentation");
5750
+ throw error2;
5751
+ }
5752
+ }
5753
+
5754
+ // src/cli/commands/docs.ts
5755
+ var docsCommand = new import_commander5.Command("docs").description("API documentation commands");
5756
+ 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) => {
5757
+ try {
5758
+ const outputPath = await generateDocs(options.output, false);
5759
+ if (options.format === "yaml") {
5760
+ const jsonContent = await import_promises4.default.readFile(outputPath, "utf-8");
5761
+ const spec = JSON.parse(jsonContent);
5762
+ const yamlPath = outputPath.replace(".json", ".yaml");
5763
+ await import_promises4.default.writeFile(yamlPath, jsonToYaml(spec));
5764
+ success(`YAML documentation generated: ${yamlPath}`);
5765
+ }
5766
+ console.log("\n\u{1F4DA} Documentation URLs:");
5767
+ info(" Swagger UI: http://localhost:3000/docs");
5768
+ info(" OpenAPI JSON: http://localhost:3000/docs/json");
5769
+ } catch (err) {
5770
+ error(err instanceof Error ? err.message : String(err));
5771
+ }
5772
+ });
5773
+ docsCommand.option("-o, --output <path>", "Output file path", "openapi.json").action(async (options) => {
5774
+ if (options.output) {
5775
+ try {
5776
+ const outputPath = await generateDocs(options.output);
5777
+ success(`Documentation written to ${outputPath}`);
5778
+ } catch (err) {
5779
+ error(err instanceof Error ? err.message : String(err));
5780
+ process.exitCode = 1;
5781
+ }
5782
+ }
5783
+ });
5784
+ 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) => {
5785
+ const spinner = (0, import_ora6.default)("Exporting documentation...").start();
5786
+ try {
5787
+ const projectRoot = getProjectRoot();
5788
+ const specPath = import_path6.default.join(projectRoot, "openapi.json");
5789
+ try {
5790
+ await import_promises4.default.access(specPath);
5791
+ } catch {
5792
+ spinner.text = "Generating OpenAPI spec first...";
5793
+ await generateDocs("openapi.json", true);
5794
+ }
5795
+ const specContent = await import_promises4.default.readFile(specPath, "utf-8");
5796
+ const spec = JSON.parse(specContent);
5797
+ let output;
5798
+ let defaultName;
5799
+ switch (options.format) {
5800
+ case "postman":
5801
+ output = JSON.stringify(convertToPostman(spec), null, 2);
5802
+ defaultName = "postman_collection.json";
5803
+ break;
5804
+ case "insomnia":
5805
+ output = JSON.stringify(convertToInsomnia(spec), null, 2);
5806
+ defaultName = "insomnia_collection.json";
5807
+ break;
5808
+ case "yaml":
5809
+ output = jsonToYaml(spec);
5810
+ defaultName = "openapi.yaml";
5811
+ break;
5812
+ default:
5813
+ throw new Error(`Unknown format: ${options.format}`);
5814
+ }
5815
+ const outPath = import_path6.default.join(projectRoot, options.output || defaultName);
5816
+ await import_promises4.default.writeFile(outPath, output);
5817
+ spinner.succeed(`Exported to: ${options.output || defaultName}`);
5818
+ if (options.format === "postman") {
5819
+ info("\n Import in Postman: File > Import > Select file");
5820
+ }
5821
+ } catch (err) {
5822
+ spinner.fail("Export failed");
5823
+ error(err instanceof Error ? err.message : String(err));
5824
+ }
5825
+ });
5826
+ docsCommand.command("status").description("Show documentation status").action(async () => {
5827
+ const projectRoot = getProjectRoot();
5828
+ console.log(import_chalk6.default.bold("\n\u{1F4CA} Documentation Status\n"));
5829
+ const specPath = import_path6.default.join(projectRoot, "openapi.json");
5830
+ try {
5831
+ const stat2 = await import_promises4.default.stat(specPath);
5832
+ success(
5833
+ `openapi.json exists (${formatBytes(stat2.size)}, modified ${formatDate(stat2.mtime)})`
5834
+ );
5835
+ const content = await import_promises4.default.readFile(specPath, "utf-8");
5836
+ const spec = JSON.parse(content);
5837
+ const pathCount = Object.keys(spec.paths || {}).length;
5838
+ info(` ${pathCount} endpoints documented`);
5839
+ } catch {
5840
+ warn('openapi.json not found - run "servcraft docs generate"');
5841
+ }
5842
+ console.log("\n\u{1F4CC} Commands:");
5843
+ info(" servcraft docs generate Generate OpenAPI spec");
5844
+ info(" servcraft docs export Export to Postman/Insomnia");
5845
+ });
5846
+ function jsonToYaml(obj, indent = 0) {
5847
+ const spaces = " ".repeat(indent);
5848
+ if (obj === null || obj === void 0) return "null";
5849
+ if (typeof obj === "string") {
5850
+ if (obj.includes("\n") || obj.includes(":") || obj.includes("#")) {
5851
+ return `"${obj.replace(/"/g, '\\"')}"`;
5852
+ }
5853
+ return obj || '""';
5854
+ }
5855
+ if (typeof obj === "number" || typeof obj === "boolean") return String(obj);
5856
+ if (Array.isArray(obj)) {
5857
+ if (obj.length === 0) return "[]";
5858
+ return obj.map((item) => `${spaces}- ${jsonToYaml(item, indent + 1).trimStart()}`).join("\n");
5859
+ }
5860
+ if (typeof obj === "object") {
5861
+ const entries = Object.entries(obj);
5862
+ if (entries.length === 0) return "{}";
5863
+ return entries.map(([key, value]) => {
5864
+ const valueStr = jsonToYaml(value, indent + 1);
5865
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0) {
5866
+ return `${spaces}${key}:
5867
+ ${valueStr}`;
5868
+ }
5869
+ return `${spaces}${key}: ${valueStr}`;
5870
+ }).join("\n");
5871
+ }
5872
+ return String(obj);
5873
+ }
5874
+ function convertToPostman(spec) {
5875
+ const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
5876
+ const items = [];
5877
+ for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
5878
+ for (const [method, details] of Object.entries(methods)) {
5879
+ items.push({
5880
+ name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
5881
+ request: {
5882
+ method: method.toUpperCase(),
5883
+ header: [
5884
+ { key: "Content-Type", value: "application/json" },
5885
+ { key: "Authorization", value: "Bearer {{token}}" }
5886
+ ],
5887
+ url: {
5888
+ raw: `{{baseUrl}}${pathUrl}`,
5889
+ host: ["{{baseUrl}}"],
5890
+ path: pathUrl.split("/").filter(Boolean)
5891
+ },
5892
+ ...details.requestBody ? { body: { mode: "raw", raw: "{}" } } : {}
5893
+ }
5894
+ });
5895
+ }
5896
+ }
5897
+ return {
5898
+ info: {
5899
+ name: spec.info.title,
5900
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
5901
+ },
5902
+ item: items,
5903
+ variable: [
5904
+ { key: "baseUrl", value: baseUrl },
5905
+ { key: "token", value: "" }
5906
+ ]
5907
+ };
5908
+ }
5909
+ function convertToInsomnia(spec) {
5910
+ const baseUrl = spec.servers?.[0]?.url || "http://localhost:3000";
5911
+ const resources = [
5912
+ { _type: "environment", name: "Base Environment", data: { baseUrl, token: "" } }
5913
+ ];
5914
+ for (const [pathUrl, methods] of Object.entries(spec.paths || {})) {
5915
+ for (const [method, details] of Object.entries(methods)) {
5916
+ resources.push({
5917
+ _type: "request",
5918
+ name: details.summary || `${method.toUpperCase()} ${pathUrl}`,
5919
+ method: method.toUpperCase(),
5920
+ url: `{{ baseUrl }}${pathUrl}`,
5921
+ headers: [
5922
+ { name: "Content-Type", value: "application/json" },
5923
+ { name: "Authorization", value: "Bearer {{ token }}" }
5924
+ ]
5925
+ });
5926
+ }
5927
+ }
5928
+ return { _type: "export", __export_format: 4, resources };
5929
+ }
5930
+ function formatBytes(bytes) {
5931
+ if (bytes < 1024) return `${bytes} B`;
5932
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5933
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5934
+ }
5935
+ function formatDate(date) {
5936
+ return date.toLocaleDateString("en-US", {
5937
+ month: "short",
5938
+ day: "numeric",
5939
+ hour: "2-digit",
5940
+ minute: "2-digit"
5941
+ });
5942
+ }
5943
+
3818
5944
  // src/cli/index.ts
3819
- var program = new import_commander5.Command();
5945
+ var program = new import_commander6.Command();
3820
5946
  program.name("servcraft").description("Servcraft - A modular Node.js backend framework CLI").version("0.1.0");
3821
5947
  program.addCommand(initCommand);
3822
5948
  program.addCommand(generateCommand);
3823
5949
  program.addCommand(addModuleCommand);
3824
5950
  program.addCommand(dbCommand);
5951
+ program.addCommand(docsCommand);
3825
5952
  program.parse();
3826
5953
  //# sourceMappingURL=index.cjs.map