From 44793fbfbbfb3482b5acbbedae3f11e31e085957 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 13:44:03 +0700 Subject: [PATCH] api web service add join for show name --- src/controllers/ApiManageController.ts | 75 ++++++- src/controllers/ApiWebServiceController.ts | 219 +++++++++++++++++++-- src/services/KeycloakAttributeService.ts | 27 ++- 3 files changed, 290 insertions(+), 31 deletions(-) diff --git a/src/controllers/ApiManageController.ts b/src/controllers/ApiManageController.ts index 22db1c7d..34f0c824 100644 --- a/src/controllers/ApiManageController.ts +++ b/src/controllers/ApiManageController.ts @@ -346,6 +346,10 @@ export class ApiManageController extends Controller { "ancestorDNA", "keycloak", "commandId", + "prefixMain", + "authRoleId", + "next_holderId", + "current_holderId", ]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity @@ -411,6 +415,34 @@ export class ApiManageController extends Controller { }, }; + // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Position entity + private readonly POSITION_FIELD_REPLACEMENTS: Record< + string, + { propertyName: string; type: string; comment: string; joinTable: string; joinField: string } + > = { + posTypeId: { + propertyName: "posTypeName", + type: "string", + comment: "ประเภทตำแหน่ง", + joinTable: "PosType", + joinField: "posTypeName", + }, + posLevelId: { + propertyName: "posLevelName", + type: "string", + comment: "ระดับตำแหน่ง", + joinTable: "PosLevel", + joinField: "posLevelName", + }, + posExecutiveId: { + propertyName: "posExecutiveName", + type: "string", + comment: "ตำแหน่งทางการบริหาร", + joinTable: "PosExecutive", + joinField: "posExecutiveName", + }, + }; + private validateSuperAdminRole(user: any): void { if (!user.role.includes("SUPER_ADMIN")) { throw new HttpError(HttpStatusCode.FORBIDDEN, "คุณไม่มีสิทธิ์ในการเข้าถึงข้อมูลนี้"); @@ -466,8 +498,8 @@ export class ApiManageController extends Controller { const replacementKeys = Object.keys(this.PROFILE_FIELD_REPLACEMENTS); // Remove ID fields that should be replaced - columns = columns.filter((col: { propertyName: string }) => - !replacementKeys.includes(col.propertyName), + columns = columns.filter( + (col: { propertyName: string }) => !replacementKeys.includes(col.propertyName), ); // Add the corresponding name fields @@ -481,6 +513,45 @@ export class ApiManageController extends Controller { columns = [...columns, ...nameFields]; } + // Special handling for Position entity - replace ID fields with name fields + if (name === "Position") { + const replacementKeys = Object.keys(this.POSITION_FIELD_REPLACEMENTS); + + // Remove ID fields that should be replaced + columns = columns.filter( + (col: { propertyName: string }) => !replacementKeys.includes(col.propertyName), + ); + + // Add the corresponding name fields + const nameFields = replacementKeys.map((key) => ({ + propertyName: this.POSITION_FIELD_REPLACEMENTS[key].propertyName, + type: "string", + comment: this.POSITION_FIELD_REPLACEMENTS[key].comment, + key: this.POSITION_FIELD_REPLACEMENTS[key].propertyName, + })); + + columns = [...columns, ...nameFields]; + } + + // Special handling for PosMaster entity - add Profile fields for holder information + if (name === "PosMaster") { + // Add Profile fields that are accessible via current_holder relation + const profileFields = ["prefix", "rank", "firstName", "lastName", "citizenId"]; + const profileRepository = AppDataSource.getRepository(Profile); + const profileColumns = profileRepository.metadata.columns + .filter( + (column: any) => !column.isPrimary && profileFields.includes(column.propertyName), + ) + .map((column: any) => ({ + propertyName: `Profile.${column.propertyName}`, + type: typeof column.type === "string" ? column.type : "string", + comment: column.comment, + key: `Profile.${column.propertyName}`, + })); + + columns = [...columns, ...profileColumns]; + } + return { tb: name, description, diff --git a/src/controllers/ApiWebServiceController.ts b/src/controllers/ApiWebServiceController.ts index 7c09fe95..49e9287d 100644 --- a/src/controllers/ApiWebServiceController.ts +++ b/src/controllers/ApiWebServiceController.ts @@ -69,6 +69,28 @@ export class ApiWebServiceController extends Controller { }, }; + // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Position entity + private readonly POSITION_FIELD_REPLACEMENTS: Record< + string, + { propertyName: string; joinRelation: string; joinField: string } + > = { + posTypeName: { + propertyName: "posTypeId", + joinRelation: "posType", + joinField: "posTypeName", + }, + posLevelName: { + propertyName: "posLevelId", + joinRelation: "posLevel", + joinField: "posLevelName", + }, + posExecutiveName: { + propertyName: "posExecutiveId", + joinRelation: "posExecutive", + joinField: "posExecutiveName", + }, + }; + /** * build posMaster permission condition * @summary สร้างเงื่อนไขการกรองข้อมูลตามสิทธิ์การเข้าถึง @@ -82,6 +104,7 @@ export class ApiWebServiceController extends Controller { dnaChild3Id?: string | null; dnaChild4Id?: string | null; }, + tableAlias: string = "posMaster", ): string { // ALL - no filtering if (accessType === "ALL") { @@ -94,49 +117,49 @@ export class ApiWebServiceController extends Controller { if (accessType === "ROOT" && dnaIds.dnaRootId) { // All organizations under this root conditions.push( - `posMaster.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%")`, + `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%")`, ); } else if (accessType === "CHILD" || accessType === "NORMAL") { // Build conditions based on which DNA level is specified if (dnaIds.dnaChild4Id) { conditions.push( - `posMaster.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`, + `${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`, ); } else if (dnaIds.dnaChild3Id) { conditions.push( - `posMaster.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`, + `${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`, ); // For CHILD type, include all descendants if (accessType === "CHILD") { conditions.push( - `(posMaster.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%") OR posMaster.orgChild4Id IS NOT NULL)`, + `(${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%") OR ${tableAlias}.orgChild4Id IS NOT NULL)`, ); } } else if (dnaIds.dnaChild2Id) { conditions.push( - `posMaster.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, + `${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, ); if (accessType === "CHILD") { conditions.push( - `(posMaster.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR posMaster.orgChild3Id IS NOT NULL)`, + `(${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR ${tableAlias}.orgChild3Id IS NOT NULL)`, ); } } else if (dnaIds.dnaChild1Id) { conditions.push( - `posMaster.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, + `${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, ); if (accessType === "CHILD") { conditions.push( - `(posMaster.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR posMaster.orgChild2Id IS NOT NULL)`, + `(${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR ${tableAlias}.orgChild2Id IS NOT NULL)`, ); } } else if (dnaIds.dnaRootId) { conditions.push( - `posMaster.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, + `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, ); if (accessType === "CHILD") { conditions.push( - `(posMaster.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR posMaster.orgChild1Id IS NOT NULL)`, + `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild1Id IS NOT NULL)`, ); } } @@ -203,8 +226,9 @@ export class ApiWebServiceController extends Controller { } let posMasterCondition: string = ""; + let posMasterAlias: string = ""; - // Special handling for Profile system with permission filtering + // Special handling for Profile and ProfileEmployee systems with permission filtering if (system == "registry") { // Get current revision const revision = await this.orgRevisionRepository.findOne({ @@ -214,15 +238,89 @@ export class ApiWebServiceController extends Controller { // Store for use in permission building this.currentRevisionId = revision?.id || ""; + posMasterAlias = "posMaster"; // Build permission condition - posMasterCondition = this.buildPosMasterPermissionCondition(request.user.accessType, { - dnaRootId: request.user.dnaRootId, - dnaChild1Id: request.user.dnaChild1Id, - dnaChild2Id: request.user.dnaChild2Id, - dnaChild3Id: request.user.dnaChild3Id, - dnaChild4Id: request.user.dnaChild4Id, + posMasterCondition = this.buildPosMasterPermissionCondition( + request.user.accessType, + { + dnaRootId: request.user.dnaRootId, + dnaChild1Id: request.user.dnaChild1Id, + dnaChild2Id: request.user.dnaChild2Id, + dnaChild3Id: request.user.dnaChild3Id, + dnaChild4Id: request.user.dnaChild4Id, + }, + posMasterAlias, + ); + } else if (system == "registry_emp") { + // Get current revision + const revision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); + + // Store for use in permission building + this.currentRevisionId = revision?.id || ""; + posMasterAlias = "employeePosMaster"; + + // Build permission condition + posMasterCondition = this.buildPosMasterPermissionCondition( + request.user.accessType, + { + dnaRootId: request.user.dnaRootId, + dnaChild1Id: request.user.dnaChild1Id, + dnaChild2Id: request.user.dnaChild2Id, + dnaChild3Id: request.user.dnaChild3Id, + dnaChild4Id: request.user.dnaChild4Id, + }, + posMasterAlias, + ); + } else if (system == "registry_temp") { + // Get current revision + const revision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + + // Store for use in permission building + this.currentRevisionId = revision?.id || ""; + posMasterAlias = "employeeTempPosMaster"; + + // Build permission condition + posMasterCondition = this.buildPosMasterPermissionCondition( + request.user.accessType, + { + dnaRootId: request.user.dnaRootId, + dnaChild1Id: request.user.dnaChild1Id, + dnaChild2Id: request.user.dnaChild2Id, + dnaChild3Id: request.user.dnaChild3Id, + dnaChild4Id: request.user.dnaChild4Id, + }, + posMasterAlias, + ); + } else if (system == "position") { + // Get current revision + const revision = await this.orgRevisionRepository.findOne({ + select: ["id"], + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + + // Store for use in permission building + this.currentRevisionId = revision?.id || ""; + posMasterAlias = "PosMaster"; // Note: Uses PascalCase to match tbMain alias + + // Build permission condition + posMasterCondition = this.buildPosMasterPermissionCondition( + request.user.accessType, + { + dnaRootId: request.user.dnaRootId, + dnaChild1Id: request.user.dnaChild1Id, + dnaChild2Id: request.user.dnaChild2Id, + dnaChild3Id: request.user.dnaChild3Id, + dnaChild4Id: request.user.dnaChild4Id, + }, + posMasterAlias, + ); } const repo = AppDataSource.getRepository(tbMain); @@ -256,6 +354,23 @@ export class ApiWebServiceController extends Controller { }); } + // สำหรับ Position: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey + const positionFieldJoins: Record = {}; // alias -> relationName + if (tbMain === "Position") { + propertyKey = propertyKey.map((key) => { + const [table, field] = key.split("."); + if (table === "Position") { + const replacement = this.POSITION_FIELD_REPLACEMENTS[field]; + if (replacement) { + const alias = `${table}_${replacement.joinRelation}`; + positionFieldJoins[alias] = replacement.joinRelation; + return `${alias}.${replacement.joinField}`; + } + } + return key; + }); + } + const queryBuilder = repo.createQueryBuilder(tbMain); // join กับตารารอง @@ -278,9 +393,40 @@ export class ApiWebServiceController extends Controller { }); } - // join กับ posMaster สำหรับ Profile เพื่อกรองตามสิทธิ์การเข้าถึง - if (tbMain === "Profile" && posMasterCondition !== "1=1") { - queryBuilder.leftJoin("Profile.current_holders", "posMaster"); + // join สำหรับฟิลด์ Position ที่ต้องการดึงค่าจากตารางอื่น + if (tbMain === "Position" && Object.keys(positionFieldJoins).length > 0) { + Object.entries(positionFieldJoins).forEach(([alias, relationName]) => { + queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); + }); + } + + // join สำหรับ PosMaster เมื่อต้องการดึงค่าจาก Profile (ข้อมูลคนครอง) + const posMasterProfileFields: string[] = []; + if (tbMain === "PosMaster") { + propertyKey.forEach((key) => { + if (key.startsWith("Profile.")) { + posMasterProfileFields.push(key); + } + }); + } + + // join PosMaster กับ Profile เมื่อมีการขอ Profile fields + if (tbMain === "PosMaster" && posMasterProfileFields.length > 0) { + queryBuilder.leftJoin("PosMaster.current_holder", "Profile"); + } + + // join กับ posMaster/employeePosMaster/employeeTempPosMaster เพื่อกรองตามสิทธิ์การเข้าถึง + if ((tbMain === "Profile" || tbMain === "ProfileEmployee") && posMasterCondition !== "1=1") { + if (tbMain === "Profile") { + queryBuilder.leftJoin("Profile.current_holders", "posMaster"); + } else if (tbMain === "ProfileEmployee") { + // Use the correct relation based on posMasterAlias + if (posMasterAlias === "employeeTempPosMaster") { + queryBuilder.leftJoin("ProfileEmployee.current_holderTemps", "employeeTempPosMaster"); + } else { + queryBuilder.leftJoin("ProfileEmployee.current_holders", "employeePosMaster"); + } + } } // // เพิ่ม Main.id เพราะจะใช้ pk ในการแมบและนับจำนวน @@ -333,6 +479,39 @@ export class ApiWebServiceController extends Controller { return flattened; } + // สำหรับ Position: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม + if (tbMain === "Position") { + const flattened: any = { ...rest }; + Object.entries(this.POSITION_FIELD_REPLACEMENTS).forEach(([nameField, config]) => { + // Remove the original ID field + delete flattened[config.propertyName]; + // Add the name field from joined table + const alias = `${tbMain}_${config.joinRelation}`; + if (rest[alias] && rest[alias][config.joinField] !== undefined) { + flattened[nameField] = rest[alias][config.joinField]; + } + // Remove the joined table object + delete flattened[alias]; + }); + return flattened; + } + + // สำหรับ PosMaster: แปลงฟิลด์ Profile ที่มาจาก join กลับเป็นฟิลด์ระดับบน + if (tbMain === "PosMaster" && posMasterProfileFields.length > 0) { + const flattened: any = { ...rest }; + // Extract Profile fields and add them at top level with "profile_" prefix to avoid conflicts + if (rest["Profile"]) { + flattened["profile_prefix"] = rest["Profile"].prefix; + flattened["profile_rank"] = rest["Profile"].rank; + flattened["profile_firstName"] = rest["Profile"].firstName; + flattened["profile_lastName"] = rest["Profile"].lastName; + flattened["profile_citizenId"] = rest["Profile"].citizenId; + // Remove the nested Profile object + delete flattened["Profile"]; + } + return flattened; + } + return rest; }); diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 1e0f3f07..5206183b 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -530,18 +530,20 @@ export class KeycloakAttributeService { // Initialize rate limiter if rate limiting is enabled if (rateLimit && rateLimit > 0) { rateLimiter = new RateLimiter(rateLimit); - console.log(`[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`); + console.log( + `[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`, + ); } // Select repository based on profile type - const repo = - profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo; + const repo = profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo; // Query profiles updated within the month const profiles = await repo .createQueryBuilder("p") .where("p.keycloak IS NOT NULL") .andWhere("p.keycloak != :empty", { empty: "" }) + .andWhere({ "p.isDeleted": false }) .andWhere("p.lastUpdatedAt BETWEEN :start AND :end", { start: startDate, end: endDate, @@ -579,8 +581,7 @@ export class KeycloakAttributeService { try { // Check if empType is empty in Keycloak - const { isEmpty, currentEmpType } = - await this.checkEmpTypeEmpty(keycloakUserId); + const { isEmpty, currentEmpType } = await this.checkEmpTypeEmpty(keycloakUserId); result.profilesChecked++; @@ -607,8 +608,7 @@ export class KeycloakAttributeService { // Sync the profile const success = await withRetry( - async () => - this.syncOnOrganizationChange(profile.id, profileType), + async () => this.syncOnOrganizationChange(profile.id, profileType), 3, // maxRetries 1000, // baseDelay ); @@ -768,7 +768,13 @@ export class KeycloakAttributeService { maxRetries?: number; // Retry attempts for failed operations rateLimit?: number; // Requests per second clearProgress?: boolean; // Start fresh, ignore existing progress - }): Promise<{ total: number; success: number; failed: number; details: any[]; resumed?: boolean }> { + }): Promise<{ + total: number; + success: number; + failed: number; + details: any[]; + resumed?: boolean; + }> { const limit = options?.limit; const concurrency = options?.concurrency ?? 5; const resume = options?.resume ?? false; @@ -922,7 +928,10 @@ export class KeycloakAttributeService { // Save progress after each batch SyncProgressManager.save(updatedState); // Log progress every 50 items - if (updatedState.lastSyncedIndex % 50 === 0 || updatedState.lastSyncedIndex === updatedState.totalProfiles) { + if ( + updatedState.lastSyncedIndex % 50 === 0 || + updatedState.lastSyncedIndex === updatedState.totalProfiles + ) { SyncProgressManager.logProgress(updatedState); } },