import { Controller, Route, Security, Tags, Path, Request, Response, Get, Query } from "tsoa"; import { AppDataSource } from "../database/data-source"; import HttpSuccess from "../interfaces/http-success"; import HttpStatusCode from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; import { ApiName } from "../entities/ApiName"; 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") @Security("webServiceAuth") @Response( HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง", ) 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< 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", }, }; // การแทนที่ฟิลด์ 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", }, }; // การแทนที่ฟิลด์ 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 สร้างเงื่อนไขการกรองข้อมูลตามสิทธิ์การเข้าถึง */ private buildPosMasterPermissionCondition( accessType: string | undefined, dnaIds: { dnaRootId?: string | null; dnaChild1Id?: string | null; dnaChild2Id?: string | null; dnaChild3Id?: string | null; dnaChild4Id?: string | null; }, tableAlias: string = "posMaster", ): 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( `${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( `${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`, ); } else if (dnaIds.dnaChild3Id) { conditions.push( `${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( `(${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( `${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, ); if (accessType === "CHILD") { conditions.push( `(${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( `${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, ); if (accessType === "CHILD") { conditions.push( `(${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( `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, ); if (accessType === "CHILD") { conditions.push( `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild1Id IS NOT NULL)`, ); } } } return conditions.length > 0 ? `(${conditions.join(" OR ")})` : "1=1"; } /** * rename ancestorDNA to id * @summary เปลี่ยนชื่อฟิลด์ ancestorDNA เป็น id */ private renameAncestorDnaToId(obj: any): any { if (!obj || typeof obj !== "object") { return obj; } const result = { ...obj }; if (result.ancestorDNA !== undefined) { result.id = result.ancestorDNA; delete result.ancestorDNA; } return result; } /** * list fields by systems * @summary รายการ fields ตาม systems */ @Get("/:system/:code") async listAttribute( @Request() request: RequestWithUserWebService, @Path("system") system: SystemCode, @Path("code") code: string, @Query("page") page: number = 1, @Query("pageSize") pageSize: number = 100, ): Promise { const apiName = await this.apiNameRepository.findOne({ where: { code }, select: ["id", "code", "methodApi", "system", "isActive"], relations: ["apiAttributes"], order: { apiAttributes: { ordering: "ASC", }, }, }); if (!apiName || apiName.system != system || !apiName.isActive || apiName.methodApi != "GET") { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบ API ที่ระบุ"); } 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); }); // สำหรับ Organization: ให้รวม ancestorDNA เสมอ เพื่อแสดงเป็น id if (system === "organization") { // สำหรับ OrgRoot const ancestorDnaField = "OrgRoot.ancestorDNA"; if (!propertyKey.includes(ancestorDnaField)) { propertyKey.push(ancestorDnaField); } if (!selectedFieldsByTable["OrgRoot"]) { selectedFieldsByTable["OrgRoot"] = new Set(); } selectedFieldsByTable["OrgRoot"].add("ancestorDNA"); // สำหรับ OrgChild1, OrgChild2, OrgChild3, OrgChild4 const childTables = ["OrgChild1", "OrgChild2", "OrgChild3", "OrgChild4"]; childTables.forEach((table) => { if (!selectedFieldsByTable[table]) { selectedFieldsByTable[table] = new Set(); } selectedFieldsByTable[table].add("ancestorDNA"); }); } let tbMain: string = ""; let condition: string = "1=1"; if (system == "registry") { tbMain = "Profile"; } else if (system == "registry_emp") { tbMain = "ProfileEmployee"; condition = `ProfileEmployee.employeeClass = "PERM"`; } else if (system == "registry_temp") { tbMain = "ProfileEmployee"; condition = `ProfileEmployee.employeeClass = "TEMP"`; } else if (system == "organization") { tbMain = "OrgRoot"; const revision = await this.orgRevisionRepository.findOne({ select: ["id"], where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); condition = `OrgRoot.orgRevisionId = "${revision?.id}"`; } else if (system == "position") { tbMain = "PosMaster"; const revision = await this.orgRevisionRepository.findOne({ select: ["id"], where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); condition = `PosMaster.orgRevisionId = "${revision?.id}"`; } let posMasterCondition: string = "1=1"; let posMasterAlias: string = ""; // Special handling for Profile and ProfileEmployee systems 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 || ""; 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, }, 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); const metadata = repo.metadata; const relationMap: Record = {}; metadata.relations.forEach((rel) => { relationMap[rel.inverseEntityMetadata.name] = rel.propertyName; }); // ดึงเฉพาะตารางรอง (ถ้าเลือกไว้) let propertyOtherKey: any[] = []; propertyOtherKey = [ ...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") { 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; }); } // สำหรับ Position: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey const positionFieldJoins: Record = {}; // alias -> relationName if (tbMain === "Position" || tbMain === "PosMaster") { 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; }); } // สำหรับ 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}`, 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]) => { queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias); }); } // join สำหรับฟิลด์ Position ที่ต้องการดึงค่าจากตารางอื่น 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); }); } // join สำหรับ PosMaster เมื่อต้องการดึงค่าจาก Profile (ข้อมูลคนครอง) const posMasterProfileFields: string[] = []; if (tbMain === "PosMaster") { // 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"); } // 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 ในการแมบและนับจำนวน // if (!propertyKey.includes(`${Main}.id`)) { // propertyKey.push(`${Main}.id`); // } // add PK - ensure propertyKey is never empty let pk: string = ""; const primaryColumns = metadata.primaryColumns; primaryColumns.forEach((col) => { pk = col.propertyName; if (!propertyKey.includes(`${tbMain}.${pk}`)) { propertyKey.push(`${tbMain}.${pk}`); } }); 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); // const results = items.map(({ pk, ...x }) => x); // const results = items.map(item => { // primaryColumns.forEach(col => delete item[col.propertyName]); // return item; // }); // split object id ออกก่อน return const data = items.map((item) => { 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]; } // Also handle nested relation objects (e.g., "posLevel": { "posLevelName": "..." }) if ( rest[config.joinRelation] && rest[config.joinRelation][config.joinField] !== undefined ) { flattened[nameField] = rest[config.joinRelation][config.joinField]; delete flattened[config.joinRelation]; } }); return flattened; } // สำหรับ Position: แปลงฟิลด์ที่มาจาก join กลับเป็นชื่อเดิม 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 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]; // Also handle nested relation objects (e.g., "posType": { "posTypeName": "..." }) if ( rest[config.joinRelation] && rest[config.joinRelation][config.joinField] !== undefined ) { flattened[nameField] = rest[config.joinRelation][config.joinField]; delete flattened[config.joinRelation]; } }); return flattened; } // สำหรับ PosMaster: แปลงฟิลด์ Profile ที่มาจาก join กลับเป็นฟิลด์ระดับบน if (tbMain === "PosMaster" && posMasterProfileFields.length > 0) { const flattened: any = { ...rest }; 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"]) { 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; }); 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) { // ถ้าเป็น ancestorDNA ให้เปลี่ยนชื่อเป็น id if (field === "ancestorDNA") { out.id = obj[field]; } else { 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", "ancestorDNA"], child1VisibleFields)) .where("OrgChild1.orgRootId IN (:...rootIds)", { rootIds }) .orderBy("OrgChild1.id", "ASC") .getMany(), AppDataSource.getRepository(OrgChild2) .createQueryBuilder("OrgChild2") .select( buildSelect("OrgChild2", ["id", "orgRootId", "orgChild1Id", "ancestorDNA"], child2VisibleFields), ) .where("OrgChild2.orgRootId IN (:...rootIds)", { rootIds }) .orderBy("OrgChild2.id", "ASC") .getMany(), AppDataSource.getRepository(OrgChild3) .createQueryBuilder("OrgChild3") .select( buildSelect( "OrgChild3", ["id", "orgRootId", "orgChild1Id", "orgChild2Id", "ancestorDNA"], 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", "ancestorDNA"], 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()); // สำหรับ Organization: เปลี่ยนชื่อ ancestorDNA เป็น id responseData = responseData.map((root: any) => { const renamed = this.renameAncestorDnaToId(root); if (Array.isArray(renamed.children)) { renamed.children = renamed.children.map((child1: any) => { const renamedChild1 = this.renameAncestorDnaToId(child1); if (Array.isArray(renamedChild1.children)) { renamedChild1.children = renamedChild1.children.map((child2: any) => { const renamedChild2 = this.renameAncestorDnaToId(child2); if (Array.isArray(renamedChild2.children)) { renamedChild2.children = renamedChild2.children.map((child3: any) => { const renamedChild3 = this.renameAncestorDnaToId(child3); if (Array.isArray(renamedChild3.children)) { renamedChild3.children = renamedChild3.children.map((child4: any) => this.renameAncestorDnaToId(child4), ); } return renamedChild3; }); } return renamedChild2; }); } return renamedChild1; }); } return renamed; }); responseTotal = responseData.length; } // save api history after query success const history = { headerApi: JSON.stringify({ host: request.headers.host, "x-api-key": request.headers["x-api-key"], connection: request.headers.connection, accept: request.headers.accept, }), tokenApi: Array.isArray(request.headers["x-api-key"]) ? request.headers["x-api-key"][0] || "" : request.headers["x-api-key"] || "", requestApi: `${request.method} ${request.protocol}://${request.headers.host}${request.originalUrl || request.url}`, responseApi: "OK", ipApi: request.ip, codeApi: code, apiKeyId: request.user.id, apiNameId: apiName.id, createdFullName: request.user.name, lastUpdateFullName: request.user.name, }; await this.apiHistoryRepository.save(history); // const results = data.map((item) => { // const flattenedItem: any = {}; // // Extract nested object properties to top level // Object.keys(item).forEach((key) => { // const value = item[key]; // if (value && typeof value === "object") { // // if (Array.isArray(value) && value.length === 1) { // // // If array has single item, extract it as object // // Object.assign(flattenedItem, value[0]); // // } else // if (!Array.isArray(value)) { // // Merge nested object properties to top level // Object.assign(flattenedItem, value); // } else { // // Keep arrays with multiple items or empty arrays as is // flattenedItem[key] = value; // } // } else { // // Keep primitive values as is // flattenedItem[key] = value; // } // }); // return flattenedItem; // }); return new HttpSuccess({ data: responseData, total: responseTotal }); } }