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