servcraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.dockerignore +45 -0
  2. package/.env.example +46 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +11 -0
  7. package/Dockerfile +76 -0
  8. package/Dockerfile.dev +31 -0
  9. package/README.md +232 -0
  10. package/commitlint.config.js +24 -0
  11. package/dist/cli/index.cjs +3968 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +3945 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/index.cjs +2458 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +828 -0
  20. package/dist/index.d.ts +828 -0
  21. package/dist/index.js +2332 -0
  22. package/dist/index.js.map +1 -0
  23. package/docker-compose.prod.yml +118 -0
  24. package/docker-compose.yml +147 -0
  25. package/eslint.config.js +27 -0
  26. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  27. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  28. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  29. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  30. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
  31. package/npm-cache/_update-notifier-last-checked +0 -0
  32. package/package.json +112 -0
  33. package/prisma/schema.prisma +157 -0
  34. package/src/cli/commands/add-module.ts +422 -0
  35. package/src/cli/commands/db.ts +137 -0
  36. package/src/cli/commands/docs.ts +16 -0
  37. package/src/cli/commands/generate.ts +459 -0
  38. package/src/cli/commands/init.ts +640 -0
  39. package/src/cli/index.ts +32 -0
  40. package/src/cli/templates/controller.ts +67 -0
  41. package/src/cli/templates/dynamic-prisma.ts +89 -0
  42. package/src/cli/templates/dynamic-schemas.ts +232 -0
  43. package/src/cli/templates/dynamic-types.ts +60 -0
  44. package/src/cli/templates/module-index.ts +33 -0
  45. package/src/cli/templates/prisma-model.ts +17 -0
  46. package/src/cli/templates/repository.ts +104 -0
  47. package/src/cli/templates/routes.ts +70 -0
  48. package/src/cli/templates/schemas.ts +26 -0
  49. package/src/cli/templates/service.ts +58 -0
  50. package/src/cli/templates/types.ts +27 -0
  51. package/src/cli/utils/docs-generator.ts +47 -0
  52. package/src/cli/utils/field-parser.ts +315 -0
  53. package/src/cli/utils/helpers.ts +89 -0
  54. package/src/config/env.ts +80 -0
  55. package/src/config/index.ts +97 -0
  56. package/src/core/index.ts +5 -0
  57. package/src/core/logger.ts +43 -0
  58. package/src/core/server.ts +132 -0
  59. package/src/database/index.ts +7 -0
  60. package/src/database/prisma.ts +54 -0
  61. package/src/database/seed.ts +59 -0
  62. package/src/index.ts +63 -0
  63. package/src/middleware/error-handler.ts +73 -0
  64. package/src/middleware/index.ts +3 -0
  65. package/src/middleware/security.ts +116 -0
  66. package/src/modules/audit/audit.service.ts +192 -0
  67. package/src/modules/audit/index.ts +2 -0
  68. package/src/modules/audit/types.ts +37 -0
  69. package/src/modules/auth/auth.controller.ts +182 -0
  70. package/src/modules/auth/auth.middleware.ts +87 -0
  71. package/src/modules/auth/auth.routes.ts +123 -0
  72. package/src/modules/auth/auth.service.ts +142 -0
  73. package/src/modules/auth/index.ts +49 -0
  74. package/src/modules/auth/schemas.ts +52 -0
  75. package/src/modules/auth/types.ts +69 -0
  76. package/src/modules/email/email.service.ts +212 -0
  77. package/src/modules/email/index.ts +10 -0
  78. package/src/modules/email/templates.ts +213 -0
  79. package/src/modules/email/types.ts +57 -0
  80. package/src/modules/swagger/index.ts +3 -0
  81. package/src/modules/swagger/schema-builder.ts +263 -0
  82. package/src/modules/swagger/swagger.service.ts +169 -0
  83. package/src/modules/swagger/types.ts +68 -0
  84. package/src/modules/user/index.ts +30 -0
  85. package/src/modules/user/schemas.ts +49 -0
  86. package/src/modules/user/types.ts +78 -0
  87. package/src/modules/user/user.controller.ts +139 -0
  88. package/src/modules/user/user.repository.ts +156 -0
  89. package/src/modules/user/user.routes.ts +199 -0
  90. package/src/modules/user/user.service.ts +145 -0
  91. package/src/modules/validation/index.ts +18 -0
  92. package/src/modules/validation/validator.ts +104 -0
  93. package/src/types/common.ts +61 -0
  94. package/src/types/index.ts +10 -0
  95. package/src/utils/errors.ts +66 -0
  96. package/src/utils/index.ts +33 -0
  97. package/src/utils/pagination.ts +38 -0
  98. package/src/utils/response.ts +63 -0
  99. package/tests/integration/auth.test.ts +59 -0
  100. package/tests/setup.ts +17 -0
  101. package/tests/unit/modules/validation.test.ts +88 -0
  102. package/tests/unit/utils/errors.test.ts +113 -0
  103. package/tests/unit/utils/pagination.test.ts +82 -0
  104. package/tsconfig.json +33 -0
  105. package/tsup.config.ts +14 -0
  106. package/vitest.config.ts +34 -0
