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 { 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", }, }; /** * 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"; } /** * 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}`); 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 = ""; 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)), ]; // สำหรับ 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") { 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 กับตารารอง if (propertyOtherKey.length > 0) { propertyOtherKey.forEach((tb) => { const relationName = relationMap[tb]; if (relationName) { queryBuilder.leftJoin( `${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`, // เช็คว่าถ้าเป็น next_holder ให้ใช้ current_holder แทน tb, ); } }); } // 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" && 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 ในการแมบและนับจำนวน // if (!propertyKey.includes(`${Main}.id`)) { // propertyKey.push(`${Main}.id`); // } // add FK let pk: string = ""; const primaryColumns = metadata.primaryColumns; primaryColumns.forEach((col) => { pk = col.propertyName; if (!propertyKey.includes(`${tbMain}.${pk}`)) { propertyKey.push(`${tbMain}.${pk}`); } }); const [items, total] = await queryBuilder .select(propertyKey) .where(condition) .andWhere(posMasterCondition) .orderBy(propertyKey[0], "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]; } }); 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; }); // console.log("queryBuilder ===> ", queryBuilder.getQuery()); // 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: data, total }); } }