api web service add join for show name

This commit is contained in:
Warunee Tamkoo 2026-05-21 13:44:03 +07:00
parent b071bc2d92
commit 44793fbfbb
3 changed files with 290 additions and 31 deletions

View file

@ -346,6 +346,10 @@ export class ApiManageController extends Controller {
"ancestorDNA", "ancestorDNA",
"keycloak", "keycloak",
"commandId", "commandId",
"prefixMain",
"authRoleId",
"next_holderId",
"current_holderId",
]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์ ]; // ฟิลด์ที่ไม่ต้องการแสดงในผลลัพธ์
// การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity // การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Profile entity
@ -411,6 +415,34 @@ export class ApiManageController extends Controller {
}, },
}; };
// การแทนที่ฟิลด์ ID ด้วยฟิลด์ Name สำหรับ Position entity
private readonly POSITION_FIELD_REPLACEMENTS: Record<
string,
{ propertyName: string; type: string; comment: string; joinTable: string; joinField: string }
> = {
posTypeId: {
propertyName: "posTypeName",
type: "string",
comment: "ประเภทตำแหน่ง",
joinTable: "PosType",
joinField: "posTypeName",
},
posLevelId: {
propertyName: "posLevelName",
type: "string",
comment: "ระดับตำแหน่ง",
joinTable: "PosLevel",
joinField: "posLevelName",
},
posExecutiveId: {
propertyName: "posExecutiveName",
type: "string",
comment: "ตำแหน่งทางการบริหาร",
joinTable: "PosExecutive",
joinField: "posExecutiveName",
},
};
private validateSuperAdminRole(user: any): void { private validateSuperAdminRole(user: any): void {
if (!user.role.includes("SUPER_ADMIN")) { if (!user.role.includes("SUPER_ADMIN")) {
throw new HttpError(HttpStatusCode.FORBIDDEN, "คุณไม่มีสิทธิ์ในการเข้าถึงข้อมูลนี้"); throw new HttpError(HttpStatusCode.FORBIDDEN, "คุณไม่มีสิทธิ์ในการเข้าถึงข้อมูลนี้");
@ -466,8 +498,8 @@ export class ApiManageController extends Controller {
const replacementKeys = Object.keys(this.PROFILE_FIELD_REPLACEMENTS); const replacementKeys = Object.keys(this.PROFILE_FIELD_REPLACEMENTS);
// Remove ID fields that should be replaced // Remove ID fields that should be replaced
columns = columns.filter((col: { propertyName: string }) => columns = columns.filter(
!replacementKeys.includes(col.propertyName), (col: { propertyName: string }) => !replacementKeys.includes(col.propertyName),
); );
// Add the corresponding name fields // Add the corresponding name fields
@ -481,6 +513,45 @@ export class ApiManageController extends Controller {
columns = [...columns, ...nameFields]; columns = [...columns, ...nameFields];
} }
// Special handling for Position entity - replace ID fields with name fields
if (name === "Position") {
const replacementKeys = Object.keys(this.POSITION_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.POSITION_FIELD_REPLACEMENTS[key].propertyName,
type: "string",
comment: this.POSITION_FIELD_REPLACEMENTS[key].comment,
key: this.POSITION_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
const profileFields = ["prefix", "rank", "firstName", "lastName", "citizenId"];
const profileRepository = AppDataSource.getRepository(Profile);
const profileColumns = profileRepository.metadata.columns
.filter(
(column: any) => !column.isPrimary && profileFields.includes(column.propertyName),
)
.map((column: any) => ({
propertyName: `Profile.${column.propertyName}`,
type: typeof column.type === "string" ? column.type : "string",
comment: column.comment,
key: `Profile.${column.propertyName}`,
}));
columns = [...columns, ...profileColumns];
}
return { return {
tb: name, tb: name,
description, description,

View file

@ -69,6 +69,28 @@ export class ApiWebServiceController extends Controller {
}, },
}; };
// การแทนที่ฟิลด์ 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 * build posMaster permission condition
* @summary * @summary
@ -82,6 +104,7 @@ export class ApiWebServiceController extends Controller {
dnaChild3Id?: string | null; dnaChild3Id?: string | null;
dnaChild4Id?: string | null; dnaChild4Id?: string | null;
}, },
tableAlias: string = "posMaster",
): string { ): string {
// ALL - no filtering // ALL - no filtering
if (accessType === "ALL") { if (accessType === "ALL") {
@ -94,49 +117,49 @@ export class ApiWebServiceController extends Controller {
if (accessType === "ROOT" && dnaIds.dnaRootId) { if (accessType === "ROOT" && dnaIds.dnaRootId) {
// All organizations under this root // All organizations under this root
conditions.push( conditions.push(
`posMaster.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%")`, `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%")`,
); );
} else if (accessType === "CHILD" || accessType === "NORMAL") { } else if (accessType === "CHILD" || accessType === "NORMAL") {
// Build conditions based on which DNA level is specified // Build conditions based on which DNA level is specified
if (dnaIds.dnaChild4Id) { if (dnaIds.dnaChild4Id) {
conditions.push( conditions.push(
`posMaster.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`, `${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild4Id}")`,
); );
} else if (dnaIds.dnaChild3Id) { } else if (dnaIds.dnaChild3Id) {
conditions.push( conditions.push(
`posMaster.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`, `${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`,
); );
// For CHILD type, include all descendants // For CHILD type, include all descendants
if (accessType === "CHILD") { if (accessType === "CHILD") {
conditions.push( conditions.push(
`(posMaster.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%") OR posMaster.orgChild4Id IS NOT NULL)`, `(${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) { } else if (dnaIds.dnaChild2Id) {
conditions.push( conditions.push(
`posMaster.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, `${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`,
); );
if (accessType === "CHILD") { if (accessType === "CHILD") {
conditions.push( conditions.push(
`(posMaster.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR posMaster.orgChild3Id IS NOT NULL)`, `(${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) { } else if (dnaIds.dnaChild1Id) {
conditions.push( conditions.push(
`posMaster.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, `${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`,
); );
if (accessType === "CHILD") { if (accessType === "CHILD") {
conditions.push( conditions.push(
`(posMaster.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR posMaster.orgChild2Id IS NOT NULL)`, `(${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) { } else if (dnaIds.dnaRootId) {
conditions.push( conditions.push(
`posMaster.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`,
); );
if (accessType === "CHILD") { if (accessType === "CHILD") {
conditions.push( conditions.push(
`(posMaster.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR posMaster.orgChild1Id IS NOT NULL)`, `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild1Id IS NOT NULL)`,
); );
} }
} }
@ -203,8 +226,9 @@ export class ApiWebServiceController extends Controller {
} }
let posMasterCondition: string = ""; let posMasterCondition: string = "";
let posMasterAlias: string = "";
// Special handling for Profile system with permission filtering // Special handling for Profile and ProfileEmployee systems with permission filtering
if (system == "registry") { if (system == "registry") {
// Get current revision // Get current revision
const revision = await this.orgRevisionRepository.findOne({ const revision = await this.orgRevisionRepository.findOne({
@ -214,15 +238,89 @@ export class ApiWebServiceController extends Controller {
// Store for use in permission building // Store for use in permission building
this.currentRevisionId = revision?.id || ""; this.currentRevisionId = revision?.id || "";
posMasterAlias = "posMaster";
// Build permission condition // Build permission condition
posMasterCondition = this.buildPosMasterPermissionCondition(request.user.accessType, { posMasterCondition = this.buildPosMasterPermissionCondition(
dnaRootId: request.user.dnaRootId, request.user.accessType,
dnaChild1Id: request.user.dnaChild1Id, {
dnaChild2Id: request.user.dnaChild2Id, dnaRootId: request.user.dnaRootId,
dnaChild3Id: request.user.dnaChild3Id, dnaChild1Id: request.user.dnaChild1Id,
dnaChild4Id: request.user.dnaChild4Id, 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 repo = AppDataSource.getRepository(tbMain);
@ -256,6 +354,23 @@ export class ApiWebServiceController extends Controller {
}); });
} }
// สำหรับ 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); const queryBuilder = repo.createQueryBuilder(tbMain);
// join กับตารารอง // join กับตารารอง
@ -278,9 +393,40 @@ export class ApiWebServiceController extends Controller {
}); });
} }
// join กับ posMaster สำหรับ Profile เพื่อกรองตามสิทธิ์การเข้าถึง // join สำหรับฟิลด์ Position ที่ต้องการดึงค่าจากตารางอื่น
if (tbMain === "Profile" && posMasterCondition !== "1=1") { if (tbMain === "Position" && Object.keys(positionFieldJoins).length > 0) {
queryBuilder.leftJoin("Profile.current_holders", "posMaster"); 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 ในการแมบและนับจำนวน // // เพิ่ม Main.id เพราะจะใช้ pk ในการแมบและนับจำนวน
@ -333,6 +479,39 @@ export class ApiWebServiceController extends Controller {
return flattened; 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; return rest;
}); });

View file

@ -530,18 +530,20 @@ export class KeycloakAttributeService {
// Initialize rate limiter if rate limiting is enabled // Initialize rate limiter if rate limiting is enabled
if (rateLimit && rateLimit > 0) { if (rateLimit && rateLimit > 0) {
rateLimiter = new RateLimiter(rateLimit); rateLimiter = new RateLimiter(rateLimit);
console.log(`[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`); console.log(
`[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`,
);
} }
// Select repository based on profile type // Select repository based on profile type
const repo = const repo = profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo;
profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo;
// Query profiles updated within the month // Query profiles updated within the month
const profiles = await repo const profiles = await repo
.createQueryBuilder("p") .createQueryBuilder("p")
.where("p.keycloak IS NOT NULL") .where("p.keycloak IS NOT NULL")
.andWhere("p.keycloak != :empty", { empty: "" }) .andWhere("p.keycloak != :empty", { empty: "" })
.andWhere({ "p.isDeleted": false })
.andWhere("p.lastUpdatedAt BETWEEN :start AND :end", { .andWhere("p.lastUpdatedAt BETWEEN :start AND :end", {
start: startDate, start: startDate,
end: endDate, end: endDate,
@ -579,8 +581,7 @@ export class KeycloakAttributeService {
try { try {
// Check if empType is empty in Keycloak // Check if empType is empty in Keycloak
const { isEmpty, currentEmpType } = const { isEmpty, currentEmpType } = await this.checkEmpTypeEmpty(keycloakUserId);
await this.checkEmpTypeEmpty(keycloakUserId);
result.profilesChecked++; result.profilesChecked++;
@ -607,8 +608,7 @@ export class KeycloakAttributeService {
// Sync the profile // Sync the profile
const success = await withRetry( const success = await withRetry(
async () => async () => this.syncOnOrganizationChange(profile.id, profileType),
this.syncOnOrganizationChange(profile.id, profileType),
3, // maxRetries 3, // maxRetries
1000, // baseDelay 1000, // baseDelay
); );
@ -768,7 +768,13 @@ export class KeycloakAttributeService {
maxRetries?: number; // Retry attempts for failed operations maxRetries?: number; // Retry attempts for failed operations
rateLimit?: number; // Requests per second rateLimit?: number; // Requests per second
clearProgress?: boolean; // Start fresh, ignore existing progress clearProgress?: boolean; // Start fresh, ignore existing progress
}): Promise<{ total: number; success: number; failed: number; details: any[]; resumed?: boolean }> { }): Promise<{
total: number;
success: number;
failed: number;
details: any[];
resumed?: boolean;
}> {
const limit = options?.limit; const limit = options?.limit;
const concurrency = options?.concurrency ?? 5; const concurrency = options?.concurrency ?? 5;
const resume = options?.resume ?? false; const resume = options?.resume ?? false;
@ -922,7 +928,10 @@ export class KeycloakAttributeService {
// Save progress after each batch // Save progress after each batch
SyncProgressManager.save(updatedState); SyncProgressManager.save(updatedState);
// Log progress every 50 items // Log progress every 50 items
if (updatedState.lastSyncedIndex % 50 === 0 || updatedState.lastSyncedIndex === updatedState.totalProfiles) { if (
updatedState.lastSyncedIndex % 50 === 0 ||
updatedState.lastSyncedIndex === updatedState.totalProfiles
) {
SyncProgressManager.logProgress(updatedState); SyncProgressManager.logProgress(updatedState);
} }
}, },