@@ -0,0 +1,199 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { UserController } from './user.controller.js';
3
+ import type { AuthService } from '../auth/auth.service.js';
4
+ import { createAuthMiddleware, createRoleMiddleware } from '../auth/auth.middleware.js';
5
+ import { commonResponses, paginationQuery, idParam } from '../swagger/index.js';
6
+
7
+ const userTag = 'Users';
8
+ const userResponse = {
9
+ type: 'object',
10
+ properties: {
11
+ success: { type: 'boolean', example: true },
12
+ data: { type: 'object' },
13
+ },
14
+ };
15
+
16
+ export function registerUserRoutes(
17
+ app: FastifyInstance,
18
+ controller: UserController,
19
+ authService: AuthService
20
+ ): void {
21
+ const authenticate = createAuthMiddleware(authService);
22
+ const isAdmin = createRoleMiddleware(['admin', 'super_admin']);
23
+ const isModerator = createRoleMiddleware(['moderator', 'admin', 'super_admin']);
24
+
25
+ // Profile routes (authenticated users)
26
+ app.get(
27
+ '/profile',
28
+ {
29
+ preHandler: [authenticate],
30
+ schema: {
31
+ tags: [userTag],
32
+ summary: 'Get current user profile',
33
+ security: [{ bearerAuth: [] }],
34
+ response: {
35
+ 200: userResponse,
36
+ 401: commonResponses.unauthorized,
37
+ },
38
+ },
39
+ },
40
+ controller.getProfile.bind(controller)
41
+ );
42
+ app.patch(
43
+ '/profile',
44
+ {
45
+ preHandler: [authenticate],
46
+ schema: {
47
+ tags: [userTag],
48
+ summary: 'Update current user profile',
49
+ security: [{ bearerAuth: [] }],
50
+ body: { type: 'object' },
51
+ response: {
52
+ 200: userResponse,
53
+ 401: commonResponses.unauthorized,
54
+ 400: commonResponses.error,
55
+ },
56
+ },
57
+ },
58
+ controller.updateProfile.bind(controller)
59
+ );
60
+
61
+ // Admin routes
62
+ app.get(
63
+ '/users',
64
+ {
65
+ preHandler: [authenticate, isModerator],
66
+ schema: {
67
+ tags: [userTag],
68
+ summary: 'List users',
69
+ security: [{ bearerAuth: [] }],
70
+ querystring: {
71
+ ...paginationQuery,
72
+ properties: {
73
+ ...paginationQuery.properties,
74
+ status: { type: 'string', enum: ['active', 'inactive', 'suspended', 'banned'] },
75
+ role: { type: 'string', enum: ['user', 'admin', 'moderator', 'super_admin'] },
76
+ search: { type: 'string' },
77
+ emailVerified: { type: 'boolean' },
78
+ },
79
+ },
80
+ response: {
81
+ 200: commonResponses.paginated,
82
+ 401: commonResponses.unauthorized,
83
+ },
84
+ },
85
+ },
86
+ controller.list.bind(controller)
87
+ );
88
+ app.get(
89
+ '/users/:id',
90
+ {
91
+ preHandler: [authenticate, isModerator],
92
+ schema: {
93
+ tags: [userTag],
94
+ summary: 'Get user by id',
95
+ security: [{ bearerAuth: [] }],
96
+ params: idParam,
97
+ response: {
98
+ 200: userResponse,
99
+ 401: commonResponses.unauthorized,
100
+ 404: commonResponses.notFound,
101
+ },
102
+ },
103
+ },
104
+ controller.getById.bind(controller)
105
+ );
106
+ app.patch(
107
+ '/users/:id',
108
+ {
109
+ preHandler: [authenticate, isAdmin],
110
+ schema: {
111
+ tags: [userTag],
112
+ summary: 'Update user',
113
+ security: [{ bearerAuth: [] }],
114
+ params: idParam,
115
+ body: { type: 'object' },
116
+ response: {
117
+ 200: userResponse,
118
+ 401: commonResponses.unauthorized,
119
+ 404: commonResponses.notFound,
120
+ },
121
+ },
122
+ },
123
+ controller.update.bind(controller)
124
+ );
125
+ app.delete(
126
+ '/users/:id',
127
+ {
128
+ preHandler: [authenticate, isAdmin],
129
+ schema: {
130
+ tags: [userTag],
131
+ summary: 'Delete user',
132
+ security: [{ bearerAuth: [] }],
133
+ params: idParam,
134
+ response: {
135
+ 204: { description: 'User deleted' },
136
+ 401: commonResponses.unauthorized,
137
+ 404: commonResponses.notFound,
138
+ },
139
+ },
140
+ },
141
+ controller.delete.bind(controller)
142
+ );
143
+
144
+ // User status management
145
+ app.post(
146
+ '/users/:id/suspend',
147
+ {
148
+ preHandler: [authenticate, isAdmin],
149
+ schema: {
150
+ tags: [userTag],
151
+ summary: 'Suspend user',
152
+ security: [{ bearerAuth: [] }],
153
+ params: idParam,
154
+ response: {
155
+ 200: userResponse,
156
+ 401: commonResponses.unauthorized,
157
+ 404: commonResponses.notFound,
158
+ },
159
+ },
160
+ },
161
+ controller.suspend.bind(controller)
162
+ );
163
+ app.post(
164
+ '/users/:id/ban',
165
+ {
166
+ preHandler: [authenticate, isAdmin],
167
+ schema: {
168
+ tags: [userTag],
169
+ summary: 'Ban user',
170
+ security: [{ bearerAuth: [] }],
171
+ params: idParam,
172
+ response: {
173
+ 200: userResponse,
174
+ 401: commonResponses.unauthorized,
175
+ 404: commonResponses.notFound,
176
+ },
177
+ },
178
+ },
179
+ controller.ban.bind(controller)
180
+ );
181
+ app.post(
182
+ '/users/:id/activate',
183
+ {
184
+ preHandler: [authenticate, isAdmin],
185
+ schema: {
186
+ tags: [userTag],
187
+ summary: 'Activate user',
188
+ security: [{ bearerAuth: [] }],
189
+ params: idParam,
190
+ response: {
191
+ 200: userResponse,
192
+ 401: commonResponses.unauthorized,
193
+ 404: commonResponses.notFound,
194
+ },
195
+ },
196
+ },
197
+ controller.activate.bind(controller)
198
+ );
199
+ }
@@ -0,0 +1,145 @@
1
+ import type { PaginatedResult, PaginationParams } from '../../types/index.js';
2
+ import { NotFoundError, ConflictError } from '../../utils/errors.js';
3
+ import { UserRepository, createUserRepository } from './user.repository.js';
4
+ import type { User, CreateUserData, UpdateUserData, UserFilters, UserRole } from './types.js';
5
+ import { DEFAULT_ROLE_PERMISSIONS } from './types.js';
6
+ import { logger } from '../../core/logger.js';
7
+
8
+ export class UserService {
9
+ constructor(private repository: UserRepository) {}
10
+
11
+ async findById(id: string): Promise<User | null> {
12
+ return this.repository.findById(id);
13
+ }
14
+
15
+ async findByEmail(email: string): Promise<User | null> {
16
+ return this.repository.findByEmail(email);
17
+ }
18
+
19
+ async findMany(
20
+ params: PaginationParams,
21
+ filters?: UserFilters
22
+ ): Promise<PaginatedResult<Omit<User, 'password'>>> {
23
+ const result = await this.repository.findMany(params, filters);
24
+
25
+ // Remove passwords from results
26
+ return {
27
+ ...result,
28
+ data: result.data.map(({ password, ...user }) => user) as Omit<User, 'password'>[],
29
+ };
30
+ }
31
+
32
+ async create(data: CreateUserData): Promise<User> {
33
+ // Check for existing user
34
+ const existing = await this.repository.findByEmail(data.email);
35
+ if (existing) {
36
+ throw new ConflictError('User with this email already exists');
37
+ }
38
+
39
+ const user = await this.repository.create(data);
40
+ logger.info({ userId: user.id, email: user.email }, 'User created');
41
+ return user;
42
+ }
43
+
44
+ async update(id: string, data: UpdateUserData): Promise<User> {
45
+ const user = await this.repository.findById(id);
46
+ if (!user) {
47
+ throw new NotFoundError('User');
48
+ }
49
+
50
+ // Check email uniqueness if changing email
51
+ if (data.email && data.email !== user.email) {
52
+ const existing = await this.repository.findByEmail(data.email);
53
+ if (existing) {
54
+ throw new ConflictError('Email already in use');
55
+ }
56
+ }
57
+
58
+ const updatedUser = await this.repository.update(id, data);
59
+ if (!updatedUser) {
60
+ throw new NotFoundError('User');
61
+ }
62
+
63
+ logger.info({ userId: id }, 'User updated');
64
+ return updatedUser;
65
+ }
66
+
67
+ async updatePassword(id: string, hashedPassword: string): Promise<User> {
68
+ const user = await this.repository.updatePassword(id, hashedPassword);
69
+ if (!user) {
70
+ throw new NotFoundError('User');
71
+ }
72
+ logger.info({ userId: id }, 'User password updated');
73
+ return user;
74
+ }
75
+
76
+ async updateLastLogin(id: string): Promise<User> {
77
+ const user = await this.repository.updateLastLogin(id);
78
+ if (!user) {
79
+ throw new NotFoundError('User');
80
+ }
81
+ return user;
82
+ }
83
+
84
+ async delete(id: string): Promise<void> {
85
+ const user = await this.repository.findById(id);
86
+ if (!user) {
87
+ throw new NotFoundError('User');
88
+ }
89
+
90
+ await this.repository.delete(id);
91
+ logger.info({ userId: id }, 'User deleted');
92
+ }
93
+
94
+ async suspend(id: string): Promise<User> {
95
+ return this.update(id, { status: 'suspended' });
96
+ }
97
+
98
+ async ban(id: string): Promise<User> {
99
+ return this.update(id, { status: 'banned' });
100
+ }
101
+
102
+ async activate(id: string): Promise<User> {
103
+ return this.update(id, { status: 'active' });
104
+ }
105
+
106
+ async verifyEmail(id: string): Promise<User> {
107
+ return this.update(id, { emailVerified: true });
108
+ }
109
+
110
+ async changeRole(id: string, role: UserRole): Promise<User> {
111
+ return this.update(id, { role });
112
+ }
113
+
114
+ // RBAC helpers
115
+ hasPermission(role: UserRole, permission: string): boolean {
116
+ const permissions = DEFAULT_ROLE_PERMISSIONS[role] || [];
117
+
118
+ // Super admin has all permissions
119
+ if (permissions.includes('*:manage')) {
120
+ return true;
121
+ }
122
+
123
+ // Check exact match
124
+ if (permissions.includes(permission)) {
125
+ return true;
126
+ }
127
+
128
+ // Check wildcard match (e.g., "content:manage" matches "content:read")
129
+ const [resource, action] = permission.split(':');
130
+ const managePermission = `${resource}:manage`;
131
+ if (permissions.includes(managePermission)) {
132
+ return true;
133
+ }
134
+
135
+ return false;
136
+ }
137
+
138
+ getPermissions(role: UserRole): string[] {
139
+ return DEFAULT_ROLE_PERMISSIONS[role] || [];
140
+ }
141
+ }
142
+
143
+ export function createUserService(repository?: UserRepository): UserService {
144
+ return new UserService(repository || createUserRepository());
145
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ validate,
3
+ validateBody,
4
+ validateQuery,
5
+ validateParams,
6
+ idParamSchema,
7
+ paginationSchema,
8
+ searchSchema,
9
+ emailSchema,
10
+ passwordSchema,
11
+ urlSchema,
12
+ phoneSchema,
13
+ dateSchema,
14
+ futureDateSchema,
15
+ pastDateSchema,
16
+ } from './validator.js';
17
+
18
+ export type { IdParam, PaginationInput } from './validator.js';
@@ -0,0 +1,104 @@
1
+ import { z } from 'zod';
2
+ import type { ZodError, ZodTypeAny } from 'zod';
3
+ import { ValidationError } from '../../utils/errors.js';
4
+
5
+ export function validateBody<T extends ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
6
+ const result = schema.safeParse(data);
7
+
8
+ if (!result.success) {
9
+ throw new ValidationError(formatZodErrors(result.error));
10
+ }
11
+
12
+ return result.data;
13
+ }
14
+
15
+ export function validateQuery<T extends ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
16
+ const result = schema.safeParse(data);
17
+
18
+ if (!result.success) {
19
+ throw new ValidationError(formatZodErrors(result.error));
20
+ }
21
+
22
+ return result.data;
23
+ }
24
+
25
+ export function validateParams<T extends ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
26
+ const result = schema.safeParse(data);
27
+
28
+ if (!result.success) {
29
+ throw new ValidationError(formatZodErrors(result.error));
30
+ }
31
+
32
+ return result.data;
33
+ }
34
+
35
+ export function validate<T extends ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
36
+ return validateBody(schema, data);
37
+ }
38
+
39
+ function formatZodErrors(error: ZodError): Record<string, string[]> {
40
+ const errors: Record<string, string[]> = {};
41
+
42
+ for (const issue of error.issues) {
43
+ const path = issue.path.join('.') || 'root';
44
+ if (!errors[path]) {
45
+ errors[path] = [];
46
+ }
47
+ errors[path].push(issue.message);
48
+ }
49
+
50
+ return errors;
51
+ }
52
+
53
+ // Common validation schemas
54
+ export const idParamSchema = z.object({
55
+ id: z.string().uuid('Invalid ID format'),
56
+ });
57
+
58
+ export const paginationSchema = z.object({
59
+ page: z.string().transform(Number).optional().default('1'),
60
+ limit: z.string().transform(Number).optional().default('20'),
61
+ sortBy: z.string().optional(),
62
+ sortOrder: z.enum(['asc', 'desc']).optional().default('asc'),
63
+ });
64
+
65
+ export const searchSchema = z.object({
66
+ q: z.string().min(1, 'Search query is required').optional(),
67
+ search: z.string().min(1).optional(),
68
+ });
69
+
70
+ // Email validation
71
+ export const emailSchema = z.string().email('Invalid email address');
72
+
73
+ // Password validation with strength requirements
74
+ export const passwordSchema = z
75
+ .string()
76
+ .min(8, 'Password must be at least 8 characters')
77
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
78
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
79
+ .regex(/[0-9]/, 'Password must contain at least one number')
80
+ .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character');
81
+
82
+ // URL validation
83
+ export const urlSchema = z.string().url('Invalid URL format');
84
+
85
+ // Phone validation (basic international format)
86
+ export const phoneSchema = z.string().regex(
87
+ /^\+?[1-9]\d{1,14}$/,
88
+ 'Invalid phone number format'
89
+ );
90
+
91
+ // Date validation
92
+ export const dateSchema = z.coerce.date();
93
+ export const futureDateSchema = z.coerce.date().refine(
94
+ (date) => date > new Date(),
95
+ 'Date must be in the future'
96
+ );
97
+ export const pastDateSchema = z.coerce.date().refine(
98
+ (date) => date < new Date(),
99
+ 'Date must be in the past'
100
+ );
101
+
102
+ // Type exports
103
+ export type IdParam = z.infer<typeof idParamSchema>;
104
+ export type PaginationInput = z.infer<typeof paginationSchema>;
@@ -0,0 +1,61 @@
1
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2
+
3
+ // Base entity with common fields
4
+ export interface BaseEntity {
5
+ id: string;
6
+ createdAt: Date;
7
+ updatedAt: Date;
8
+ }
9
+
10
+ // Pagination
11
+ export interface PaginationParams {
12
+ page: number;
13
+ limit: number;
14
+ sortBy?: string;
15
+ sortOrder?: 'asc' | 'desc';
16
+ }
17
+
18
+ export interface PaginatedResult<T> {
19
+ data: T[];
20
+ meta: {
21
+ total: number;
22
+ page: number;
23
+ limit: number;
24
+ totalPages: number;
25
+ hasNextPage: boolean;
26
+ hasPrevPage: boolean;
27
+ };
28
+ }
29
+
30
+ // API Response
31
+ export interface ApiResponse<T = unknown> {
32
+ success: boolean;
33
+ data?: T;
34
+ message?: string;
35
+ errors?: Record<string, string[]>;
36
+ }
37
+
38
+ // Service Result
39
+ export type ServiceResult<T, E = Error> =
40
+ | { success: true; data: T }
41
+ | { success: false; error: E; message: string };
42
+
43
+ // Repository interface
44
+ export interface Repository<T extends BaseEntity> {
45
+ findById(id: string): Promise<T | null>;
46
+ findMany(params?: PaginationParams): Promise<PaginatedResult<T>>;
47
+ create(data: Omit<T, keyof BaseEntity>): Promise<T>;
48
+ update(id: string, data: Partial<Omit<T, keyof BaseEntity>>): Promise<T | null>;
49
+ delete(id: string): Promise<boolean>;
50
+ }
51
+
52
+ // Controller handler type
53
+ export type ControllerHandler = (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
54
+
55
+ // Module interface
56
+ export interface Module {
57
+ name: string;
58
+ register(app: FastifyInstance): Promise<void>;
59
+ }
60
+
61
+ import type { FastifyInstance } from 'fastify';
@@ -0,0 +1,10 @@
1
+ export type {
2
+ BaseEntity,
3
+ PaginationParams,
4
+ PaginatedResult,
5
+ ApiResponse,
6
+ ServiceResult,
7
+ Repository,
8
+ ControllerHandler,
9
+ Module,
10
+ } from './common.js';
@@ -0,0 +1,66 @@
1
+ export class AppError extends Error {
2
+ public readonly statusCode: number;
3
+ public readonly isOperational: boolean;
4
+ public readonly errors?: Record<string, string[]>;
5
+
6
+ constructor(
7
+ message: string,
8
+ statusCode = 500,
9
+ isOperational = true,
10
+ errors?: Record<string, string[]>
11
+ ) {
12
+ super(message);
13
+ this.statusCode = statusCode;
14
+ this.isOperational = isOperational;
15
+ this.errors = errors;
16
+
17
+ Object.setPrototypeOf(this, AppError.prototype);
18
+ Error.captureStackTrace(this, this.constructor);
19
+ }
20
+ }
21
+
22
+ export class NotFoundError extends AppError {
23
+ constructor(resource = 'Resource') {
24
+ super(`${resource} not found`, 404);
25
+ }
26
+ }
27
+
28
+ export class UnauthorizedError extends AppError {
29
+ constructor(message = 'Unauthorized') {
30
+ super(message, 401);
31
+ }
32
+ }
33
+
34
+ export class ForbiddenError extends AppError {
35
+ constructor(message = 'Forbidden') {
36
+ super(message, 403);
37
+ }
38
+ }
39
+
40
+ export class BadRequestError extends AppError {
41
+ constructor(message = 'Bad request', errors?: Record<string, string[]>) {
42
+ super(message, 400, true, errors);
43
+ }
44
+ }
45
+
46
+ export class ConflictError extends AppError {
47
+ constructor(message = 'Resource already exists') {
48
+ super(message, 409);
49
+ }
50
+ }
51
+
52
+ export class ValidationError extends AppError {
53
+ constructor(errors: Record<string, string[]>) {
54
+ super('Validation failed', 422, true, errors);
55
+ }
56
+ }
57
+
58
+ export class TooManyRequestsError extends AppError {
59
+ constructor(message = 'Too many requests') {
60
+ super(message, 429);
61
+ }
62
+ }
63
+
64
+ export function isAppError(error: unknown): error is AppError {
65
+ return error instanceof AppError;
66
+ }
@@ -0,0 +1,33 @@
1
+ export {
2
+ success,
3
+ created,
4
+ noContent,
5
+ error,
6
+ notFound,
7
+ unauthorized,
8
+ forbidden,
9
+ badRequest,
10
+ conflict,
11
+ internalError,
12
+ } from './response.js';
13
+
14
+ export {
15
+ parsePaginationParams,
16
+ createPaginatedResult,
17
+ getSkip,
18
+ DEFAULT_PAGE,
19
+ DEFAULT_LIMIT,
20
+ MAX_LIMIT,
21
+ } from './pagination.js';
22
+
23
+ export {
24
+ AppError,
25
+ NotFoundError,
26
+ UnauthorizedError,
27
+ ForbiddenError,
28
+ BadRequestError,
29
+ ConflictError,
30
+ ValidationError,
31
+ TooManyRequestsError,
32
+ isAppError,
33
+ } from './errors.js';
@@ -0,0 +1,38 @@
1
+ import type { PaginationParams, PaginatedResult } from '../types/index.js';
2
+
3
+ export const DEFAULT_PAGE = 1;
4
+ export const DEFAULT_LIMIT = 20;
5
+ export const MAX_LIMIT = 100;
6
+
7
+ export function parsePaginationParams(query: Record<string, unknown>): PaginationParams {
8
+ const page = Math.max(1, parseInt(String(query.page || DEFAULT_PAGE), 10));
9
+ const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(query.limit || DEFAULT_LIMIT), 10)));
10
+ const sortBy = typeof query.sortBy === 'string' ? query.sortBy : undefined;
11
+ const sortOrder = query.sortOrder === 'desc' ? 'desc' : 'asc';
12
+
13
+ return { page, limit, sortBy, sortOrder };
14
+ }
15
+
16
+ export function createPaginatedResult<T>(
17
+ data: T[],
18
+ total: number,
19
+ params: PaginationParams
20
+ ): PaginatedResult<T> {
21
+ const totalPages = Math.ceil(total / params.limit);
22
+
23
+ return {
24
+ data,
25
+ meta: {
26
+ total,
27
+ page: params.page,
28
+ limit: params.limit,
29
+ totalPages,
30
+ hasNextPage: params.page < totalPages,
31
+ hasPrevPage: params.page > 1,
32
+ },
33
+ };
34
+ }
35
+
36
+ export function getSkip(params: PaginationParams): number {
37
+ return (params.page - 1) * params.limit;
38
+ }