From 9fe91ce49c6de5a5e80e2f3b00b2ec56e90bd5fd Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 10:53:45 +0700 Subject: [PATCH 1/5] =?UTF-8?q?=E0=B8=95=E0=B8=B1=E0=B8=94=E0=B8=9F?= =?UTF-8?q?=E0=B8=B4=E0=B8=A5=E0=B8=94=E0=B9=8C=E0=B8=8B=E0=B9=89=E0=B8=B3?= =?UTF-8?q?=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=9F=E0=B8=B4=E0=B8=A5=E0=B8=94?= =?UTF-8?q?=E0=B9=8C=20id=20=E0=B8=AD=E0=B8=AD=E0=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ApiManageController.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/controllers/ApiManageController.ts b/src/controllers/ApiManageController.ts index 01a27eb5..8caad7fd 100644 --- a/src/controllers/ApiManageController.ts +++ b/src/controllers/ApiManageController.ts @@ -325,7 +325,28 @@ export class ApiManageController extends Controller { ]; private readonly DEFAULT_PAGE_SIZE = 10; // ขนาดหน้าเริ่มต้น - private readonly EXCLUDED_COLUMNS = ["createdUserId", "lastUpdateUserId"]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ + private readonly EXCLUDED_COLUMNS = [ + "createdUserId", + "lastUpdateUserId", + "createdAt", + "createdFullName", + "lastUpdateFullName", + "avatarName", + "profileId", + "prefixId", + "profileEmployeeId", + "documentId", + "orgRevisionId", + "posMasterId", + "orgRootId", + "orgChild1Id", + "orgChild2Id", + "orgChild3Id", + "orgChild4Id", + "ancestorDNA", + "keycloak", + "commandId", + ]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ private validateSuperAdminRole(user: any): void { if (!user.role.includes("SUPER_ADMIN")) { From b2d59ef6980779678845da34e43315ffe18105b5 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 11:07:15 +0700 Subject: [PATCH 2/5] =?UTF-8?q?=E0=B9=81=E0=B8=81=E0=B9=89=E0=B9=84?= =?UTF-8?q?=E0=B8=82=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B9=80=E0=B8=A0=E0=B8=97?= =?UTF-8?q?=20=E0=B8=A3=E0=B8=B0=E0=B8=94=E0=B8=B1=E0=B8=9A=E0=B8=95?= =?UTF-8?q?=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87=20?= =?UTF-8?q?=E0=B9=81=E0=B8=A5=E0=B8=B0=E0=B8=88=E0=B8=B1=E0=B8=87=E0=B8=AB?= =?UTF-8?q?=E0=B8=A7=E0=B8=B1=E0=B8=94=20=E0=B8=AD=E0=B8=B3=E0=B9=80?= =?UTF-8?q?=E0=B8=A0=E0=B8=AD=20=E0=B8=95=E0=B8=B3=E0=B8=9A=E0=B8=A5?= =?UTF-8?q?=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87?= =?UTF-8?q?=E0=B8=9F=E0=B8=B4=E0=B8=A5=E0=B8=94=E0=B9=8C=E0=B8=97=E0=B8=B5?= =?UTF-8?q?=E0=B9=88=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B8=8A=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B9=80=E0=B8=A5?= =?UTF-8?q?=E0=B8=B7=E0=B8=AD=E0=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ApiManageController.ts | 101 +++++++++++++++++++-- src/controllers/ApiWebServiceController.ts | 91 ++++++++++++++++++- 2 files changed, 182 insertions(+), 10 deletions(-) diff --git a/src/controllers/ApiManageController.ts b/src/controllers/ApiManageController.ts index 8caad7fd..22db1c7d 100644 --- a/src/controllers/ApiManageController.ts +++ b/src/controllers/ApiManageController.ts @@ -348,6 +348,69 @@ export class ApiManageController extends Controller { "commandId", ]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ + // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity + private readonly PROFILE_FIELD_REPLACEMENTS: Record< + string, + { propertyName: string; type: string; comment: string; joinTable: string; joinField: string } + > = { + posLevelId: { + propertyName: "posLevelName", + type: "string", + comment: "ระดับตำแหน่ง", + joinTable: "PosLevel", + joinField: "posLevelName", + }, + posTypeId: { + propertyName: "posTypeName", + type: "string", + comment: "ประเภทตำแหน่ง", + joinTable: "PosType", + joinField: "posTypeName", + }, + registrationProvinceId: { + propertyName: "registrationProvinceName", + type: "string", + comment: "จังหวัดตามทะเบียนบ้าน", + joinTable: "Province", + joinField: "name", + }, + registrationDistrictId: { + propertyName: "registrationDistrictName", + type: "string", + comment: "เขตตามทะเบียนบ้าน", + joinTable: "District", + joinField: "name", + }, + registrationSubDistrictId: { + propertyName: "registrationSubDistrictName", + type: "string", + comment: "แขวงตามทะเบียนบ้าน", + joinTable: "SubDistrict", + joinField: "name", + }, + currentProvinceId: { + propertyName: "currentProvinceName", + type: "string", + comment: "จังหวัดตามปัจจุบัน", + joinTable: "Province", + joinField: "name", + }, + currentDistrictId: { + propertyName: "currentDistrictName", + type: "string", + comment: "เขตตามปัจจุบัน", + joinTable: "District", + joinField: "name", + }, + currentSubDistrictId: { + propertyName: "currentSubDistrictName", + type: "string", + comment: "แขวงตามปัจจุบัน", + joinTable: "SubDistrict", + joinField: "name", + }, + }; + private validateSuperAdminRole(user: any): void { if (!user.role.includes("SUPER_ADMIN")) { throw new HttpError(HttpStatusCode.FORBIDDEN, "คุณไม่มีสิทธิ์ในการเข้าถึงข้อมูลนี้"); @@ -385,11 +448,8 @@ export class ApiManageController extends Controller { const result = this.entities .filter((entity) => entity.system.includes(system)) - .map(({ name, repository, description, isMain }) => ({ - tb: name, - description, - isMain: isMain || false, - propertys: repository.metadata.columns + .map(({ name, repository, description, isMain }) => { + let columns = repository.metadata.columns .filter( (column: any) => !column.isPrimary && !this.EXCLUDED_COLUMNS.includes(column.propertyName), @@ -399,8 +459,35 @@ export class ApiManageController extends Controller { type: typeof column.type === "string" ? column.type : "string", comment: column.comment, key: column.propertyName, - })), - })); + })); + + // Special handling for Profile entity - replace ID fields with name fields + if (name === "Profile") { + 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), + ); + + // Add the corresponding name fields + const nameFields = replacementKeys.map((key) => ({ + propertyName: this.PROFILE_FIELD_REPLACEMENTS[key].propertyName, + type: "string", + comment: this.PROFILE_FIELD_REPLACEMENTS[key].comment, + key: this.PROFILE_FIELD_REPLACEMENTS[key].propertyName, + })); + + columns = [...columns, ...nameFields]; + } + + return { + tb: name, + description, + isMain: isMain || false, + propertys: columns, + }; + }); return new HttpSuccess(result); } catch (error) { diff --git a/src/controllers/ApiWebServiceController.ts b/src/controllers/ApiWebServiceController.ts index 61f3d54a..8a1e87be 100644 --- a/src/controllers/ApiWebServiceController.ts +++ b/src/controllers/ApiWebServiceController.ts @@ -21,6 +21,53 @@ export class ApiWebServiceController extends Controller { private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private apiHistoryRepository = AppDataSource.getRepository(ApiHistory); + // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity + private readonly PROFILE_FIELD_REPLACEMENTS: Record< + string, + { propertyName: string; joinRelation: string; joinField: string } + > = { + posLevelName: { + propertyName: "posLevelId", + joinRelation: "posLevel", + joinField: "posLevelName", + }, + posTypeName: { + propertyName: "posTypeId", + joinRelation: "posType", + joinField: "posTypeName", + }, + registrationProvinceName: { + propertyName: "registrationProvinceId", + joinRelation: "registrationProvince", + joinField: "name", + }, + registrationDistrictName: { + propertyName: "registrationDistrictId", + joinRelation: "registrationDistrict", + joinField: "name", + }, + registrationSubDistrictName: { + propertyName: "registrationSubDistrictId", + joinRelation: "registrationSubDistrict", + joinField: "name", + }, + currentProvinceName: { + propertyName: "currentProvinceId", + joinRelation: "currentProvince", + joinField: "name", + }, + currentDistrictName: { + propertyName: "currentDistrictId", + joinRelation: "currentDistrict", + joinField: "name", + }, + currentSubDistrictName: { + propertyName: "currentSubDistrictId", + joinRelation: "currentSubDistrict", + joinField: "name", + }, + }; + /** * list fields by systems * @summary รายการ fields ตาม systems @@ -50,7 +97,7 @@ export class ApiWebServiceController extends Controller { } await isPermissionRequest(request, apiName.id); const offset = (page - 1) * pageSize; - const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`); + let propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`); let tbMain: string = ""; let condition: string = "1=1"; @@ -92,6 +139,23 @@ export class ApiWebServiceController extends Controller { ...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)), ]; + // สำหรับ Profile: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey + const profileFieldJoins: Record = {}; // alias -> relationName + if (tbMain === "Profile") { + propertyKey = propertyKey.map((key) => { + const [table, field] = key.split("."); + if (table === "Profile") { + const replacement = this.PROFILE_FIELD_REPLACEMENTS[field]; + if (replacement) { + const alias = `${table}_${replacement.joinRelation}`; + profileFieldJoins[alias] = replacement.joinRelation; + return `${alias}.${replacement.joinField}`; + } + } + return key; + }); + } + const queryBuilder = repo.createQueryBuilder(tbMain); // join กับตารารอง @@ -107,6 +171,13 @@ export class ApiWebServiceController extends Controller { }); } + // join สำหรับฟิลด์ Profile ที่ต้องการดึงค่าจากตารางอื่น + if (tbMain === "Profile" && Object.keys(profileFieldJoins).length > 0) { + Object.entries(profileFieldJoins).forEach(([alias, relationName]) => { + queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); + }); + } + // // เพิ่ม Main.id เพราะจะใช้ pk ในการแมบและนับจำนวน // if (!propertyKey.includes(`${Main}.id`)) { // propertyKey.push(`${Main}.id`); @@ -141,8 +212,22 @@ export class ApiWebServiceController extends Controller { // split object id ออกก่อน return const data = items.map((item) => { - const { [pk]: removedPk, ...x } = item; - return x; + const { [pk]: removedPk, ...rest } = item; + + // สำหรับ Profile: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม + if (tbMain === "Profile") { + const flattened: any = { ...rest }; + Object.entries(this.PROFILE_FIELD_REPLACEMENTS).forEach(([nameField, config]) => { + const alias = `${tbMain}_${config.joinRelation}`; + if (rest[alias] && rest[alias][config.joinField] !== undefined) { + flattened[nameField] = rest[alias][config.joinField]; + delete flattened[alias]; + } + }); + return flattened; + } + + return rest; }); // console.log("queryBuilder ===> ", queryBuilder.getQuery()); From b071bc2d924977c84d0f10965e14d7b20f989f7f Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 11:44:28 +0700 Subject: [PATCH 3/5] api service add filter by dnaId of Profile --- src/controllers/ApiWebServiceController.ts | 106 +++++++++++++++++++++ src/middlewares/authWebService.ts | 18 +++- src/middlewares/user.ts | 6 ++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/controllers/ApiWebServiceController.ts b/src/controllers/ApiWebServiceController.ts index 8a1e87be..7c09fe95 100644 --- a/src/controllers/ApiWebServiceController.ts +++ b/src/controllers/ApiWebServiceController.ts @@ -20,6 +20,7 @@ export class ApiWebServiceController extends Controller { private apiNameRepository = AppDataSource.getRepository(ApiName); private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); private apiHistoryRepository = AppDataSource.getRepository(ApiHistory); + private currentRevisionId: string = ""; // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity private readonly PROFILE_FIELD_REPLACEMENTS: Record< @@ -68,6 +69,82 @@ export class ApiWebServiceController extends Controller { }, }; + /** + * build posMaster permission condition + * @summary สร้างเงื่อนไขการกรองข้อมูลตามสิทธิ์การเข้าถึง + */ + private buildPosMasterPermissionCondition( + accessType: string | undefined, + dnaIds: { + dnaRootId?: string | null; + dnaChild1Id?: string | null; + dnaChild2Id?: string | null; + dnaChild3Id?: string | null; + dnaChild4Id?: string | null; + }, + ): string { + // ALL - no filtering + if (accessType === "ALL") { + return "1=1"; + } + + // No access type specified but has DNA IDs - default to NORMAL behavior + const conditions: string[] = []; + + 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}%")`, + ); + } 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}")`, + ); + } else if (dnaIds.dnaChild3Id) { + conditions.push( + `posMaster.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)`, + ); + } + } else if (dnaIds.dnaChild2Id) { + conditions.push( + `posMaster.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)`, + ); + } + } else if (dnaIds.dnaChild1Id) { + conditions.push( + `posMaster.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)`, + ); + } + } else if (dnaIds.dnaRootId) { + conditions.push( + `posMaster.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)`, + ); + } + } + } + + return conditions.length > 0 ? `(${conditions.join(" OR ")})` : "1=1"; + } + /** * list fields by systems * @summary รายการ fields ตาม systems @@ -125,6 +202,29 @@ export class ApiWebServiceController extends Controller { condition = `PosMaster.orgRevisionId = "${revision?.id}"`; } + let posMasterCondition: string = ""; + + // Special handling for Profile system with permission filtering + if (system == "registry") { + // 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 || ""; + + // 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, + }); + } + const repo = AppDataSource.getRepository(tbMain); const metadata = repo.metadata; @@ -178,6 +278,11 @@ export class ApiWebServiceController extends Controller { }); } + // join กับ posMaster สำหรับ Profile เพื่อกรองตามสิทธิ์การเข้าถึง + if (tbMain === "Profile" && posMasterCondition !== "1=1") { + queryBuilder.leftJoin("Profile.current_holders", "posMaster"); + } + // // เพิ่ม Main.id เพราะจะใช้ pk ในการแมบและนับจำนวน // if (!propertyKey.includes(`${Main}.id`)) { // propertyKey.push(`${Main}.id`); @@ -196,6 +301,7 @@ export class ApiWebServiceController extends Controller { const [items, total] = await queryBuilder .select(propertyKey) .where(condition) + .andWhere(posMasterCondition) .orderBy(propertyKey[0], "ASC") .skip(offset) .take(pageSize) diff --git a/src/middlewares/authWebService.ts b/src/middlewares/authWebService.ts index fa50b3fe..1f17b9cf 100644 --- a/src/middlewares/authWebService.ts +++ b/src/middlewares/authWebService.ts @@ -17,7 +17,17 @@ export async function handleWebServiceAuth(request: express.Request) { // ตรวจสอบ API Key กับฐานข้อมูล const apiKeyData = await AppDataSource.getRepository(ApiKey).findOne({ - select: { id: true, name: true, keyApi: true }, + select: { + id: true, + name: true, + keyApi: true, + accessType: true, + dnaRootId: true, + dnaChild1Id: true, + dnaChild2Id: true, + dnaChild3Id: true, + dnaChild4Id: true, + }, where: { keyApi: apiKey }, relations: ["apiNames"], }); @@ -40,6 +50,12 @@ export async function handleWebServiceAuth(request: express.Request) { name: apiKeyData.name, type: "web-service", accessApi: apiKeyData.apiNames.map((x) => x.id) ?? [], + accessType: apiKeyData.accessType, + dnaRootId: apiKeyData.dnaRootId, + dnaChild1Id: apiKeyData.dnaChild1Id, + dnaChild2Id: apiKeyData.dnaChild2Id, + dnaChild3Id: apiKeyData.dnaChild3Id, + dnaChild4Id: apiKeyData.dnaChild4Id, }; } diff --git a/src/middlewares/user.ts b/src/middlewares/user.ts index 75c84d01..09e32ef9 100644 --- a/src/middlewares/user.ts +++ b/src/middlewares/user.ts @@ -25,5 +25,11 @@ export type RequestWithUserWebService = Request & { id: string; name: string; accessApi: string[]; + accessType?: string; + dnaRootId?: string | null; + dnaChild1Id?: string | null; + dnaChild2Id?: string | null; + dnaChild3Id?: string | null; + dnaChild4Id?: string | null; }; }; From 44793fbfbbfb3482b5acbbedae3f11e31e085957 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 13:44:03 +0700 Subject: [PATCH 4/5] 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); } }, From b7c80ea6d4a4f94ce48c34ac42e2f1dd7c3d4fac Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 14:03:30 +0700 Subject: [PATCH 5/5] fixed error --- src/controllers/ScriptProfileOrgController.ts | 2 +- src/services/KeycloakAttributeService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ScriptProfileOrgController.ts b/src/controllers/ScriptProfileOrgController.ts index 4881249e..0494be98 100644 --- a/src/controllers/ScriptProfileOrgController.ts +++ b/src/controllers/ScriptProfileOrgController.ts @@ -49,7 +49,7 @@ export class ScriptProfileOrgController extends Controller { * @summary Update org structure for profiles updated within a certain time window and sync to Keycloak */ @Post("update-org") - public async cronjobUpdateOrg(@Request() request: RequestWithUser) { + public async cronjobUpdateOrg(@Request() _request: RequestWithUser) { // Idempotency check - prevent concurrent runs if (this.isRunning) { console.log("cronjobUpdateOrg: Job already running, skipping this execution"); diff --git a/src/services/KeycloakAttributeService.ts b/src/services/KeycloakAttributeService.ts index 5206183b..7bfe88ed 100644 --- a/src/services/KeycloakAttributeService.ts +++ b/src/services/KeycloakAttributeService.ts @@ -543,7 +543,7 @@ export class KeycloakAttributeService { .createQueryBuilder("p") .where("p.keycloak IS NOT NULL") .andWhere("p.keycloak != :empty", { empty: "" }) - .andWhere({ "p.isDeleted": false }) + .andWhere("p.isDelete = :isDelete", { isDelete: false }) .andWhere("p.lastUpdatedAt BETWEEN :start AND :end", { start: startDate, end: endDate,