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,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,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,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,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,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
|
+
}
|