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,147 @@
1
+ import RouteConfig from "@config/RouteConfig";
2
+ import {GetControllerMetadata} from "@shared/decorators/ApiDecorator";
3
+ import SwaggerUtil from "@shared/utils/SwaggerUtil";
4
+ import {IRouteDefinition} from "@shared/interfaces/RouteMetadataInterface";
5
+ import {ErrorResponse, SuccessResponse} from "@shared/models/response/ApiResponse";
6
+ import {z} from "zod";
7
+
8
+ // Default responses yang otomatis dipakai jika @BodyResponse tidak didefinisikan
9
+ const DEFAULT_RESPONSES: Record<string, { description: string; schema: any }> = {
10
+ '200': {
11
+ description: 'Success',
12
+ schema: SwaggerUtil.generateSchema(SuccessResponse(z.object({})))
13
+ },
14
+ '400': {
15
+ description: 'Bad Request',
16
+ schema: SwaggerUtil.generateSchema(ErrorResponse())
17
+ },
18
+ '401': {
19
+ description: 'Unauthorized',
20
+ schema: SwaggerUtil.generateSchema(ErrorResponse())
21
+ },
22
+ '500': {
23
+ description: 'Internal Server Error',
24
+ schema: SwaggerUtil.generateSchema(ErrorResponse())
25
+ }
26
+ };
27
+
28
+
29
+ export default class SwaggerConfig extends RouteConfig {
30
+
31
+ protected static MainObject = {
32
+ openapi: '3.0.0',
33
+ info: {
34
+ title: 'REST API Documentation',
35
+ version: '1.0.0',
36
+ description: 'Auto-generated API documentation with TypeScript decorators'
37
+ },
38
+ servers: [
39
+ {
40
+ url: 'http://localhost:3000',
41
+ description: 'Local development server'
42
+ }
43
+ ]
44
+ ,
45
+ components: {
46
+ securitySchemes: {
47
+ bearerAuth: {
48
+ type: 'http',
49
+ scheme: 'bearer',
50
+ bearerFormat: 'JWT',
51
+ description: 'Enter your JWT token'
52
+ }
53
+ }
54
+ }
55
+ };
56
+
57
+ static generate() {
58
+ const paths: any = {};
59
+
60
+ RouteConfig.getControllers().forEach(ControllerClass => {
61
+ const metadata = GetControllerMetadata(ControllerClass);
62
+
63
+ metadata.routes.forEach((route: IRouteDefinition) => {
64
+ const fullPath = `${metadata.basePath}${route.path}`
65
+ .replace(/:/g, '{')
66
+ .replace(/{(\w+)}/g, '{$1}');
67
+
68
+ if (!paths[fullPath]) {
69
+ paths[fullPath] = {};
70
+ }
71
+
72
+ const swagger = route.swagger || {};
73
+ const requireAuth = route.authenticate?.authenticate === true; // true hanya jika explisit { authenticate: true }
74
+
75
+ const operation: any = {
76
+ tags: metadata.swagger.tags || [],
77
+ summary: swagger.summary || '',
78
+ description: swagger.description || swagger.summary || '',
79
+ parameters: [],
80
+ responses: {},
81
+ // Add security if authentication required
82
+ ...(requireAuth && { security: [{ bearerAuth: [] }] })
83
+ };
84
+
85
+ if (swagger.paramsType) {
86
+ operation.parameters.push(
87
+ ...SwaggerUtil.generateParameters(swagger.paramsType, 'path')
88
+ );
89
+ }
90
+
91
+ if (swagger.queryType) {
92
+ operation.parameters.push(
93
+ ...SwaggerUtil.generateParameters(swagger.queryType, 'query')
94
+ );
95
+ }
96
+
97
+ if (swagger.bodyType) {
98
+ operation.requestBody = {
99
+ required: true,
100
+ content: {
101
+ 'application/json': {
102
+ schema: SwaggerUtil.generateSchema(swagger.bodyType)
103
+ }
104
+ }
105
+ };
106
+ }
107
+
108
+ // Mulai dari default responses, lalu timpa dengan yang didefinisikan via @BodyResponse
109
+ const mergedResponses: Record<string, any> = {
110
+ ...DEFAULT_RESPONSES,
111
+ ...(requireAuth ? {} : { '401': undefined }) // hapus 401 default jika tidak butuh auth
112
+ };
113
+
114
+ if (swagger.responses) {
115
+ for (const [statusCode, config] of Object.entries(swagger.responses)) {
116
+ const responseConfig: any = config;
117
+ mergedResponses[statusCode] = {
118
+ description: responseConfig.description || 'Success',
119
+ schema: responseConfig.type
120
+ ? SwaggerUtil.generateSchema(responseConfig.type)
121
+ : { type: 'object' }
122
+ };
123
+ }
124
+ }
125
+
126
+ for (const [statusCode, config] of Object.entries(mergedResponses)) {
127
+ if (!config) continue; // skip yang di-undefined-kan
128
+ operation.responses[statusCode] = {
129
+ description: config.description,
130
+ content: {
131
+ 'application/json': {
132
+ schema: config.schema
133
+ }
134
+ }
135
+ };
136
+ }
137
+
138
+ paths[fullPath][route.method] = operation;
139
+ });
140
+ });
141
+
142
+ return {
143
+ ...this.MainObject,
144
+ paths
145
+ };
146
+ }
147
+ }
@@ -0,0 +1,5 @@
1
+ import {config} from "dotenv";
2
+ const env = process.env.NODE_ENV || 'dev'
3
+ config({
4
+ path: `.env.${env}`
5
+ })
@@ -0,0 +1,75 @@
1
+ import "@config/envConfig"
2
+ import cors from "cors";
3
+ import express, {Router} from "express";
4
+ import swaggerUi from 'swagger-ui-express';
5
+ import expressRateLimit from "express-rate-limit";
6
+ import {log, serverLogger} from "@config/LoggerConfig";
7
+ import GlobalHandlerError from "@config/GlobalHandlerError";
8
+ import UnauthorizedException from "@core/exceptions/UnauthorizedException";
9
+ import DatabaseConnection from "@database/DatabaseConnection";
10
+ import LookupConfig from "@config/LookupConfig";
11
+ import {UserLookup} from "@database/entity/UserModel";
12
+ import {ProfileLookup} from "@database/entity/ProfileModel";
13
+ import SwaggerConfig from "@config/SwaggerConfig";
14
+ import RouteConfig from "@config/RouteConfig";
15
+ import UserController from "@api/controllers/UserController";
16
+
17
+ DatabaseConnection().then(() => {
18
+ log.info("✅ Database connected successfully.");
19
+ LookupConfig.init([UserLookup, ProfileLookup])
20
+ log.info("🚀 Starting server...");
21
+ RouteConfig.register(UserController)
22
+ const app = express();
23
+
24
+ app.use(serverLogger({
25
+ enableFileLog: false,
26
+ enableConsoleLog: true
27
+ }))
28
+
29
+ app.use(express.json({
30
+ limit: '20mb'
31
+ }));
32
+
33
+ app.use(express.urlencoded({extended: true}));
34
+ app.use(cors({
35
+ origin: '*',
36
+ methods: ['GET', 'POST', 'DELETE'],
37
+ allowedHeaders: "*"
38
+ }));
39
+
40
+ // Rate Limiter
41
+ const limiter = expressRateLimit({
42
+ windowMs: 15 * 60 * 1000, // 15 minutes
43
+ max: 100, // limit each IP to 100 requests per windowMs
44
+ standardHeaders: true,
45
+ legacyHeaders: false,
46
+ message: 'Too many requests from this IP, please try again later.'
47
+ });
48
+ app.use(limiter);
49
+
50
+ // Register routes
51
+ const router = RouteConfig.buildRoutes(Router());
52
+ app.use(router);
53
+
54
+ // Test error endpoint
55
+ app.get("/test-error", (req, res, next) => {
56
+ next(new UnauthorizedException("Test unauthorized error"));
57
+ });
58
+
59
+ const swaggerDoc = SwaggerConfig.generate();
60
+ app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDoc));
61
+
62
+ // 404 handler harus sebelum error handler
63
+ app.use(GlobalHandlerError.notFound);
64
+
65
+ // Error handler harus terakhir (4 parameters)
66
+ app.use(GlobalHandlerError.error);
67
+
68
+ app.listen(process.env.SERVER_PORT || 3000, () => {
69
+ log.info('Server is running on port ' + (process.env.SERVER_PORT || 3000));
70
+ log.info("━".repeat(60));
71
+ })
72
+ }).catch((err) => {
73
+ log.error("❌ Database connection failed:", err);
74
+ process.exit(1);
75
+ })
@@ -0,0 +1,7 @@
1
+ import BaseResponseError from "@base/BaseResponseError";
2
+
3
+ export default class BadRequestException extends BaseResponseError {
4
+ constructor(message: string = 'Bad request', errors?: Record<string, unknown>[]) {
5
+ super(400, message, errors);
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import BaseResponseError from "@base/BaseResponseError";
2
+
3
+ export default class ForbiddenException extends BaseResponseError {
4
+ constructor(message: string = "You don't have permission to access this resource") {
5
+ super(403, message);
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import BaseResponseError from "@base/BaseResponseError";
2
+
3
+ export default class NotFoundException extends BaseResponseError {
4
+ constructor(message: string = 'Resource not found') {
5
+ super(404, message);
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import BaseResponseError from "@base/BaseResponseError";
2
+
3
+ export default class UnauthorizedException extends BaseResponseError {
4
+ constructor(message: string = 'Authentication required') {
5
+ super(401, message);
6
+ }
7
+ }
@@ -0,0 +1,53 @@
1
+ import BaseRepository from "@base/BaseRepository";
2
+ import {IUser, IUserCreate, IUserProfileCreate} from "@shared/interfaces/UserInterface";
3
+ import UserEntity from "@database/entity/UserModel";
4
+ import ProfileEntity from "@database/entity/ProfileModel";
5
+ import {dbCreateSeason} from "@shared/utils/DatabaseUtil";
6
+
7
+ export default class UserRepository extends BaseRepository<IUser>{
8
+ constructor() {
9
+ super(UserEntity);
10
+ }
11
+
12
+ async createWithProfile(
13
+ userData: IUserCreate,
14
+ profileData: IUserProfileCreate,
15
+ idUser?: ObjectId
16
+ ) {
17
+ const session = await dbCreateSeason();
18
+ session.startTransaction();
19
+ try {
20
+ // Create user
21
+ const user = new this.model({
22
+ ...userData,
23
+ createdBy: idUser,
24
+ modifiedBy: idUser
25
+ })
26
+ await user.save({ session });
27
+
28
+ // Create profile linked to the user
29
+ const profile = new ProfileEntity({
30
+ ...profileData,
31
+ idUser,
32
+ createdBy: idUser,
33
+ modifiedBy: idUser
34
+ })
35
+ await profile.save({ session });
36
+
37
+ // Link profile to user
38
+ user.idProfile = profile._id;
39
+ await user.save({ session });
40
+
41
+ // Commit transaction
42
+ await session.commitTransaction();
43
+
44
+ // Return the created user with profile populated
45
+ return user;
46
+ } catch (error) {
47
+ await session.abortTransaction();
48
+ throw new Error("Internal Server Error");
49
+ } finally {
50
+ await session.endSession();
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,11 @@
1
+ import mongoose from "mongoose";
2
+ import {log} from "@config/LoggerConfig";
3
+
4
+ export default async () => {
5
+ log.info("━".repeat(60));
6
+ log.info("🔄️Connecting to MongoDB");
7
+ await mongoose.connect(process.env.MONGO_DB_URL || "http://localhost:27017/local",{
8
+ user: process.env.MONGO_DB_USER,
9
+ pass: process.env.MONGO_DB_PASSWORD
10
+ });
11
+ }
@@ -0,0 +1,177 @@
1
+ import SearchCriteria from "@database/builder/SearchCriteria";
2
+ import MultipleSearchCriteria from "@database/builder/MultipleSearchCriteria";
3
+ import MetaDataConfig from "@config/MetaDataConfig";
4
+ import LookupConfig from "@config/LookupConfig";
5
+
6
+
7
+ export default class CustomBuilder<T> {
8
+ private criteria: Array<SearchCriteria | MultipleSearchCriteria> = [];
9
+ private requiredLookups: Set<string> = new Set();
10
+ private entityClass?: any;
11
+
12
+ /**
13
+ * @param entityClass - Optional Entity class to auto-include all @Lookup decorators
14
+ */
15
+ constructor(entityClass?: any) {
16
+ if (entityClass) {
17
+ this.entityClass = entityClass;
18
+ this.withLookupsFrom(entityClass);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Auto-include all lookups defined in Entity class via @Lookup decorator
24
+ * @param entityClass - The Entity class (e.g., UserEntity)
25
+ */
26
+ withLookupsFrom(entityClass: any): CustomBuilder<T> {
27
+ // Use originalClassName from @Entity decorator, fallback to name
28
+ const className = entityClass.originalClassName || entityClass.name;
29
+ const lookupKeys = MetaDataConfig.lookupGetKeys(className);
30
+ lookupKeys.forEach(key => this.requiredLookups.add(key));
31
+ return this;
32
+ }
33
+
34
+ with(criterion: SearchCriteria | MultipleSearchCriteria): CustomBuilder<T> {
35
+ this.criteria.push(criterion);
36
+
37
+ if (criterion instanceof SearchCriteria && criterion.isLookupOperation()) {
38
+ const lookupInfo = criterion.getLookupInfo();
39
+ if (lookupInfo) {
40
+ this.requiredLookups.add(lookupInfo.prefix);
41
+ }
42
+ } else if (criterion instanceof MultipleSearchCriteria) {
43
+ criterion.criteriaList.forEach(c => {
44
+ if (c.isLookupOperation()) {
45
+ const lookupInfo = c.getLookupInfo();
46
+ if (lookupInfo) {
47
+ this.requiredLookups.add(lookupInfo.prefix);
48
+ }
49
+ }
50
+ });
51
+ }
52
+
53
+ return this;
54
+ }
55
+
56
+ /**
57
+ * Explicitly add a lookup to join related collection
58
+ * @param lookupName - The registered lookup name (e.g., 'profile', 'user')
59
+ */
60
+ withLookup(lookupName: string): CustomBuilder<T> {
61
+ this.requiredLookups.add(lookupName);
62
+ return this;
63
+ }
64
+
65
+ build(): any {
66
+ if (this.criteria.length === 0) {
67
+ return {};
68
+ }
69
+
70
+ if (this.criteria.length === 1) {
71
+ return this.criteria[0].toMongoQuery();
72
+ }
73
+
74
+ const queries = this.criteria.map(c => c.toMongoQuery());
75
+ return { $and: queries };
76
+ }
77
+
78
+ buildAggregation(): any[] {
79
+ const pipeline: any[] = [];
80
+
81
+ this.requiredLookups.forEach(prefix => {
82
+ const config = LookupConfig.get(prefix);
83
+ if (config) {
84
+ pipeline.push({
85
+ $lookup: {
86
+ from: config.from,
87
+ localField: config.localField,
88
+ foreignField: config.foreignField,
89
+ as: config.as
90
+ }
91
+ });
92
+
93
+ if (config.unwind) {
94
+ pipeline.push({
95
+ $unwind: {
96
+ path: `$${config.as}`,
97
+ preserveNullAndEmptyArrays: true
98
+ }
99
+ });
100
+ }
101
+
102
+ // Auto-apply virtual fields for the looked-up entity
103
+ this.applyVirtualFields(pipeline, prefix, config.from);
104
+ }
105
+ });
106
+
107
+ if (this.criteria.length > 0) {
108
+ pipeline.push({
109
+ $match: this.build()
110
+ });
111
+ }
112
+
113
+ return pipeline;
114
+ }
115
+
116
+ /**
117
+ * Apply virtual/computed fields from @Virtual decorators to aggregation pipeline
118
+ */
119
+ private applyVirtualFields(pipeline: any[], lookupPrefix: string, collectionName: string): void {
120
+ // Map collection names to entity class names
121
+ const collectionToEntity: Record<string, string> = {
122
+ 'profiles': 'ProfileEntity',
123
+ 'users': 'UserEntity'
124
+ };
125
+
126
+ const entityName = collectionToEntity[collectionName];
127
+ if (!entityName) return;
128
+
129
+ const virtualFields = MetaDataConfig.virtualGetFields(entityName);
130
+ if (virtualFields.length === 0) return;
131
+
132
+ const addFieldsStage: any = { $addFields: {} };
133
+
134
+ virtualFields.forEach(virtual => {
135
+ const fieldPath = `${lookupPrefix}.${virtual.field}`;
136
+ // Replace field references in expression to include lookup prefix
137
+ addFieldsStage.$addFields[fieldPath] = this.prefixFieldPaths(virtual.aggregationExpression, lookupPrefix);
138
+ });
139
+
140
+ pipeline.push(addFieldsStage);
141
+ }
142
+
143
+ /**
144
+ * Prefix field paths in aggregation expression with lookup prefix
145
+ */
146
+ private prefixFieldPaths(expression: any, prefix: string): any {
147
+ if (typeof expression === 'string' && expression.startsWith('$')) {
148
+ // Replace $fieldName with $prefix.fieldName
149
+ const fieldName = expression.substring(1);
150
+ return `$${prefix}.${fieldName}`;
151
+ }
152
+
153
+ if (Array.isArray(expression)) {
154
+ return expression.map(item => this.prefixFieldPaths(item, prefix));
155
+ }
156
+
157
+ if (typeof expression === 'object' && expression !== null) {
158
+ const result: any = {};
159
+ for (const [key, value] of Object.entries(expression)) {
160
+ result[key] = this.prefixFieldPaths(value, prefix);
161
+ }
162
+ return result;
163
+ }
164
+
165
+ return expression;
166
+ }
167
+
168
+ hasLookups(): boolean {
169
+ return this.requiredLookups.size > 0;
170
+ }
171
+
172
+ clear(): CustomBuilder<T> {
173
+ this.criteria = [];
174
+ this.requiredLookups.clear();
175
+ return this;
176
+ }
177
+ }
@@ -0,0 +1,12 @@
1
+ export default class CustomFilter {
2
+ static readonly OPERATION_EQUAL = 'eq';
3
+ static readonly OPERATION_NOT_EQUAL = 'ne';
4
+ static readonly OPERATION_LIKE = 'like';
5
+ static readonly OPERATION_GREATER_THAN = 'gt';
6
+ static readonly OPERATION_LESS_THAN = 'lt';
7
+ static readonly OPERATION_GREATER_THAN_EQUAL = 'gte';
8
+ static readonly OPERATION_LESS_THAN_EQUAL = 'lte';
9
+ static readonly OPERATION_IN = 'in';
10
+ static readonly OPERATION_LOOKUP_EQUAL = 'lookup_eq';
11
+ static readonly OPERATION_LOOKUP_LIKE = 'lookup_like';
12
+ }
@@ -0,0 +1,25 @@
1
+ import SearchCriteria from "@database/builder/SearchCriteria";
2
+
3
+ export default class MultipleSearchCriteria {
4
+ constructor(
5
+ public logicalOperation: string,
6
+ public criteriaList: SearchCriteria[]
7
+ ) {}
8
+
9
+ static of(
10
+ logicalOperation: string,
11
+ ...criteriaList: SearchCriteria[]
12
+ ): MultipleSearchCriteria {
13
+ return new MultipleSearchCriteria(logicalOperation, criteriaList);
14
+ }
15
+
16
+ toMongoQuery(): any {
17
+ const queries = this.criteriaList.map(c => c.toMongoQuery());
18
+
19
+ if (this.logicalOperation === SearchCriteria.OPERATION_OR) {
20
+ return { $or: queries };
21
+ } else {
22
+ return { $and: queries };
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,79 @@
1
+ import CustomFilter from "@database/builder/CustomFilter";
2
+ import LookupConfig from "@config/LookupConfig";
3
+
4
+ export default class SearchCriteria {
5
+ static readonly OPERATION_AND = 'AND';
6
+ static readonly OPERATION_OR = 'OR';
7
+
8
+ constructor(
9
+ public key: string,
10
+ public operation: string,
11
+ public value: any
12
+ ) {}
13
+
14
+ static of(key: string, operation: string, value: any): SearchCriteria {
15
+ return new SearchCriteria(key, operation, value);
16
+ }
17
+
18
+ isLookupOperation(): boolean {
19
+ return this.operation === CustomFilter.OPERATION_LOOKUP_EQUAL ||
20
+ this.operation === CustomFilter.OPERATION_LOOKUP_LIKE;
21
+ }
22
+
23
+ getLookupInfo(): { prefix: string; config: LookupConfig; field: string } | null {
24
+ const lookupInfo = LookupConfig.getLookupForPath(this.key);
25
+ if (!lookupInfo) return null;
26
+
27
+ const { prefix, config } = lookupInfo;
28
+ const field = this.key.substring(prefix.length + 1);
29
+
30
+ return { prefix, config, field };
31
+ }
32
+
33
+ toMongoQuery(): any {
34
+ const operation = this.operation;
35
+
36
+ if (!this.isLookupOperation()) {
37
+ const fieldPath = this.key;
38
+
39
+ switch (operation) {
40
+ case CustomFilter.OPERATION_EQUAL:
41
+ return { [fieldPath]: this.value };
42
+ case CustomFilter.OPERATION_NOT_EQUAL:
43
+ return { [fieldPath]: { $ne: this.value } };
44
+ case CustomFilter.OPERATION_LIKE:
45
+ return { [fieldPath]: { $regex: this.value, $options: 'i' } };
46
+ case CustomFilter.OPERATION_GREATER_THAN:
47
+ return { [fieldPath]: { $gt: this.value } };
48
+ case CustomFilter.OPERATION_LESS_THAN:
49
+ return { [fieldPath]: { $lt: this.value } };
50
+ case CustomFilter.OPERATION_GREATER_THAN_EQUAL:
51
+ return { [fieldPath]: { $gte: this.value } };
52
+ case CustomFilter.OPERATION_LESS_THAN_EQUAL:
53
+ return { [fieldPath]: { $lte: this.value } };
54
+ case CustomFilter.OPERATION_IN:
55
+ return { [fieldPath]: { $in: this.value } };
56
+ default:
57
+ return { [fieldPath]: this.value };
58
+ }
59
+ }
60
+
61
+ const lookupInfo = this.getLookupInfo();
62
+ if (!lookupInfo) {
63
+ throw new Error(`Lookup configuration not found for path: ${this.key}`);
64
+ }
65
+
66
+ const { prefix, field } = lookupInfo;
67
+ const matchField = `${prefix}.${field}`;
68
+
69
+ if (operation === CustomFilter.OPERATION_LOOKUP_EQUAL) {
70
+ return { [matchField]: this.value };
71
+ }
72
+
73
+ if (operation === CustomFilter.OPERATION_LOOKUP_LIKE) {
74
+ return { [matchField]: { $regex: this.value, $options: 'i' } };
75
+ }
76
+
77
+ return {};
78
+ }
79
+ }
@@ -0,0 +1,54 @@
1
+ import {ComponentLookup, Lookup, Virtual} from "@shared/decorators/LookupDecorator";
2
+ import {IUser, IUserProfile} from "@shared/interfaces/UserInterface";
3
+ import {AgeVirtual, FullNameVirtual} from "@shared/helpers/VirtualHelper";
4
+ import mongoose, {Schema} from "mongoose";
5
+ import BaseSchema from "@base/BaseSchema";
6
+
7
+ @ComponentLookup("profiles")
8
+ export class ProfileLookup {
9
+ @Lookup({
10
+ from: 'users',
11
+ localField: 'idUser',
12
+ foreignField: '_id',
13
+ as: 'user',
14
+ unwind: true
15
+ })
16
+ user?: IUser;
17
+
18
+ @Virtual(
19
+ FullNameVirtual("fullName", "firstName", "lastName")
20
+ )
21
+ fullName?: string;
22
+
23
+ @Virtual(
24
+ AgeVirtual("age", "dateOfBirth")
25
+ )
26
+ age?: number;
27
+ }
28
+
29
+ const ProfileSchema = BaseSchema.create<IUserProfile>({
30
+ userId: {
31
+ type: Schema.Types.ObjectId,
32
+ ref: 'users',
33
+ required: true,
34
+ unique: true
35
+ },
36
+ firstName: {
37
+ type: String,
38
+ required: true },
39
+ lastName: {
40
+ type: String,
41
+ required: true
42
+ },
43
+ phone: {
44
+ type: String
45
+ },
46
+ address: {
47
+ type: String
48
+ },
49
+ dateOfBirth: {
50
+ type: Number
51
+ } // Epoch timestamp
52
+ });
53
+ BaseSchema.addSoftDelete(ProfileSchema);
54
+ export default mongoose.model<IUserProfile>("profile", ProfileSchema);