fix api web service

This commit is contained in:
Warunee Tamkoo 2026-05-21 18:08:20 +07:00
parent 8c9a62a378
commit d3f01165ae
2 changed files with 367 additions and 36 deletions

View file

@ -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

View file

@ -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<string, Set<string>> = {};
apiName.apiAttributes.forEach((attr) => {
if (!selectedFieldsByTable[attr.tbName]) {
selectedFieldsByTable[attr.tbName] = new Set<string>();
}
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<string, string> = {}; // alias -> relationName
if (tbMain === "Profile") {
@ -356,7 +391,7 @@ export class ApiWebServiceController extends Controller {
// สำหรับ Position: ตรวจสอบฟิลด์ที่ต้องการ join และแปลง propertyKey
const positionFieldJoins: Record<string, string> = {}; // 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<string, string> = {}; // 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<string, any>();
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<string, any>();
const child2Map = new Map<string, any>();
const child3Map = new Map<string, any>();
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 });
}
}