create-forgeapi 1.0.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 (64) hide show
  1. package/README.md +79 -0
  2. package/dist/bin/index.d.ts +2 -0
  3. package/dist/bin/index.js +286 -0
  4. package/package.json +37 -0
  5. package/template/.env.example +7 -0
  6. package/template/nodemon.json +7 -0
  7. package/template/package.json +21 -0
  8. package/template/src/App.ts +1 -0
  9. package/template/src/api/controllers/UserController.ts +54 -0
  10. package/template/src/api/middlewares/AuthMiddleware.ts +24 -0
  11. package/template/src/base/BaseRepository.ts +112 -0
  12. package/template/src/base/BaseResponse.ts +45 -0
  13. package/template/src/base/BaseResponseError.ts +12 -0
  14. package/template/src/base/BaseSchema.ts +82 -0
  15. package/template/src/config/DateTimeConfig.ts +4 -0
  16. package/template/src/config/GlobalHandlerError.ts +81 -0
  17. package/template/src/config/LoggerConfig.ts +157 -0
  18. package/template/src/config/LookupConfig.ts +68 -0
  19. package/template/src/config/MetaDataConfig.ts +25 -0
  20. package/template/src/config/RouteConfig.ts +98 -0
  21. package/template/src/config/SwaggerConfig.ts +147 -0
  22. package/template/src/config/envConfig.ts +5 -0
  23. package/template/src/core/Server.ts +75 -0
  24. package/template/src/core/exceptions/BadRequestException.ts +7 -0
  25. package/template/src/core/exceptions/ForbiddenException.ts +7 -0
  26. package/template/src/core/exceptions/NotFoundException.ts +7 -0
  27. package/template/src/core/exceptions/UnauthorizedException.ts +7 -0
  28. package/template/src/core/repositories/UserRepository.ts +53 -0
  29. package/template/src/database/DatabaseConnection.ts +11 -0
  30. package/template/src/database/builder/CustomBuilder.ts +177 -0
  31. package/template/src/database/builder/CustomFilter.ts +12 -0
  32. package/template/src/database/builder/MultipleSearchCriteria.ts +25 -0
  33. package/template/src/database/builder/SearchCriteria.ts +79 -0
  34. package/template/src/database/entity/ProfileModel.ts +54 -0
  35. package/template/src/database/entity/UserModel.ts +46 -0
  36. package/template/src/shared/decorators/ApiDecorator.ts +88 -0
  37. package/template/src/shared/decorators/LookupDecorator.ts +51 -0
  38. package/template/src/shared/decorators/MethodeDecorator.ts +38 -0
  39. package/template/src/shared/helpers/VirtualHelper.ts +53 -0
  40. package/template/src/shared/interfaces/BaseInterface.ts +14 -0
  41. package/template/src/shared/interfaces/DatabaseInterface.ts +41 -0
  42. package/template/src/shared/interfaces/HttpInterface.ts +37 -0
  43. package/template/src/shared/interfaces/LoggerOptionsInterface.ts +16 -0
  44. package/template/src/shared/interfaces/MiddlewareInterface.ts +14 -0
  45. package/template/src/shared/interfaces/RouteMetadataInterface.ts +10 -0
  46. package/template/src/shared/interfaces/SoftDeleteInterface.ts +8 -0
  47. package/template/src/shared/interfaces/SwaggerMetadataInterface.ts +12 -0
  48. package/template/src/shared/interfaces/UserInterface.ts +37 -0
  49. package/template/src/shared/interfaces/user/UserCreateInterface.ts +4 -0
  50. package/template/src/shared/models/enum/MiddlewareEnum.ts +19 -0
  51. package/template/src/shared/models/request/UserCreateRequest.ts +12 -0
  52. package/template/src/shared/models/response/ApiResponse.ts +27 -0
  53. package/template/src/shared/types/express.d.ts +14 -0
  54. package/template/src/shared/types/mongoose.d.ts +7 -0
  55. package/template/src/shared/utils/ColorUtil.ts +17 -0
  56. package/template/src/shared/utils/DatabaseUtil.ts +3 -0
  57. package/template/src/shared/utils/DecoratorUtil.ts +4 -0
  58. package/template/src/shared/utils/FolderUtil.ts +15 -0
  59. package/template/src/shared/utils/JwtUtil.ts +21 -0
  60. package/template/src/shared/utils/SwaggerUtil.ts +107 -0
  61. package/template/src/shared/utils/TimeUtil.ts +74 -0
  62. package/template/tsconfig.dev.json +9 -0
  63. package/template/tsconfig.json +33 -0
  64. package/template/tsconfig.prod.json +10 -0
