From d3f01165aeb580246eb39fe7939bae45e5ba6491 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Thu, 21 May 2026 18:08:20 +0700 Subject: [PATCH] fix api web service --- src/controllers/ApiManageController.ts | 53 +++- src/controllers/ApiWebServiceController.ts | 350 +++++++++++++++++++-- 2 files changed, 367 insertions(+), 36 deletions(-) diff --git a/src/controllers/ApiManageController.ts b/src/controllers/ApiManageController.ts index 34f0c824..1f89e98d 100644 --- a/src/controllers/ApiManageController.ts +++ b/src/controllers/ApiManageController.ts @@ -316,12 +316,12 @@ export class ApiManageController extends Controller { description: "ข้อมูลส่วนราชการ ระดับที่ 4", system: ["position"], }, - { - name: "Profile", - repository: this.profileRepository, - description: "ข้อมูลคนครอง", - system: ["position"], - }, + // { + // name: "Profile", + // repository: this.profileRepository, + // description: "ข้อมูลคนครอง", + // system: ["position"], + // }, ]; private readonly DEFAULT_PAGE_SIZE = 10; // ขนาดหน้าเริ่มต้น @@ -443,6 +443,27 @@ export class ApiManageController extends Controller { }, }; + // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ ProfileEmployee entity + private readonly PROFILEEMPLOYEE_FIELD_REPLACEMENTS: Record< + string, + { propertyName: string; type: string; comment: string; joinTable: string; joinField: string } + > = { + posLevelId: { + propertyName: "posLevelName", + type: "string", + comment: "ระดับชั้นงาน", + joinTable: "EmployeePosLevel", + joinField: "posLevelName", + }, + posTypeId: { + propertyName: "posTypeName", + type: "string", + comment: "กลุ่มงาน", + joinTable: "EmployeePosType", + joinField: "posTypeName", + }, + }; + private validateSuperAdminRole(user: any): void { if (!user.role.includes("SUPER_ADMIN")) { throw new HttpError(HttpStatusCode.FORBIDDEN, "คุณไม่มีสิทธิ์ในการเข้าถึงข้อมูลนี้"); @@ -533,6 +554,26 @@ export class ApiManageController extends Controller { columns = [...columns, ...nameFields]; } + // Special handling for ProfileEmployee entity - replace ID fields with name fields + if (name === "ProfileEmployee") { + const replacementKeys = Object.keys(this.PROFILEEMPLOYEE_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.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].propertyName, + type: "string", + comment: this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[key].comment, + key: this.PROFILEEMPLOYEE_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 diff --git a/src/controllers/ApiWebServiceController.ts b/src/controllers/ApiWebServiceController.ts index 49e9287d..8673e684 100644 --- a/src/controllers/ApiWebServiceController.ts +++ b/src/controllers/ApiWebServiceController.ts @@ -8,6 +8,10 @@ import { isPermissionRequest } from "../middlewares/authWebService"; import { RequestWithUserWebService } from "../middlewares/user"; import { OrgRevision } from "../entities/OrgRevision"; import { ApiHistory } from "../entities/ApiHistory"; +import { OrgChild1 } from "../entities/OrgChild1"; +import { OrgChild2 } from "../entities/OrgChild2"; +import { OrgChild3 } from "../entities/OrgChild3"; +import { OrgChild4 } from "../entities/OrgChild4"; import { SystemCode } from "./../interfaces/api-type"; @Route("api/v1/org/api-service") @Tags("ApiKey") @@ -91,6 +95,23 @@ export class ApiWebServiceController extends Controller { }, }; + // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ ProfileEmployee entity + private readonly PROFILEEMPLOYEE_FIELD_REPLACEMENTS: Record< + string, + { propertyName: string; joinRelation: string; joinField: string } + > = { + posTypeName: { + propertyName: "posTypeId", + joinRelation: "posType", + joinField: "posTypeName", + }, + posLevelName: { + propertyName: "posLevelId", + joinRelation: "posLevel", + joinField: "posLevelName", + }, + }; + /** * build posMaster permission condition * @summary สร้างเงื่อนไขการกรองข้อมูลตามสิทธิ์การเข้าถึง @@ -198,6 +219,13 @@ export class ApiWebServiceController extends Controller { await isPermissionRequest(request, apiName.id); const offset = (page - 1) * pageSize; let propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`); + const selectedFieldsByTable: Record> = {}; + apiName.apiAttributes.forEach((attr) => { + if (!selectedFieldsByTable[attr.tbName]) { + selectedFieldsByTable[attr.tbName] = new Set(); + } + selectedFieldsByTable[attr.tbName].add(attr.propertyKey); + }); let tbMain: string = ""; let condition: string = "1=1"; @@ -225,7 +253,7 @@ export class ApiWebServiceController extends Controller { condition = `PosMaster.orgRevisionId = "${revision?.id}"`; } - let posMasterCondition: string = ""; + let posMasterCondition: string = "1=1"; let posMasterAlias: string = ""; // Special handling for Profile and ProfileEmployee systems with permission filtering @@ -337,6 +365,13 @@ export class ApiWebServiceController extends Controller { ...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)), ]; + // Organization hierarchy is assembled in a separate step; keep main query focused on OrgRoot only. + if (tbMain === "OrgRoot") { + const orgChildTables = new Set(["OrgChild1", "OrgChild2", "OrgChild3", "OrgChild4"]); + propertyKey = propertyKey.filter((key) => !orgChildTables.has(key.split(".")[0])); + propertyOtherKey = propertyOtherKey.filter((tb) => !orgChildTables.has(tb)); + } + // สำหรับ Profile: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey const profileFieldJoins: Record = {}; // alias -> relationName if (tbMain === "Profile") { @@ -356,7 +391,7 @@ export class ApiWebServiceController extends Controller { // สำหรับ Position: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey const positionFieldJoins: Record = {}; // alias -> relationName - if (tbMain === "Position") { + if (tbMain === "Position" || tbMain === "PosMaster") { propertyKey = propertyKey.map((key) => { const [table, field] = key.split("."); if (table === "Position") { @@ -371,21 +406,56 @@ export class ApiWebServiceController extends Controller { }); } + // สำหรับ ProfileEmployee: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey + const profileEmployeeFieldJoins: Record = {}; // alias -> relationName + if (tbMain === "ProfileEmployee") { + propertyKey = propertyKey.map((key) => { + const [table, field] = key.split("."); + if (table === "ProfileEmployee") { + const replacement = this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS[field]; + if (replacement) { + const alias = `${table}_${replacement.joinRelation}`; + profileEmployeeFieldJoins[alias] = replacement.joinRelation; + return `${alias}.${replacement.joinField}`; + } + } + return key; + }); + } + const queryBuilder = repo.createQueryBuilder(tbMain); // join กับตารารอง if (propertyOtherKey.length > 0) { propertyOtherKey.forEach((tb) => { + // Skip Profile join for PosMaster - it's handled separately below + if (tbMain === "PosMaster" && tb === "Profile") { + return; + } + + // Skip Position join for PosMaster - it's handled separately below + if (tbMain === "PosMaster" && tb === "Position") { + return; + } + const relationName = relationMap[tb]; if (relationName) { - queryBuilder.leftJoin( - `${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`, // เช็คว่าถ้าเป็น next_holder ให้ใช้ current_holder แทน - tb, - ); + queryBuilder.leftJoin(`${tbMain}.${relationName}`, tb); + } else { + // Remove fields from this table from propertyKey + propertyKey = propertyKey.filter((key) => !key.startsWith(`${tb}.`)); } }); } + // Check if propertyKey is empty after filtering + if (propertyKey.length === 0) { + throw new HttpError( + HttpStatusCode.BAD_REQUEST, + "ไม่พบฟิลด์ที่ต้องการแสดงผล กรุณาตรวจสอบการตั้งค่า API (ไม่สามารถ join ตารางลูกได้)", + ); + } + // join สำหรับฟิลด์ Profile ที่ต้องการดึงค่าจากตารางอื่น if (tbMain === "Profile" && Object.keys(profileFieldJoins).length > 0) { Object.entries(profileFieldJoins).forEach(([alias, relationName]) => { @@ -394,8 +464,35 @@ export class ApiWebServiceController extends Controller { } // join สำหรับฟิลด์ Position ที่ต้องการดึงค่าจากตารางอื่น - if (tbMain === "Position" && Object.keys(positionFieldJoins).length > 0) { + if ( + (tbMain === "Position" || tbMain === "PosMaster") && + Object.keys(positionFieldJoins).length > 0 + ) { + if (tbMain === "PosMaster") { + const posMasterPositionRelation = relationMap["Position"]; + if (!posMasterPositionRelation) { + throw new HttpError( + HttpStatusCode.BAD_REQUEST, + "ไม่พบความสัมพันธ์ระหว่าง PosMaster กับ Position กรุณาตรวจสอบการตั้งค่า API", + ); + } + + // Join PosMaster -> Position once using actual relation name from metadata + queryBuilder.leftJoin(`PosMaster.${posMasterPositionRelation}`, "Position"); + } + Object.entries(positionFieldJoins).forEach(([alias, relationName]) => { + if (tbMain === "PosMaster") { + queryBuilder.leftJoin(`Position.${relationName}`, alias); + } else { + queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); + } + }); + } + + // join สำหรับฟิลด์ ProfileEmployee ที่ต้องการดึงค่าจากตารางอื่น + if (tbMain === "ProfileEmployee" && Object.keys(profileEmployeeFieldJoins).length > 0) { + Object.entries(profileEmployeeFieldJoins).forEach(([alias, relationName]) => { queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); }); } @@ -403,15 +500,24 @@ export class ApiWebServiceController extends Controller { // join สำหรับ PosMaster เมื่อต้องการดึงค่าจาก Profile (ข้อมูลคนครอง) const posMasterProfileFields: string[] = []; if (tbMain === "PosMaster") { - propertyKey.forEach((key) => { - if (key.startsWith("Profile.")) { - posMasterProfileFields.push(key); - } - }); + // Collect Profile fields from both formats: "Profile.xxx" and "PosMaster.Profile.xxx" + const extractedProfileFields = propertyKey + .filter((key) => key.startsWith("Profile.") || key.startsWith("PosMaster.Profile.")) + .map((key) => key.replace(/^PosMaster\.Profile\./, "Profile.")); + + posMasterProfileFields.push(...new Set(extractedProfileFields)); + + // Remove Profile fields then add back normalized "Profile.xxx" form + propertyKey = propertyKey.filter( + (key) => !key.startsWith("Profile.") && !key.startsWith("PosMaster.Profile."), + ); + propertyKey.push(...posMasterProfileFields); } // join PosMaster กับ Profile เมื่อมีการขอ Profile fields if (tbMain === "PosMaster" && posMasterProfileFields.length > 0) { + // Always join via current_holder (not next_holder) because PosMaster has two relations + // to Profile and relationMap["Profile"] would resolve to next_holder (last defined in entity) queryBuilder.leftJoin("PosMaster.current_holder", "Profile"); } @@ -434,7 +540,7 @@ export class ApiWebServiceController extends Controller { // propertyKey.push(`${Main}.id`); // } - // add FK + // add PK - ensure propertyKey is never empty let pk: string = ""; const primaryColumns = metadata.primaryColumns; primaryColumns.forEach((col) => { @@ -444,14 +550,27 @@ export class ApiWebServiceController extends Controller { } }); - const [items, total] = await queryBuilder - .select(propertyKey) - .where(condition) - .andWhere(posMasterCondition) - .orderBy(propertyKey[0], "ASC") - .skip(offset) - .take(pageSize) - .getManyAndCount(); + let items: any[] = []; + let total = 0; + + if (tbMain === "OrgRoot") { + // Organization API should always return full hierarchy regardless of page/pageSize. + [items, total] = await queryBuilder + .select(propertyKey) + .where(condition) + .andWhere(posMasterCondition) + .orderBy(propertyKey[0] || `${tbMain}.${pk}`, "ASC") + .getManyAndCount(); + } else { + [items, total] = await queryBuilder + .select(propertyKey) + .where(condition) + .andWhere(posMasterCondition) + .orderBy(propertyKey[0] || `${tbMain}.${pk}`, "ASC") + .skip(offset) + .take(pageSize) + .getManyAndCount(); + } // ลบ Main.id // const results = items.map(({ id, ...x }) => x); @@ -480,9 +599,30 @@ export class ApiWebServiceController extends Controller { } // สำหรับ Position: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม - if (tbMain === "Position") { + if (tbMain === "Position" || tbMain === "PosMaster") { 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 = `Position_${config.joinRelation}`; + if (rest[alias] && rest[alias][config.joinField] !== undefined) { + flattened[nameField] = rest[alias][config.joinField]; + } + // Remove the joined table object + delete flattened[alias]; + }); + // Remove Position object if exists + if (flattened["Position"]) { + delete flattened["Position"]; + } + return flattened; + } + + // สำหรับ ProfileEmployee: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม + if (tbMain === "ProfileEmployee") { + const flattened: any = { ...rest }; + Object.entries(this.PROFILEEMPLOYEE_FIELD_REPLACEMENTS).forEach(([nameField, config]) => { // Remove the original ID field delete flattened[config.propertyName]; // Add the name field from joined table @@ -499,23 +639,173 @@ export class ApiWebServiceController extends Controller { // สำหรับ 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 + const profileFieldNames = posMasterProfileFields + .filter((field) => field.startsWith("Profile.")) + .map((field) => field.replace("Profile.", "")); + + // Extract only requested Profile fields and add top-level aliases 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; + profileFieldNames.forEach((fieldName) => { + if (rest["Profile"][fieldName] !== undefined) { + flattened[`profile_${fieldName}`] = rest["Profile"][fieldName]; + } + }); // Remove the nested Profile object delete flattened["Profile"]; } return flattened; } + // สำหรับ OrgRoot: เก็บ primary key ไว้ใช้ group ข้อมูล แล้วแยก children ภายหลัง + if (tbMain === "OrgRoot") { + return { __rootPk: removedPk, ...rest }; + } + return rest; }); - // console.log("queryBuilder ===> ", queryBuilder.getQuery()); + let responseData: any[] = data; + let responseTotal = total; + + // สำหรับ Organization: รวมข้อมูลให้เหลือ 1 root ต่อ 1 object และจัด children ตาม hierarchy + if (tbMain === "OrgRoot") { + const rootVisibleFields = Array.from(selectedFieldsByTable["OrgRoot"] || []); + const child1VisibleFields = Array.from(selectedFieldsByTable["OrgChild1"] || []); + const child2VisibleFields = Array.from(selectedFieldsByTable["OrgChild2"] || []); + const child3VisibleFields = Array.from(selectedFieldsByTable["OrgChild3"] || []); + const child4VisibleFields = Array.from(selectedFieldsByTable["OrgChild4"] || []); + + const pickVisibleFields = (obj: any, fields: string[]) => { + const out: any = {}; + fields.forEach((field) => { + if (obj[field] !== undefined) { + out[field] = obj[field]; + } + }); + return out; + }; + + const rootMap = new Map(); + data.forEach((row: any) => { + if (!row.__rootPk || rootMap.has(row.__rootPk)) { + return; + } + + const rootNode = { + ...pickVisibleFields(row, rootVisibleFields), + children: [], + }; + rootMap.set(row.__rootPk, rootNode); + }); + + const rootIds = Array.from(rootMap.keys()); + + if (rootIds.length > 0) { + const buildSelect = (alias: string, required: string[], visible: string[]) => + Array.from(new Set([...required, ...visible])).map((field) => `${alias}.${field}`); + + const [child1Rows, child2Rows, child3Rows, child4Rows] = await Promise.all([ + AppDataSource.getRepository(OrgChild1) + .createQueryBuilder("OrgChild1") + .select(buildSelect("OrgChild1", ["id", "orgRootId"], child1VisibleFields)) + .where("OrgChild1.orgRootId IN (:...rootIds)", { rootIds }) + .orderBy("OrgChild1.id", "ASC") + .getMany(), + AppDataSource.getRepository(OrgChild2) + .createQueryBuilder("OrgChild2") + .select( + buildSelect("OrgChild2", ["id", "orgRootId", "orgChild1Id"], child2VisibleFields), + ) + .where("OrgChild2.orgRootId IN (:...rootIds)", { rootIds }) + .orderBy("OrgChild2.id", "ASC") + .getMany(), + AppDataSource.getRepository(OrgChild3) + .createQueryBuilder("OrgChild3") + .select( + buildSelect( + "OrgChild3", + ["id", "orgRootId", "orgChild1Id", "orgChild2Id"], + child3VisibleFields, + ), + ) + .where("OrgChild3.orgRootId IN (:...rootIds)", { rootIds }) + .orderBy("OrgChild3.id", "ASC") + .getMany(), + AppDataSource.getRepository(OrgChild4) + .createQueryBuilder("OrgChild4") + .select( + buildSelect( + "OrgChild4", + ["id", "orgRootId", "orgChild1Id", "orgChild2Id", "orgChild3Id"], + child4VisibleFields, + ), + ) + .where("OrgChild4.orgRootId IN (:...rootIds)", { rootIds }) + .orderBy("OrgChild4.id", "ASC") + .getMany(), + ]); + + const child1Map = new Map(); + const child2Map = new Map(); + const child3Map = new Map(); + + child1Rows.forEach((row) => { + const node = { + ...pickVisibleFields(row, child1VisibleFields), + children: [], + }; + child1Map.set(row.id, node); + + const rootNode = rootMap.get(row.orgRootId); + if (rootNode) { + rootNode.children.push(node); + } + }); + + child2Rows.forEach((row) => { + const node = { + ...pickVisibleFields(row, child2VisibleFields), + children: [], + }; + child2Map.set(row.id, node); + + const parent = child1Map.get(row.orgChild1Id); + if (parent) { + parent.children.push(node); + } + }); + + child3Rows.forEach((row) => { + const node = { + ...pickVisibleFields(row, child3VisibleFields), + children: [], + }; + child3Map.set(row.id, node); + + const parent = child2Map.get(row.orgChild2Id); + if (parent) { + parent.children.push(node); + } + }); + + child4Rows.forEach((row) => { + const node = { + ...pickVisibleFields(row, child4VisibleFields), + }; + + const parent = child3Map.get(row.orgChild3Id); + if (parent) { + if (!Array.isArray(parent.children)) { + parent.children = []; + } + parent.children.push(node); + } + }); + } + + responseData = Array.from(rootMap.values()); + responseTotal = responseData.length; + } // save api history after query success const history = { @@ -565,6 +855,6 @@ export class ApiWebServiceController extends Controller { // return flattenedItem; // }); - return new HttpSuccess({ data: data, total }); + return new HttpSuccess({ data: responseData, total: responseTotal }); } }