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.
- package/README.md +79 -0
- package/dist/bin/index.d.ts +2 -0
- package/dist/bin/index.js +286 -0
- package/package.json +37 -0
- package/template/.env.example +7 -0
- package/template/nodemon.json +7 -0
- package/template/package.json +21 -0
- package/template/src/App.ts +1 -0
- package/template/src/api/controllers/UserController.ts +54 -0
- package/template/src/api/middlewares/AuthMiddleware.ts +24 -0
- package/template/src/base/BaseRepository.ts +112 -0
- package/template/src/base/BaseResponse.ts +45 -0
- package/template/src/base/BaseResponseError.ts +12 -0
- package/template/src/base/BaseSchema.ts +82 -0
- package/template/src/config/DateTimeConfig.ts +4 -0
- package/template/src/config/GlobalHandlerError.ts +81 -0
- package/template/src/config/LoggerConfig.ts +157 -0
- package/template/src/config/LookupConfig.ts +68 -0
- package/template/src/config/MetaDataConfig.ts +25 -0
- package/template/src/config/RouteConfig.ts +98 -0
- package/template/src/config/SwaggerConfig.ts +147 -0
- package/template/src/config/envConfig.ts +5 -0
- package/template/src/core/Server.ts +75 -0
- package/template/src/core/exceptions/BadRequestException.ts +7 -0
- package/template/src/core/exceptions/ForbiddenException.ts +7 -0
- package/template/src/core/exceptions/NotFoundException.ts +7 -0
- package/template/src/core/exceptions/UnauthorizedException.ts +7 -0
- package/template/src/core/repositories/UserRepository.ts +53 -0
- package/template/src/database/DatabaseConnection.ts +11 -0
- package/template/src/database/builder/CustomBuilder.ts +177 -0
- package/template/src/database/builder/CustomFilter.ts +12 -0
- package/template/src/database/builder/MultipleSearchCriteria.ts +25 -0
- package/template/src/database/builder/SearchCriteria.ts +79 -0
- package/template/src/database/entity/ProfileModel.ts +54 -0
- package/template/src/database/entity/UserModel.ts +46 -0
- package/template/src/shared/decorators/ApiDecorator.ts +88 -0
- package/template/src/shared/decorators/LookupDecorator.ts +51 -0
- package/template/src/shared/decorators/MethodeDecorator.ts +38 -0
- package/template/src/shared/helpers/VirtualHelper.ts +53 -0
- package/template/src/shared/interfaces/BaseInterface.ts +14 -0
- package/template/src/shared/interfaces/DatabaseInterface.ts +41 -0
- package/template/src/shared/interfaces/HttpInterface.ts +37 -0
- package/template/src/shared/interfaces/LoggerOptionsInterface.ts +16 -0
- package/template/src/shared/interfaces/MiddlewareInterface.ts +14 -0
- package/template/src/shared/interfaces/RouteMetadataInterface.ts +10 -0
- package/template/src/shared/interfaces/SoftDeleteInterface.ts +8 -0
- package/template/src/shared/interfaces/SwaggerMetadataInterface.ts +12 -0
- package/template/src/shared/interfaces/UserInterface.ts +37 -0
- package/template/src/shared/interfaces/user/UserCreateInterface.ts +4 -0
- package/template/src/shared/models/enum/MiddlewareEnum.ts +19 -0
- package/template/src/shared/models/request/UserCreateRequest.ts +12 -0
- package/template/src/shared/models/response/ApiResponse.ts +27 -0
- package/template/src/shared/types/express.d.ts +14 -0
- package/template/src/shared/types/mongoose.d.ts +7 -0
- package/template/src/shared/utils/ColorUtil.ts +17 -0
- package/template/src/shared/utils/DatabaseUtil.ts +3 -0
- package/template/src/shared/utils/DecoratorUtil.ts +4 -0
- package/template/src/shared/utils/FolderUtil.ts +15 -0
- package/template/src/shared/utils/JwtUtil.ts +21 -0
- package/template/src/shared/utils/SwaggerUtil.ts +107 -0
- package/template/src/shared/utils/TimeUtil.ts +74 -0
- package/template/tsconfig.dev.json +9 -0
- package/template/tsconfig.json +33 -0
- package/template/tsconfig.prod.json +10 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {Response} from "express";
|
|
2
|
+
import {SuccessResponseData} from "@shared/interfaces/HttpInterface";
|
|
3
|
+
|
|
4
|
+
export default class BaseResponse {
|
|
5
|
+
|
|
6
|
+
static async ok<T>(
|
|
7
|
+
res: Response,
|
|
8
|
+
serviceFunction: () => Promise<T>,
|
|
9
|
+
message: string = 'Success'
|
|
10
|
+
): Promise<Response> {
|
|
11
|
+
const data = await serviceFunction();
|
|
12
|
+
|
|
13
|
+
const response: SuccessResponseData<T> = {
|
|
14
|
+
success: true,
|
|
15
|
+
message,
|
|
16
|
+
data,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return res.status(200).json(response);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static async created<T>(
|
|
23
|
+
res: Response,
|
|
24
|
+
serviceFunction: () => Promise<T>,
|
|
25
|
+
message: string = 'Resource created successfully'
|
|
26
|
+
): Promise<Response> {
|
|
27
|
+
const data = await serviceFunction();
|
|
28
|
+
|
|
29
|
+
const response: SuccessResponseData<T> = {
|
|
30
|
+
success: true,
|
|
31
|
+
message,
|
|
32
|
+
data,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return res.status(201).json(response);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static async noContent(
|
|
39
|
+
res: Response,
|
|
40
|
+
serviceFunction: () => Promise<void>
|
|
41
|
+
): Promise<Response> {
|
|
42
|
+
await serviceFunction();
|
|
43
|
+
return res.status(204).send();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
export default class BaseResponseError extends Error {
|
|
3
|
+
public statusCode: number;
|
|
4
|
+
public errors?: Record<string, unknown>[];
|
|
5
|
+
constructor(statusCode: number, message: string, errors?: Record<string, unknown>[]) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.errors = errors;
|
|
9
|
+
this.name = this.constructor.name;
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {Schema} from "mongoose";
|
|
2
|
+
|
|
3
|
+
export default class BaseSchema {
|
|
4
|
+
public static create <T>(schemaDefinition: any, options?: any): Schema<T> {
|
|
5
|
+
const baseFields = {
|
|
6
|
+
createdAt: {
|
|
7
|
+
type: Number,
|
|
8
|
+
required: false,
|
|
9
|
+
index: true
|
|
10
|
+
},
|
|
11
|
+
updatedAt: {
|
|
12
|
+
type: Number,
|
|
13
|
+
required: false
|
|
14
|
+
},
|
|
15
|
+
createdBy: {
|
|
16
|
+
type: Schema.Types.ObjectId,
|
|
17
|
+
ref: 'users',
|
|
18
|
+
required: false,
|
|
19
|
+
index: true
|
|
20
|
+
},
|
|
21
|
+
modifiedBy: {
|
|
22
|
+
type: Schema.Types.ObjectId,
|
|
23
|
+
ref: 'users',
|
|
24
|
+
required: false
|
|
25
|
+
},
|
|
26
|
+
deletedAt: {
|
|
27
|
+
type: Number,
|
|
28
|
+
required: false
|
|
29
|
+
},
|
|
30
|
+
deletedBy: {
|
|
31
|
+
type: Schema.Types.ObjectId,
|
|
32
|
+
ref: 'users',
|
|
33
|
+
required: false
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const mergedDefinition = {
|
|
37
|
+
...schemaDefinition,
|
|
38
|
+
...baseFields
|
|
39
|
+
};
|
|
40
|
+
const mergedOptions = {
|
|
41
|
+
timestamps: false,
|
|
42
|
+
versionKey: false,
|
|
43
|
+
...options
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const schema = new Schema<T>(mergedDefinition, mergedOptions);
|
|
47
|
+
|
|
48
|
+
schema.pre('save', function(next) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const doc = this as any;
|
|
51
|
+
if (doc.isNew) {
|
|
52
|
+
doc.createdAt = now;
|
|
53
|
+
}
|
|
54
|
+
doc.updatedAt = now;
|
|
55
|
+
next();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
schema.pre(['findOneAndUpdate', 'updateOne', 'updateMany'], function(next) {
|
|
59
|
+
this.set({ updatedAt: Date.now() });
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return schema;
|
|
64
|
+
}
|
|
65
|
+
public static addSoftDelete<T>(schema: Schema<T>): void {
|
|
66
|
+
schema.methods.delete = async function(deletedBy: any) {
|
|
67
|
+
this.deletedAt = Date.now();
|
|
68
|
+
this.deletedBy = deletedBy;
|
|
69
|
+
return await this.save();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
schema.methods.restore = async function() {
|
|
73
|
+
this.deletedAt = undefined;
|
|
74
|
+
this.deletedBy = undefined;
|
|
75
|
+
return await this.save();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
schema.methods.isDeleted = function() {
|
|
79
|
+
return !!this.deletedAt;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {Response, Request, NextFunction} from "express";
|
|
2
|
+
import BaseResponseError from "@base/BaseResponseError";
|
|
3
|
+
import {ErrorContext, ErrorResponseData, NotFoundResponse} from "@shared/interfaces/HttpInterface";
|
|
4
|
+
import {log} from "@config/LoggerConfig";
|
|
5
|
+
import TimeUtil from "@shared/utils/TimeUtil";
|
|
6
|
+
|
|
7
|
+
const findSimilarRoute = (requestedPath: string, routes: string[]): string | null => {
|
|
8
|
+
// Simple similarity check - bisa diganti dengan library seperti 'string-similarity'
|
|
9
|
+
for (const route of routes) {
|
|
10
|
+
if (requestedPath.toLowerCase().includes(route.toLowerCase().split('/')[2])) {
|
|
11
|
+
return route;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default class {
|
|
18
|
+
static error(
|
|
19
|
+
err: Error | BaseResponseError,
|
|
20
|
+
req: Request,
|
|
21
|
+
res: Response,
|
|
22
|
+
next: NextFunction
|
|
23
|
+
) {
|
|
24
|
+
const context: ErrorContext = {
|
|
25
|
+
endpoint: req.path,
|
|
26
|
+
method: req.method,
|
|
27
|
+
userId: (req as any).user?.id, // jika ada authentication
|
|
28
|
+
body: req.body,
|
|
29
|
+
query: req.query,
|
|
30
|
+
params: req.params
|
|
31
|
+
};
|
|
32
|
+
log.server(err, context);
|
|
33
|
+
|
|
34
|
+
// Jika error adalah BaseResponseError
|
|
35
|
+
if (err instanceof BaseResponseError) {
|
|
36
|
+
const response: ErrorResponseData = {
|
|
37
|
+
success: false,
|
|
38
|
+
message: err.message,
|
|
39
|
+
...(err.errors && { errors: err.errors }),
|
|
40
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
|
41
|
+
};
|
|
42
|
+
res.status(err.statusCode).json(response);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Default error (500)
|
|
47
|
+
const response: ErrorResponseData = {
|
|
48
|
+
success: false,
|
|
49
|
+
message: err.message || 'Internal server error',
|
|
50
|
+
...(process.env.NODE_ENV === 'development' && {
|
|
51
|
+
error: err.toString(),
|
|
52
|
+
stack: err.stack
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
res.status(500).json(response);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
static notFound(
|
|
59
|
+
req: Request,
|
|
60
|
+
res: Response,
|
|
61
|
+
next: NextFunction
|
|
62
|
+
) {
|
|
63
|
+
const response: NotFoundResponse = {
|
|
64
|
+
success: false,
|
|
65
|
+
message: 'API endpoint not found',
|
|
66
|
+
path: req.originalUrl || req.url,
|
|
67
|
+
method: req.method,
|
|
68
|
+
timestamp: TimeUtil.getReadableDayDateTime(new Date())
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Optional: Tambahkan suggestion untuk typo umum
|
|
72
|
+
const availableRoutes = [''];
|
|
73
|
+
const similarRoute = findSimilarRoute(req.path, availableRoutes);
|
|
74
|
+
|
|
75
|
+
if (similarRoute) {
|
|
76
|
+
response.suggestion = `Did you mean ${similarRoute}?`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.status(404).json(response);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import TimeUtil from "@shared/utils/TimeUtil";
|
|
2
|
+
import {LogData, LoggerOptions} from "@shared/interfaces/LoggerOptionsInterface";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import FolderUtil from "@shared/utils/FolderUtil";
|
|
5
|
+
import {NextFunction, Request, Response} from "express";
|
|
6
|
+
import {getStatusColor, getStatusEmoji} from "@shared/utils/ColorUtil";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import {ErrorContext, ErrorLogData} from "@shared/interfaces/HttpInterface";
|
|
9
|
+
import BaseResponseError from "@base/BaseResponseError";
|
|
10
|
+
import {Color} from "@shared/models/enum/ColorEnum";
|
|
11
|
+
|
|
12
|
+
export const serverLogger = (options: LoggerOptions = {}) => {
|
|
13
|
+
const {
|
|
14
|
+
enableFileLog = true,
|
|
15
|
+
enableConsoleLog = true,
|
|
16
|
+
logFileName = `app-${new Date().toISOString().split('T')[0]}.log`,
|
|
17
|
+
folderPath = 'logs'
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
const logFilePath = path.join(FolderUtil.mkdir(folderPath), logFileName);
|
|
21
|
+
|
|
22
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
|
|
25
|
+
// Capture response
|
|
26
|
+
const originalSend = res.send;
|
|
27
|
+
let responseBody: any;
|
|
28
|
+
|
|
29
|
+
res.send = function (data: any): Response {
|
|
30
|
+
responseBody = data;
|
|
31
|
+
return originalSend.apply(res, [data]);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Log ketika response selesai
|
|
35
|
+
res.on('finish', () => {
|
|
36
|
+
const duration = Date.now() - startTime;
|
|
37
|
+
const timestamp = TimeUtil.getTimestamp();
|
|
38
|
+
|
|
39
|
+
// Data log
|
|
40
|
+
const logData: LogData = {
|
|
41
|
+
timestamp,
|
|
42
|
+
method: req.method,
|
|
43
|
+
url: req.originalUrl || req.url,
|
|
44
|
+
status: res.statusCode,
|
|
45
|
+
duration: `${duration}ms`,
|
|
46
|
+
ip: req.ip || req.socket.remoteAddress || 'Unknown',
|
|
47
|
+
userAgent: req.get('user-agent') || 'Unknown'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Console log dengan warna dan emoji
|
|
51
|
+
if (enableConsoleLog) {
|
|
52
|
+
const statusColor = getStatusColor(res.statusCode);
|
|
53
|
+
const emoji = getStatusEmoji(res.statusCode);
|
|
54
|
+
|
|
55
|
+
console.log(
|
|
56
|
+
`${Color.GRAY}[${timestamp}]${Color.RESET} ` +
|
|
57
|
+
`${emoji} ` +
|
|
58
|
+
`${Color.BRIGHT}${req.method}${Color.RESET} ` +
|
|
59
|
+
`${Color.CYAN}${req.originalUrl || req.url}${Color.RESET} ` +
|
|
60
|
+
`${statusColor}${res.statusCode}${Color.RESET} ` +
|
|
61
|
+
`${Color.MAGENTA}${TimeUtil.formatDuration(duration)}${Color.RESET} ` +
|
|
62
|
+
`${Color.GRAY}(${logData.ip})${Color.RESET}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// File log (format yang lebih mudah dibaca)
|
|
67
|
+
if (enableFileLog) {
|
|
68
|
+
const logEntry = [
|
|
69
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
70
|
+
`Timestamp : ${timestamp}`,
|
|
71
|
+
`Method : ${logData.method}`,
|
|
72
|
+
`URL : ${logData.url}`,
|
|
73
|
+
`Status : ${logData.status}`,
|
|
74
|
+
`Duration : ${logData.duration}`,
|
|
75
|
+
`IP Address : ${logData.ip}`,
|
|
76
|
+
`User Agent : ${logData.userAgent}`,
|
|
77
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`
|
|
78
|
+
].join('\n');
|
|
79
|
+
|
|
80
|
+
fs.appendFile(logFilePath, logEntry, (err) => {
|
|
81
|
+
if (err) console.error('Error writing to log file:', err);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
next();
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export class log {
|
|
91
|
+
static info (...message: unknown[]) {
|
|
92
|
+
const text = message
|
|
93
|
+
.map(item =>
|
|
94
|
+
typeof item === 'string'
|
|
95
|
+
? item
|
|
96
|
+
: JSON.stringify(item)
|
|
97
|
+
)
|
|
98
|
+
.join(' ');
|
|
99
|
+
|
|
100
|
+
const log = `${Color.BLUE}ℹ️ INFO${Color.RESET} [${TimeUtil.getTimeString()}] ${text}`;
|
|
101
|
+
console.log(log);
|
|
102
|
+
// const log = `${Color.BLUE}ℹ️ INFO${Color.RESET} [${TimeUtil.getTimestamp()}] ${message}`;
|
|
103
|
+
// console.log(log);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static success(message: string) {
|
|
107
|
+
const log = `${Color.GREEN}✅ SUCCESS${Color.RESET} [${TimeUtil.getTimeString()}] ${message}`;
|
|
108
|
+
console.log(log);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static warning(message: string) {
|
|
112
|
+
const log = `${Color.YELLOW}⚠️ WARNING${Color.RESET} [${TimeUtil.getTimeString()}] ${message}`;
|
|
113
|
+
console.log(log);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static error(...message: unknown[]) {
|
|
117
|
+
const text = message
|
|
118
|
+
.map(item =>
|
|
119
|
+
typeof item === 'string'
|
|
120
|
+
? item
|
|
121
|
+
: JSON.stringify(item)
|
|
122
|
+
)
|
|
123
|
+
.join(' ');
|
|
124
|
+
|
|
125
|
+
const log =
|
|
126
|
+
`${Color.YELLOW}❌ ERROR${Color.RED} ` +
|
|
127
|
+
`[${TimeUtil.getTimeString()}] ${text}`;
|
|
128
|
+
|
|
129
|
+
console.log(log);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static server(
|
|
133
|
+
error: Error,
|
|
134
|
+
context?: ErrorContext
|
|
135
|
+
) {
|
|
136
|
+
const errorLog: ErrorLogData = {
|
|
137
|
+
timestamp: TimeUtil.getTimeString(),
|
|
138
|
+
error: error,
|
|
139
|
+
context: context,
|
|
140
|
+
};
|
|
141
|
+
log.error('Time:', errorLog.timestamp);
|
|
142
|
+
log.error('Error Name:', error.name);
|
|
143
|
+
log.error('Message:', error.message);
|
|
144
|
+
log.error('Stack:', error.stack);
|
|
145
|
+
|
|
146
|
+
if (error instanceof BaseResponseError) {
|
|
147
|
+
log.error('Status Code:', error.statusCode);
|
|
148
|
+
if (error.errors) {
|
|
149
|
+
log.error('Validation Errors:', JSON.stringify(error.errors, null, 2));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (context) {
|
|
154
|
+
log.error('Context:', JSON.stringify(context, null, 2));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {ILookupConfig} from "@shared/interfaces/DatabaseInterface";
|
|
2
|
+
import {log} from "@config/LoggerConfig";
|
|
3
|
+
|
|
4
|
+
export default class LookupConfig {
|
|
5
|
+
private static lookups: Map<string, ILookupConfig> = new Map();
|
|
6
|
+
|
|
7
|
+
static register(prefix: string, config: ILookupConfig): void {
|
|
8
|
+
this.lookups.set(prefix, config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static get(prefix: string): ILookupConfig | undefined {
|
|
12
|
+
return this.lookups.get(prefix);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static getLookupForPath(path: string): { prefix: string; config: ILookupConfig } | null {
|
|
16
|
+
for (const [prefix, config] of this.lookups.entries()) {
|
|
17
|
+
if (path.startsWith(prefix + '.')) {
|
|
18
|
+
return { prefix, config };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getAll(): Map<string, ILookupConfig> {
|
|
25
|
+
return this.lookups;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static init(entities: any[]) {
|
|
29
|
+
log.info("━".repeat(60));
|
|
30
|
+
log.info('🚀 Initializing lookups...');
|
|
31
|
+
|
|
32
|
+
// Auto-register dari decorators
|
|
33
|
+
for (const entity of entities) {
|
|
34
|
+
if (typeof entity.registerLookups === 'function') {
|
|
35
|
+
entity.registerLookups();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Register lookup untuk audit fields (GLOBAL - untuk semua entity)
|
|
40
|
+
LookupConfig.register('creatorUser', {
|
|
41
|
+
from: 'users',
|
|
42
|
+
localField: 'createdBy',
|
|
43
|
+
foreignField: '_id',
|
|
44
|
+
as: 'creatorUser',
|
|
45
|
+
unwind: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
LookupConfig.register('modifierUser', {
|
|
49
|
+
from: 'users',
|
|
50
|
+
localField: 'modifiedBy',
|
|
51
|
+
foreignField: '_id',
|
|
52
|
+
as: 'modifierUser',
|
|
53
|
+
unwind: true
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
LookupConfig.register('deleterUser', {
|
|
57
|
+
from: 'users',
|
|
58
|
+
localField: 'deletedBy',
|
|
59
|
+
foreignField: '_id',
|
|
60
|
+
as: 'deleterUser',
|
|
61
|
+
unwind: true
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
log.info('✅ All lookups initialized');
|
|
65
|
+
log.info('📋 Registered lookups:', Array.from(LookupConfig.getAll().keys()));
|
|
66
|
+
log.info("━".repeat(60));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {LookupDecoratorOptions, VirtualFieldConfig} from "@shared/interfaces/DatabaseInterface";
|
|
2
|
+
|
|
3
|
+
export default class MetaDataConfig {
|
|
4
|
+
public static lookupMetadata = new Map<string, Map<string, LookupDecoratorOptions>>();
|
|
5
|
+
|
|
6
|
+
public static virtualMetadata = new Map<string, Map<string, VirtualFieldConfig>>();
|
|
7
|
+
|
|
8
|
+
public static lookupGetMetadata(className: string): Map<string, LookupDecoratorOptions> {
|
|
9
|
+
return this.lookupMetadata.get(className) || new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public static lookupGetKeys(className: string): string[] {
|
|
13
|
+
const metadata = this.lookupMetadata.get(className);
|
|
14
|
+
return metadata ? Array.from(metadata.keys()) : [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public static virtualGetMetadata(className: string): Map<string, VirtualFieldConfig> {
|
|
18
|
+
return this.virtualMetadata.get(className) || new Map();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public static virtualGetFields(className: string): VirtualFieldConfig[] {
|
|
22
|
+
const metadata = this.virtualMetadata.get(className);
|
|
23
|
+
return metadata ? Array.from(metadata.values()) : [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {Router, Request, Response, NextFunction} from "express";
|
|
2
|
+
import {GetControllerMetadata} from "@shared/decorators/ApiDecorator";
|
|
3
|
+
import AuthMiddleware from "@api/middlewares/AuthMiddleware";
|
|
4
|
+
import {IRouteDefinition} from "@shared/interfaces/RouteMetadataInterface";
|
|
5
|
+
|
|
6
|
+
export default class RouteConfig {
|
|
7
|
+
protected static controllers: any[] = [];
|
|
8
|
+
|
|
9
|
+
static register(...controller: any) {
|
|
10
|
+
console.log('📝 Registering controllers:', controller.map((c: any) => c.name));
|
|
11
|
+
this.controllers.push(...controller);
|
|
12
|
+
console.log('📝 Total controllers:', this.controllers.length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static getControllers() {
|
|
16
|
+
return this.controllers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static buildRoutes(router: Router) {
|
|
20
|
+
console.log('🔷 buildRoutes called, controllers count:', this.controllers.length);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
this.controllers.forEach((ControllerClass, index) => {
|
|
24
|
+
console.log(`🔷 Processing controller ${index+1}:`, ControllerClass.name);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const instance = new ControllerClass();
|
|
28
|
+
const metadata = GetControllerMetadata(ControllerClass);
|
|
29
|
+
|
|
30
|
+
// console.log('🔷 Metadata:', metadata);
|
|
31
|
+
|
|
32
|
+
if (!metadata) {
|
|
33
|
+
console.error('❌ No metadata found for controller:', ControllerClass.name);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!metadata.routes || !Array.isArray(metadata.routes)) {
|
|
38
|
+
console.error('❌ No routes found in metadata for controller:', ControllerClass.name);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
metadata.routes.forEach((route: IRouteDefinition) => {
|
|
43
|
+
const fullPath = `${metadata.basePath}${route.path}`;
|
|
44
|
+
const handler = (instance as any)[route.handlerName as string].bind(instance);
|
|
45
|
+
|
|
46
|
+
console.log(`🔷 Registering route: ${route.method.toString().toUpperCase()} ${fullPath}`);
|
|
47
|
+
|
|
48
|
+
// requireAuth: true hanya jika authenticate.authenticate === true (default: false)
|
|
49
|
+
const requireAuth = route.authenticate?.authenticate === true;
|
|
50
|
+
|
|
51
|
+
const middleware = requireAuth
|
|
52
|
+
? [
|
|
53
|
+
// Inject route.authenticate ke req sebelum AuthMiddleware berjalan
|
|
54
|
+
(req: Request, _res: Response, next: NextFunction) => {
|
|
55
|
+
req.authenticate = route.authenticate;
|
|
56
|
+
next();
|
|
57
|
+
},
|
|
58
|
+
AuthMiddleware,
|
|
59
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await handler(req, res, next);
|
|
62
|
+
if (result !== undefined && !res.headersSent) {
|
|
63
|
+
res.json(result);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
next(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
: [async (req: Request, res: Response, next: NextFunction) => {
|
|
71
|
+
try {
|
|
72
|
+
const result = await handler(req, res, next);
|
|
73
|
+
if (result !== undefined && !res.headersSent) {
|
|
74
|
+
res.json(result);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.log('🟡 Error caught in RouteConfig (no auth):', error);
|
|
78
|
+
next(error);
|
|
79
|
+
}
|
|
80
|
+
}];
|
|
81
|
+
|
|
82
|
+
(router as any)[route.method](fullPath, ...middleware);
|
|
83
|
+
});
|
|
84
|
+
} catch (controllerError) {
|
|
85
|
+
console.error('❌ Error processing controller:', ControllerClass.name, controllerError);
|
|
86
|
+
throw controllerError;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log('✅ All routes registered successfully');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('❌ Error in buildRoutes:', error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return router;
|
|
97
|
+
}
|
|
98
|
+
}
|