@@ -0,0 +1,46 @@
1
+ import {ComponentLookup, Lookup} from "@shared/decorators/LookupDecorator";
2
+ import {IUser, IUserProfile} from "@shared/interfaces/UserInterface";
3
+ import BaseSchema from "@base/BaseSchema";
4
+ import mongoose, {Schema} from "mongoose";
5
+
6
+ @ComponentLookup("users")
7
+ export class UserLookup {
8
+ @Lookup({
9
+ from: 'profiles',
10
+ localField: 'idProfile',
11
+ foreignField: '_id',
12
+ as: 'profile',
13
+ unwind: true
14
+ })
15
+ profile?: IUserProfile;
16
+ }
17
+
18
+ const UserSchema = BaseSchema.create<IUser>({
19
+ username: {
20
+ type: String,
21
+ required: true,
22
+ unique: true
23
+ },
24
+ email: {
25
+ type: String,
26
+ required: true,
27
+ unique: true
28
+ },
29
+ password: {
30
+ type: String,
31
+ required: true
32
+ },
33
+ profileId: {
34
+ type: Schema.Types.ObjectId,
35
+ ref: 'profiles',
36
+ unique: true,
37
+ sparse: true
38
+ },
39
+ status: {
40
+ type: String,
41
+ enum: ['active', 'inactive', 'pending'],
42
+ default: 'active'
43
+ }
44
+ });
45
+ BaseSchema.addSoftDelete(UserSchema);
46
+ export default mongoose.model<IUser>("user", UserSchema);
@@ -0,0 +1,88 @@
1
+ import 'reflect-metadata';
2
+ import {CONTROLLER_METADATA, ROUTE_METADATA, SWAGGER_METADATA, PENDING_SWAGGER_METADATA} from "@shared/utils/DecoratorUtil";
3
+ import {IRouteDefinition} from "@shared/interfaces/RouteMetadataInterface";
4
+ import {ZodType} from "zod";
5
+
6
+ function mergeSwagger(target: any, propertyKey: string | symbol, patch: Record<string, any>) {
7
+ const routes: IRouteDefinition[] = Reflect.getMetadata(ROUTE_METADATA, target.constructor) || [];
8
+ const route = routes.find(r => r.handlerName === propertyKey);
9
+ if (route) {
10
+ route.swagger = { ...route.swagger, ...patch };
11
+ Reflect.defineMetadata(ROUTE_METADATA, routes, target.constructor);
12
+ } else {
13
+ // Route belum terdaftar (decorator dieksekusi bottom-up), simpan sementara
14
+ const pending: Record<string, any> = Reflect.getMetadata(PENDING_SWAGGER_METADATA, target.constructor) || {};
15
+ pending[propertyKey as string] = { ...(pending[propertyKey as string] || {}), ...patch };
16
+ Reflect.defineMetadata(PENDING_SWAGGER_METADATA, pending, target.constructor);
17
+ }
18
+ }
19
+
20
+ export function Controller(basePath: string = ''): ClassDecorator {
21
+ return (target: any) => {
22
+ Reflect.defineMetadata(CONTROLLER_METADATA, basePath, target);
23
+ };
24
+ }
25
+
26
+ export function SwaggerTag(...tags: string[]): ClassDecorator {
27
+ return (target: any) => {
28
+ const existing = Reflect.getMetadata(SWAGGER_METADATA, target) || {};
29
+ Reflect.defineMetadata(SWAGGER_METADATA, { ...existing, tags }, target);
30
+ };
31
+ }
32
+
33
+ export function GetControllerMetadata(target: any) {
34
+ return {
35
+ basePath: Reflect.getMetadata(CONTROLLER_METADATA, target) || '',
36
+ routes: Reflect.getMetadata(ROUTE_METADATA, target) || [],
37
+ swagger: Reflect.getMetadata(SWAGGER_METADATA, target) || {},
38
+ };
39
+ }
40
+
41
+ export function ApiOperation(summary: string, description?: string): MethodDecorator {
42
+ return (target: any, propertyKey, descriptor) => {
43
+ mergeSwagger(target, propertyKey, { summary, description: description || summary });
44
+ return descriptor;
45
+ };
46
+ }
47
+
48
+ export function RequiredBody(type: ZodType): MethodDecorator {
49
+ return (target: any, propertyKey, descriptor) => {
50
+ mergeSwagger(target, propertyKey, { bodyType: type });
51
+ return descriptor;
52
+ };
53
+ }
54
+
55
+ export function BodyResponse(statusCode: number, type: ZodType, description?: string): MethodDecorator {
56
+ return (target: any, propertyKey, descriptor) => {
57
+ const routes: IRouteDefinition[] = Reflect.getMetadata(ROUTE_METADATA, target.constructor) || [];
58
+ const route = routes.find(r => r.handlerName === propertyKey);
59
+ const responsesPatch = { [statusCode]: { description: description || 'Success', type } };
60
+ if (route) {
61
+ if (!route.swagger) route.swagger = {};
62
+ if (!route.swagger.responses) route.swagger.responses = {};
63
+ route.swagger.responses[statusCode] = responsesPatch[statusCode];
64
+ Reflect.defineMetadata(ROUTE_METADATA, routes, target.constructor);
65
+ } else {
66
+ const pending: Record<string, any> = Reflect.getMetadata(PENDING_SWAGGER_METADATA, target.constructor) || {};
67
+ if (!pending[propertyKey as string]) pending[propertyKey as string] = {};
68
+ if (!pending[propertyKey as string].responses) pending[propertyKey as string].responses = {};
69
+ pending[propertyKey as string].responses[statusCode] = responsesPatch[statusCode];
70
+ Reflect.defineMetadata(PENDING_SWAGGER_METADATA, pending, target.constructor);
71
+ }
72
+ return descriptor;
73
+ };
74
+ }
75
+
76
+ export function RequiredParam(type: ZodType): MethodDecorator {
77
+ return (target: any, propertyKey, descriptor) => {
78
+ mergeSwagger(target, propertyKey, { paramsType: type });
79
+ return descriptor;
80
+ };
81
+ }
82
+
83
+ export function RequiredQuery(type: ZodType): MethodDecorator {
84
+ return (target: any, propertyKey, descriptor) => {
85
+ mergeSwagger(target, propertyKey, { queryType: type });
86
+ return descriptor;
87
+ };
88
+ }
@@ -0,0 +1,51 @@
1
+ import {LookupDecoratorOptions, VirtualFieldConfig} from "@shared/interfaces/DatabaseInterface";
2
+ import LookupConfig from "@config/LookupConfig";
3
+ import MetaDataConfig from "@config/MetaDataConfig";
4
+ import {log} from "@config/LoggerConfig";
5
+
6
+ export function Virtual(config: VirtualFieldConfig) {
7
+ return function (target: any, propertyKey: string) {
8
+ const className = target.constructor.name;
9
+ if (!MetaDataConfig.virtualMetadata.has(className)) {
10
+ MetaDataConfig.virtualMetadata.set(className, new Map());
11
+ }
12
+ MetaDataConfig.virtualMetadata.get(className)!.set(propertyKey, config);
13
+ };
14
+ }
15
+
16
+ export function Lookup(options: LookupDecoratorOptions) {
17
+ return function (target: any, propertyKey: string) {
18
+ const className = target.constructor.name;
19
+ if (!MetaDataConfig.lookupMetadata.has(className)) {
20
+ MetaDataConfig.lookupMetadata.set(className, new Map());
21
+ }
22
+ MetaDataConfig.lookupMetadata.get(className)!.set(propertyKey, options);
23
+ };
24
+ }
25
+
26
+ export function ComponentLookup(collectionName: string) {
27
+ return function <T extends { new (...args: any[]): {} }>(constructor: T) {
28
+ const originalName = constructor.name;
29
+
30
+ return class extends constructor {
31
+ static collectionName = collectionName;
32
+ static originalClassName = originalName;
33
+
34
+ static registerLookups() {
35
+ const lookups = MetaDataConfig.lookupGetMetadata(originalName);
36
+
37
+ lookups.forEach((options, propertyKey) => {
38
+ LookupConfig.register(propertyKey, {
39
+ from: options.from,
40
+ localField: options.localField,
41
+ foreignField: options.foreignField,
42
+ as: options.as,
43
+ unwind: options.unwind ?? true
44
+ });
45
+
46
+ log.info(`✅ Registered lookup [${originalName}]: ${propertyKey} -> ${options.from}`);
47
+ });
48
+ }
49
+ };
50
+ };
51
+ }
@@ -0,0 +1,38 @@
1
+ import {IRouteDefinition} from "@shared/interfaces/RouteMetadataInterface";
2
+ import {ROUTE_METADATA, PENDING_SWAGGER_METADATA} from "@shared/utils/DecoratorUtil";
3
+ import {MiddlewareAccess} from "@shared/interfaces/MiddlewareInterface";
4
+
5
+ const createMappingDecorator = (method: IRouteDefinition['method']) => {
6
+ return (path: string = '', authenticate?: MiddlewareAccess): MethodDecorator => {
7
+ return (target: any, propertyKey, descriptor) => {
8
+ const routes: IRouteDefinition[] = Reflect.getMetadata(ROUTE_METADATA, target.constructor) || [];
9
+
10
+ // Ambil pending swagger yang sudah disimpan oleh decorator di bawah
11
+ const pending: Record<string, any> = Reflect.getMetadata(PENDING_SWAGGER_METADATA, target.constructor) || {};
12
+ const pendingSwagger = pending[propertyKey as string] || undefined;
13
+
14
+ routes.push({
15
+ method,
16
+ path,
17
+ handlerName: propertyKey,
18
+ authenticate,
19
+ ...(pendingSwagger ? { swagger: pendingSwagger } : {})
20
+ });
21
+
22
+ // Hapus pending swagger yang sudah di-merge
23
+ if (pendingSwagger) {
24
+ delete pending[propertyKey as string];
25
+ Reflect.defineMetadata(PENDING_SWAGGER_METADATA, pending, target.constructor);
26
+ }
27
+
28
+ Reflect.defineMetadata(ROUTE_METADATA, routes, target.constructor);
29
+ return descriptor;
30
+ };
31
+ };
32
+ }
33
+
34
+ export const GetMapping = createMappingDecorator('get');
35
+ export const PostMapping = createMappingDecorator('post');
36
+ export const PutMapping = createMappingDecorator('put');
37
+ export const DeleteMapping = createMappingDecorator('delete');
38
+ export const PatchMapping = createMappingDecorator('patch');
@@ -0,0 +1,53 @@
1
+ import {VirtualFieldConfig} from "@shared/interfaces/DatabaseInterface";
2
+
3
+ /**
4
+ * Helper to create fullName virtual from firstName and lastName
5
+ * @param fieldName - Name of the virtual field (e.g., 'fullName', 'displayName')
6
+ * @param firstNameField - Field path for first name
7
+ * @param lastNameField - Field path for last name
8
+ */
9
+ export function FullNameVirtual(fieldName: string, firstNameField: string, lastNameField: string): VirtualFieldConfig {
10
+ return {
11
+ field: fieldName,
12
+ aggregationExpression: {
13
+ $concat: [`$${firstNameField}`, ' ', `$${lastNameField}`]
14
+ }
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Helper to create age virtual from dateOfBirth
20
+ * @param fieldName - Name of the virtual field (e.g., 'age', 'yearsOld')
21
+ * @param dateOfBirthField - Field path for date of birth (epoch timestamp)
22
+ */
23
+ export function AgeVirtual(fieldName: string, dateOfBirthField: string): VirtualFieldConfig {
24
+ return {
25
+ field: fieldName,
26
+ aggregationExpression: {
27
+ $cond: {
28
+ if: { $ne: [`$${dateOfBirthField}`, null] },
29
+ then: {
30
+ $floor: {
31
+ $divide: [
32
+ { $subtract: [new Date().getTime(), `$${dateOfBirthField}`] },
33
+ 31536000000 // milliseconds in a year
34
+ ]
35
+ }
36
+ },
37
+ else: null
38
+ }
39
+ }
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Generic helper to create custom virtual field with any aggregation expression
45
+ * @param fieldName - Name of the virtual field
46
+ * @param aggregationExpression - MongoDB aggregation expression
47
+ */
48
+ export function CustomVirtual(fieldName: string, aggregationExpression: any): VirtualFieldConfig {
49
+ return {
50
+ field: fieldName,
51
+ aggregationExpression
52
+ };
53
+ }
@@ -0,0 +1,14 @@
1
+ export interface BaseEntity {
2
+ _id: ObjectId;
3
+ createdAt?: number; // Epoch timestamp (milliseconds)
4
+ updatedAt?: number; // Epoch timestamp (milliseconds)
5
+ createdBy?: ObjectId; // User ObjectId
6
+ modifiedBy?: ObjectId; // User ObjectId
7
+ deletedAt?: number; // Epoch timestamp (milliseconds)
8
+ deletedBy?: ObjectId; // User ObjectId
9
+
10
+ // Virtual fields untuk lookup
11
+ creatorUser?: any;
12
+ modifierUser?: any;
13
+ deleterUser?: any;
14
+ }
@@ -0,0 +1,41 @@
1
+
2
+ export interface LookupDecoratorOptions {
3
+ from: string;
4
+ localField: string;
5
+ foreignField: string;
6
+ as: string;
7
+ unwind?: boolean;
8
+ }
9
+
10
+ export interface ILookupConfig {
11
+ from: string;
12
+ localField: string;
13
+ foreignField: string;
14
+ as: string;
15
+ unwind?: boolean;
16
+ }
17
+
18
+ export interface VirtualFieldConfig {
19
+ field: string;
20
+ aggregationExpression: any;
21
+ }
22
+
23
+ export interface Pageable {
24
+ page: number;
25
+ size: number;
26
+ sort?: { [key: string]: 1 | -1 };
27
+ }
28
+
29
+ export interface PageResult<T> {
30
+ content: T[];
31
+ totalElements: number;
32
+ totalPages: number;
33
+ currentPage: number;
34
+ size: number;
35
+ hasNext: boolean;
36
+ hasPrevious: boolean;
37
+ }
38
+
39
+ export interface SortOption {
40
+ [key: string]: 1 | -1;
41
+ }
@@ -0,0 +1,37 @@
1
+ export interface SuccessResponseData<T = any> {
2
+ success: true;
3
+ message: string;
4
+ data?: T;
5
+ }
6
+
7
+ export interface ErrorResponseData<T = any> {
8
+ success: false;
9
+ message: string;
10
+ error?: string;
11
+ errors?: Record<string, unknown>[];
12
+ stack?: string;
13
+ }
14
+
15
+ export interface NotFoundResponse {
16
+ success: boolean;
17
+ message: string;
18
+ path: string;
19
+ method: string;
20
+ timestamp: string;
21
+ suggestion?: string;
22
+ }
23
+
24
+ export interface ErrorContext {
25
+ endpoint?: string;
26
+ method?: string;
27
+ userId?: string;
28
+ body?: Record<string, unknown>;
29
+ query?: Record<string, unknown>;
30
+ params?: Record<string, unknown>;
31
+ }
32
+
33
+ export interface ErrorLogData {
34
+ timestamp: string;
35
+ error: Error;
36
+ context?: ErrorContext;
37
+ }
@@ -0,0 +1,16 @@
1
+ export interface LoggerOptions {
2
+ enableFileLog?: boolean;
3
+ enableConsoleLog?: boolean;
4
+ logFileName?: string;
5
+ folderPath?: string;
6
+ }
7
+
8
+ export interface LogData {
9
+ timestamp: string;
10
+ method: string;
11
+ url: string;
12
+ status: number;
13
+ duration: string;
14
+ ip: string;
15
+ userAgent: string;
16
+ }
@@ -0,0 +1,14 @@
1
+ import {
2
+ MiddlewareCodeAccess,
3
+ MiddlewareCodeMenu,
4
+ MiddlewareTypeAccess
5
+ } from "@shared/models/enum/MiddlewareEnum";
6
+
7
+ export * from "@shared/models/enum/MiddlewareEnum";
8
+
9
+ export interface MiddlewareAccess {
10
+ authenticate?: boolean;
11
+ typeAccess?: MiddlewareTypeAccess;
12
+ codeAccess?: MiddlewareCodeAccess;
13
+ codeMenu?: MiddlewareCodeMenu;
14
+ }
@@ -0,0 +1,10 @@
1
+ import {ISwaggerMetadata} from "@shared/interfaces/SwaggerMetadataInterface";
2
+ import {MiddlewareAccess} from "@shared/interfaces/MiddlewareInterface";
3
+
4
+ export interface IRouteDefinition {
5
+ path: string;
6
+ method: 'get' | 'post' | 'put' | 'delete' | 'patch';
7
+ handlerName: string | symbol;
8
+ swagger?: ISwaggerMetadata;
9
+ authenticate?: MiddlewareAccess;
10
+ }
@@ -0,0 +1,8 @@
1
+
2
+ export interface SoftDeleteDocument {
3
+ deletedAt?: number;
4
+ deletedBy?: ObjectId;
5
+ delete(deletedBy: ObjectId): Promise<this>;
6
+ restore(): Promise<this>;
7
+ isDeleted(): boolean;
8
+ }
@@ -0,0 +1,12 @@
1
+ import {ZodType} from "zod";
2
+
3
+ export interface ISwaggerMetadata {
4
+ summary?: string;
5
+ description?: string;
6
+ tags?: string[];
7
+ responses?: Record<string, any>;
8
+ bodyType?: ZodType;
9
+ responseType?: ZodType;
10
+ paramsType?: ZodType;
11
+ queryType?: ZodType;
12
+ }
@@ -0,0 +1,37 @@
1
+ import {Document} from "mongoose";
2
+ import {BaseEntity} from "@shared/interfaces/BaseInterface";
3
+ import {SoftDeleteDocument} from "@shared/interfaces/SoftDeleteInterface";
4
+
5
+ export interface IUser extends Document, BaseEntity, SoftDeleteDocument {
6
+ username: string;
7
+ email: string;
8
+ password: string;
9
+ idProfile?: ObjectId;
10
+ status?: string;
11
+ profile?: IUserProfile;
12
+ }
13
+
14
+ export interface IUserProfile extends Document, BaseEntity, SoftDeleteDocument {
15
+ idUser: ObjectId;
16
+ firstName: string;
17
+ lastName: string;
18
+ phone?: string;
19
+ address?: string;
20
+ dateOfBirth?: number; // Epoch
21
+ user?: any;
22
+ }
23
+
24
+ export interface IUserCreate {
25
+ username: string;
26
+ email: string;
27
+ password: string;
28
+ status?: string;
29
+ }
30
+
31
+ export interface IUserProfileCreate {
32
+ firstName: string;
33
+ lastName: string;
34
+ phone?: string;
35
+ address?: string;
36
+ dateOfBirth?: number; // Epoch
37
+ }
@@ -0,0 +1,4 @@
1
+ import {z} from "zod";
2
+ import {UserCreateRequest} from "@shared/models/request/UserCreateRequest";
3
+
4
+ export type UserCreateInterface = z.infer<typeof UserCreateRequest>;
@@ -0,0 +1,19 @@
1
+
2
+ export enum MiddlewareTypeAccess {
3
+ READ = "read",
4
+ WRITE = "write",
5
+ DELETE = "delete",
6
+ UPDATE = "update",
7
+ }
8
+
9
+ export enum MiddlewareCodeMenu {
10
+ MENU_USER = 2100,
11
+ SUB_MENU_USER = 2101,
12
+ }
13
+
14
+ export enum MiddlewareCodeAccess {
15
+ TAMBAH,
16
+ LIHAT,
17
+ UBAH,
18
+ HAPUS,
19
+ }
@@ -0,0 +1,12 @@
1
+ import {z} from "zod";
2
+
3
+ export const UserCreateRequest = z.object({
4
+ username: z.string().min(4, "username minimal 3 karakter").max(30, "username maksimal 30 karakter"),
5
+ email: z.string().email("Invalid email address"),
6
+ password: z.string().min(6, "password minimal 6 karakter").max(100, "password maksimal 100 karakter"),
7
+ firstName: z.string().min(3, "firstName minimal 6 karakter").max(50, "firstName minimal 50 karakter"),
8
+ lastName: z.string().min(3, "lastName minimal 6 karakter").max(50, "lastName minimal 50 karakter"),
9
+ phone: z.string().optional(),
10
+ address: z.string().optional(),
11
+ dateOfBirth: z.number().optional() // Epoch timestamp
12
+ });
@@ -0,0 +1,27 @@
1
+ import {z, ZodType} from "zod";
2
+
3
+ /**
4
+ * Wrap ZodType menjadi format response sukses:
5
+ * { success: true, message: string, data: <your schema> }
6
+ */
7
+ export function SuccessResponse<T extends ZodType>(dataSchema: T) {
8
+ return z.object({
9
+ success: z.literal(true),
10
+ message: z.string(),
11
+ data: dataSchema
12
+ });
13
+ }
14
+
15
+ /**
16
+ * Wrap ZodType menjadi format response error:
17
+ * { success: false, message: string, error?: string, errors?: [...] }
18
+ */
19
+ export function ErrorResponse() {
20
+ return z.object({
21
+ success: z.literal(false),
22
+ message: z.string(),
23
+ error: z.string().optional(),
24
+ errors: z.array(z.record(z.string(), z.unknown())).optional()
25
+ });
26
+ }
27
+
@@ -0,0 +1,14 @@
1
+ import { Request } from 'express';
2
+ import {MiddlewareAccess} from "@shared/interfaces/MiddlewareInterface";
3
+
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ user?: any;
8
+ authenticate?: MiddlewareAccess;
9
+ }
10
+ }
11
+ }
12
+
13
+ export {};
14
+
@@ -0,0 +1,7 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ declare global {
4
+ type ObjectId = mongoose.Types.ObjectId;
5
+ }
6
+
7
+ export {};
@@ -0,0 +1,17 @@
1
+ import {StatusColor, StatusEmoji} from "@shared/models/enum/ColorEnum";
2
+
3
+ export function getStatusColor(status: number): StatusColor {
4
+ if (status >= 500) return StatusColor.SERVER_ERR;
5
+ if (status >= 400) return StatusColor.CLIENT_ERR;
6
+ if (status >= 300) return StatusColor.REDIRECT;
7
+ if (status >= 200) return StatusColor.SUCCESS;
8
+ return StatusColor.DEFAULT;
9
+ }
10
+
11
+ export function getStatusEmoji(status: number): StatusEmoji {
12
+ if (status >= 500) return StatusEmoji.SERVER_ERR;
13
+ if (status >= 400) return StatusEmoji.CLIENT_ERR;
14
+ if (status >= 300) return StatusEmoji.REDIRECT;
15
+ if (status >= 200) return StatusEmoji.SUCCESS;
16
+ return StatusEmoji.DEFAULT;
17
+ }
@@ -0,0 +1,3 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export const dbCreateSeason = async () => await mongoose.startSession()
@@ -0,0 +1,4 @@
1
+ export const CONTROLLER_METADATA = Symbol('controller');
2
+ export const ROUTE_METADATA = Symbol('route');
3
+ export const SWAGGER_METADATA = Symbol('swagger');
4
+ export const PENDING_SWAGGER_METADATA = Symbol('pending_swagger');
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ import * as fs from "node:fs";
3
+
4
+ export default class FolderUtil {
5
+ static mkdir(folderPath: string): string {
6
+ if(!folderPath)
7
+ throw new Error("Folder path is required");
8
+ const basePath = path.join(__dirname, "../../../" + folderPath);
9
+ if (!fs.existsSync(basePath)) {
10
+ fs.mkdirSync(basePath, { recursive: true });
11
+ return folderPath;
12
+ }
13
+ return folderPath;
14
+ }
15
+ }
@@ -0,0 +1,21 @@
1
+ import type {StringValue} from "ms";
2
+ import jwt, {JwtPayload, SignOptions} from "jsonwebtoken";
3
+ import ForbiddenException from "@core/exceptions/ForbiddenException";
4
+
5
+ export default class JwtUtil {
6
+ public static async validateToken(token: string): Promise<string | JwtPayload> {
7
+ try {
8
+ return jwt.verify(token, process.env.JWT_SECRET_KEY as string);
9
+ } catch (err) {
10
+ throw new ForbiddenException("Invalid or expired token");
11
+ }
12
+ }
13
+
14
+ public static async createToken(payload: object, expiresIn: StringValue | number = '1h'): Promise<string> {
15
+ const options: SignOptions = {
16
+ algorithm: "HS256",
17
+ expiresIn
18
+ }
19
+ return jwt.sign(payload, process.env.JWT_SECRET_KEY as string, options);
20
+ }
21
+ }