hrms-api-org/src/controllers/ApiWebServiceController.ts

571 lines
22 KiB
TypeScript
Raw Normal View History

import { Controller, Route, Security, Tags, Path, Request, Response, Get, Query } from "tsoa";
2025-08-07 13:05:58 +07:00
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";
2025-08-08 09:26:48 +07:00
import { isPermissionRequest } from "../middlewares/authWebService";
import { RequestWithUserWebService } from "../middlewares/user";
2025-08-08 17:14:29 +07:00
import { OrgRevision } from "../entities/OrgRevision";
2025-08-13 16:14:44 +07:00
import { ApiHistory } from "../entities/ApiHistory";
import { SystemCode } from "./../interfaces/api-type";
@Route("api/v1/org/api-service")
2025-08-07 13:05:58 +07:00
@Tags("ApiKey")
2025-08-08 09:26:48 +07:00
@Security("webServiceAuth")
2025-08-07 13:05:58 +07:00
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง",
)
export class ApiWebServiceController extends Controller {
private apiNameRepository = AppDataSource.getRepository(ApiName);
2025-08-08 17:14:29 +07:00
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
2025-08-13 16:14:44 +07:00
private apiHistoryRepository = AppDataSource.getRepository(ApiHistory);
private currentRevisionId: string = "";
2025-08-13 16:14:44 +07:00
// การแทนที่ฟิลด์ 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",
},
};
2026-05-21 13:44:03 +07:00
// การแทนที่ฟิลด์ 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;
},
2026-05-21 13:44:03 +07:00
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(
2026-05-21 13:44:03 +07:00
`${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(
2026-05-21 13:44:03 +07:00
`${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`,
);
} else if (dnaIds.dnaChild3Id) {
conditions.push(
2026-05-21 13:44:03 +07:00
`${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(
2026-05-21 13:44:03 +07:00
`(${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(
2026-05-21 13:44:03 +07:00
`${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`,
);
if (accessType === "CHILD") {
conditions.push(
2026-05-21 13:44:03 +07:00
`(${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(
2026-05-21 13:44:03 +07:00
`${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`,
);
if (accessType === "CHILD") {
conditions.push(
2026-05-21 13:44:03 +07:00
`(${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(
2026-05-21 13:44:03 +07:00
`${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`,
);
if (accessType === "CHILD") {
conditions.push(
2026-05-21 13:44:03 +07:00
`(${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";
}
2025-08-07 13:05:58 +07:00
/**
* list fields by systems
* @summary fields systems
*/
@Get("/:system/:code")
async listAttribute(
2025-08-08 09:26:48 +07:00
@Request() request: RequestWithUserWebService,
@Path("system")
system: SystemCode,
2025-08-07 13:05:58 +07:00
@Path("code") code: string,
@Query("page") page: number = 1,
@Query("pageSize") pageSize: number = 100,
): Promise<HttpSuccess | HttpError> {
2025-08-08 17:14:29 +07:00
const apiName = await this.apiNameRepository.findOne({
where: { code },
select: ["id", "code", "methodApi", "system", "isActive"],
relations: ["apiAttributes"],
order: {
apiAttributes: {
ordering: "ASC",
2025-08-07 13:05:58 +07:00
},
2025-08-08 17:14:29 +07:00
},
});
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}`);
2025-08-07 13:05:58 +07:00
let tbMain: string = "";
let condition: string = "1=1";
2025-08-08 17:14:29 +07:00
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"`;
2025-08-14 12:20:59 +07:00
} else if (system == "organization") {
tbMain = "OrgRoot";
2025-08-08 17:14:29 +07:00
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
condition = `OrgRoot.orgRevisionId = "${revision?.id}"`;
2025-08-14 12:20:59 +07:00
} else if (system == "position") {
tbMain = "PosMaster";
const revision = await this.orgRevisionRepository.findOne({
select: ["id"],
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
condition = `PosMaster.orgRevisionId = "${revision?.id}"`;
2025-08-08 17:14:29 +07:00
}
2025-08-13 11:45:34 +07:00
let posMasterCondition: string = "";
2026-05-21 13:44:03 +07:00
let posMasterAlias: string = "";
2026-05-21 13:44:03 +07:00
// 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 || "";
2026-05-21 13:44:03 +07:00
posMasterAlias = "posMaster";
// Build permission condition
2026-05-21 13:44:03 +07:00
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 },
});
2026-05-21 13:44:03 +07:00
// 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);
2025-08-08 17:14:29 +07:00
const metadata = repo.metadata;
2025-08-08 17:14:29 +07:00
const relationMap: Record<string, string> = {};
metadata.relations.forEach((rel) => {
relationMap[rel.inverseEntityMetadata.name] = rel.propertyName;
});
2025-08-08 17:14:29 +07:00
// ดึงเฉพาะตารางรอง (ถ้าเลือกไว้)
let propertyOtherKey: any[] = [];
2025-08-08 17:14:29 +07:00
propertyOtherKey = [
...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)),
];
// สำหรับ Profile: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey
const profileFieldJoins: Record<string, string> = {}; // 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;
});
}
2026-05-21 13:44:03 +07:00
// สำหรับ Position: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey
const positionFieldJoins: Record<string, string> = {}; // 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);
2025-08-07 13:05:58 +07:00
2025-08-08 17:14:29 +07:00
// join กับตารารอง
if (propertyOtherKey.length > 0) {
propertyOtherKey.forEach((tb) => {
const relationName = relationMap[tb];
if (relationName) {
2025-08-14 12:20:59 +07:00
queryBuilder.leftJoin(
`${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`, // เช็คว่าถ้าเป็น next_holder ให้ใช้ current_holder แทน
tb,
);
2025-08-08 17:14:29 +07:00
}
});
}
// join สำหรับฟิลด์ Profile ที่ต้องการดึงค่าจากตารางอื่น
if (tbMain === "Profile" && Object.keys(profileFieldJoins).length > 0) {
Object.entries(profileFieldJoins).forEach(([alias, relationName]) => {
queryBuilder.leftJoin(`${tbMain}.${relationName}`, alias);
});
}
2026-05-21 13:44:03 +07:00
// 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");
}
}
}
2025-08-13 16:14:44 +07:00
// // เพิ่ม Main.id เพราะจะใช้ pk ในการแมบและนับจำนวน
// if (!propertyKey.includes(`${Main}.id`)) {
// propertyKey.push(`${Main}.id`);
// }
// add FK
let pk: string = "";
2025-08-13 16:14:44 +07:00
const primaryColumns = metadata.primaryColumns;
primaryColumns.forEach((col) => {
2025-08-13 16:14:44 +07:00
pk = col.propertyName;
if (!propertyKey.includes(`${tbMain}.${pk}`)) {
propertyKey.push(`${tbMain}.${pk}`);
2025-08-13 16:14:44 +07:00
}
});
2025-08-13 11:45:34 +07:00
2025-08-08 17:14:29 +07:00
const [items, total] = await queryBuilder
2025-08-13 11:45:34 +07:00
.select(propertyKey)
2025-08-08 17:14:29 +07:00
.where(condition)
.andWhere(posMasterCondition)
2025-08-13 16:14:44 +07:00
.orderBy(propertyKey[0], "ASC")
2025-08-08 17:14:29 +07:00
.skip(offset)
.take(pageSize)
.getManyAndCount();
2025-08-13 11:45:34 +07:00
// ลบ Main.id
2025-08-13 16:14:44 +07:00
// 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;
}
2026-05-21 13:44:03 +07:00
// สำหรับ 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;
});
2025-08-14 12:20:59 +07:00
// console.log("queryBuilder ===> ", queryBuilder.getQuery());
// save api history after query success
2025-08-13 16:14:44 +07:00
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);
2025-08-14 12:20:59 +07:00
2025-08-19 12:07:36 +07:00
// 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 });
2025-08-07 13:05:58 +07:00
}
}