fix emp temp and add script clear org dna at keycloak

This commit is contained in:
Warunee Tamkoo 2026-02-27 11:40:56 +07:00
parent b714dfe239
commit 49a8494a8d
3 changed files with 363 additions and 57 deletions

View file

@ -187,6 +187,48 @@ export class KeycloakSyncController extends Controller {
});
}
/**
* Clear org DNA attributes for profiles (Admin only)
*
* @summary Clear org DNA attributes in Keycloak for given profiles (ADMIN)
*
* @description
* This endpoint will:
* - Clear all org DNA fields (orgRootDnaId, orgChild1-4DnaId) by setting them to empty strings
* - Use when an employee leaves their position (current_holderId becomes null)
*
* @param {request} request Request body containing profileIds array and profileType
*/
@Post("clear-org-dna")
async clearOrgDna(
@Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
) {
const { profileIds, profileType } = request;
// Validate profileIds
if (!profileIds || profileIds.length === 0) {
throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
}
// Validate profileType
if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
);
}
const result = await this.keycloakAttributeService.clearOrgDnaAttributes(
profileIds,
profileType,
);
return new HttpSuccess({
message: "Clear org DNA attributes เสร็จสิ้น",
...result,
});
}
/**
* Batch sync all users (Admin only)
*

View file

@ -9,6 +9,7 @@ import { PosMaster } from "./../entities/PosMaster";
import axios from "axios";
import { KeycloakSyncController } from "./KeycloakSyncController";
import { EmployeePosMaster } from "./../entities/EmployeePosMaster";
import { EmployeeTempPosMaster } from "./../entities/EmployeeTempPosMaster";
interface OrgUpdatePayload {
profileId: string;
@ -26,6 +27,7 @@ interface OrgUpdatePayload {
export class ScriptProfileOrgController extends Controller {
private posMasterRepo = AppDataSource.getRepository(PosMaster);
private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster);
private employeeTempPosMasterRepo = AppDataSource.getRepository(EmployeeTempPosMaster);
// Idempotency flag to prevent concurrent runs
private isRunning = false;
@ -37,6 +39,11 @@ export class ScriptProfileOrgController extends Controller {
10,
);
/**
* Script to update profile's organizational structure in leave service and sync to Keycloak
*
* @summary Update org structure for profiles updated within a certain time window and sync to Keycloak
*/
@Post("update-org")
public async cronjobUpdateOrg(@Request() request: RequestWithUser) {
// Idempotency check - prevent concurrent runs
@ -61,7 +68,7 @@ export class ScriptProfileOrgController extends Controller {
});
// Query with optimized select - only fetch required fields
const [posMasters, posMasterEmployee] = await Promise.all([
const [posMasters, posMasterEmployee, posMasterEmployeeTemp] = await Promise.all([
this.posMasterRepo.find({
where: {
lastUpdatedAt: MoreThanOrEqual(windowStart),
@ -120,16 +127,46 @@ export class ScriptProfileOrgController extends Controller {
current_holder: { id: true },
},
}),
this.employeeTempPosMasterRepo.find({
where: {
lastUpdatedAt: MoreThanOrEqual(windowStart),
orgRevision: {
orgRevisionIsCurrent: true,
},
},
relations: [
"orgRevision",
"orgRoot",
"orgChild1",
"orgChild2",
"orgChild3",
"orgChild4",
"current_holder",
],
select: {
id: true,
current_holderId: true,
lastUpdatedAt: true,
orgRevision: { id: true },
orgRoot: { ancestorDNA: true },
orgChild1: { ancestorDNA: true },
orgChild2: { ancestorDNA: true },
orgChild3: { ancestorDNA: true },
orgChild4: { ancestorDNA: true },
current_holder: { id: true },
},
}),
]);
console.log("cronjobUpdateOrg: Database query completed", {
posMastersCount: posMasters.length,
employeePosCount: posMasterEmployee.length,
totalRecords: posMasters.length + posMasterEmployee.length,
employeeTempPosCount: posMasterEmployeeTemp.length,
totalRecords: posMasters.length + posMasterEmployee.length + posMasterEmployeeTemp.length,
});
// Build payloads with proper profile type tracking
const payloads = this.buildPayloads(posMasters, posMasterEmployee);
const payloads = this.buildPayloads(posMasters, posMasterEmployee, posMasterEmployeeTemp);
if (payloads.length === 0) {
console.log("cronjobUpdateOrg: No records to process");
@ -246,12 +283,13 @@ export class ScriptProfileOrgController extends Controller {
}
/**
* Build payloads from PosMaster and EmployeePosMaster records
* Build payloads from PosMaster, EmployeePosMaster, and EmployeeTempPosMaster records
* Includes proper profile type tracking for accurate Keycloak sync
*/
private buildPayloads(
posMasters: PosMaster[],
posMasterEmployee: EmployeePosMaster[],
posMasterEmployeeTemp: EmployeeTempPosMaster[],
): OrgUpdatePayload[] {
const payloads: OrgUpdatePayload[] = [];
@ -285,6 +323,21 @@ export class ScriptProfileOrgController extends Controller {
}
}
// Process EmployeeTempPosMaster records (PROFILE_EMPLOYEE type)
for (const employeeTempPos of posMasterEmployeeTemp) {
if (employeeTempPos.current_holder && employeeTempPos.current_holderId) {
payloads.push({
profileId: employeeTempPos.current_holderId,
rootDnaId: employeeTempPos.orgRoot?.ancestorDNA || null,
child1DnaId: employeeTempPos.orgChild1?.ancestorDNA || null,
child2DnaId: employeeTempPos.orgChild2?.ancestorDNA || null,
child3DnaId: employeeTempPos.orgChild3?.ancestorDNA || null,
child4DnaId: employeeTempPos.orgChild4?.ancestorDNA || null,
profileType: "PROFILE_EMPLOYEE",
});
}
}
return payloads;
}

View file

@ -88,40 +88,101 @@ export class KeycloakAttributeService {
}
// If not found in Profile, try ProfileEmployee (ลูกจ้าง)
const profileEmployeeResult = await this.profileEmployeeRepo
// First, get the profileEmployee to check employeeClass
const profileEmployeeBasic = await this.profileEmployeeRepo
.createQueryBuilder("pe")
.leftJoinAndSelect("pe.current_holders", "epm")
.leftJoinAndSelect("epm.orgRoot", "org")
.leftJoinAndSelect("epm.orgChild1", "orgChild1")
.leftJoinAndSelect("epm.orgChild2", "orgChild2")
.leftJoinAndSelect("epm.orgChild3", "orgChild3")
.leftJoinAndSelect("epm.orgChild4", "orgChild4")
.where("pe.keycloak = :keycloakUserId", { keycloakUserId })
.getOne();
if (
profileEmployeeResult &&
profileEmployeeResult.current_holders &&
profileEmployeeResult.current_holders.length > 0
) {
const currentPos = profileEmployeeResult.current_holders[0];
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
if (!profileEmployeeBasic) {
// Return null values if no profile found
return {
profileId: profileEmployeeResult.id,
orgRootDnaId,
orgChild1DnaId,
orgChild2DnaId,
orgChild3DnaId,
orgChild4DnaId,
empType: profileEmployeeResult.employeeClass,
prefix: profileEmployeeResult.prefix,
profileId: null,
orgRootDnaId: null,
orgChild1DnaId: null,
orgChild2DnaId: null,
orgChild3DnaId: null,
orgChild4DnaId: null,
empType: null,
prefix: null,
};
}
// Check employeeClass to determine which table to query
const isPermEmployee = profileEmployeeBasic.employeeClass === "PERM";
if (isPermEmployee) {
// ลูกจ้างประจำ (PERM) - ใช้ EmployeePosMaster
const profileEmployeeResult = await this.profileEmployeeRepo
.createQueryBuilder("pe")
.leftJoinAndSelect("pe.current_holders", "epm")
.leftJoinAndSelect("epm.orgRoot", "org")
.leftJoinAndSelect("epm.orgChild1", "orgChild1")
.leftJoinAndSelect("epm.orgChild2", "orgChild2")
.leftJoinAndSelect("epm.orgChild3", "orgChild3")
.leftJoinAndSelect("epm.orgChild4", "orgChild4")
.where("pe.keycloak = :keycloakUserId", { keycloakUserId })
.getOne();
if (
profileEmployeeResult &&
profileEmployeeResult.current_holders &&
profileEmployeeResult.current_holders.length > 0
) {
const currentPos = profileEmployeeResult.current_holders[0];
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
return {
profileId: profileEmployeeResult.id,
orgRootDnaId,
orgChild1DnaId,
orgChild2DnaId,
orgChild3DnaId,
orgChild4DnaId,
empType: profileEmployeeResult.employeeClass,
prefix: profileEmployeeResult.prefix,
};
}
} else {
// ลูกจ้างชั่วคราว (TEMP) - ใช้ EmployeeTempPosMaster
const profileEmployeeResult = await this.profileEmployeeRepo
.createQueryBuilder("pe")
.leftJoinAndSelect("pe.current_holderTemps", "etpm")
.leftJoinAndSelect("etpm.orgRoot", "org")
.leftJoinAndSelect("etpm.orgChild1", "orgChild1")
.leftJoinAndSelect("etpm.orgChild2", "orgChild2")
.leftJoinAndSelect("etpm.orgChild3", "orgChild3")
.leftJoinAndSelect("etpm.orgChild4", "orgChild4")
.where("pe.keycloak = :keycloakUserId", { keycloakUserId })
.getOne();
if (
profileEmployeeResult &&
profileEmployeeResult.current_holderTemps &&
profileEmployeeResult.current_holderTemps.length > 0
) {
const currentPos = profileEmployeeResult.current_holderTemps[0];
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
return {
profileId: profileEmployeeResult.id,
orgRootDnaId,
orgChild1DnaId,
orgChild2DnaId,
orgChild3DnaId,
orgChild4DnaId,
empType: profileEmployeeResult.employeeClass,
prefix: profileEmployeeResult.prefix,
};
}
}
// Return null values if no profile found
return {
profileId: null,
@ -187,40 +248,101 @@ export class KeycloakAttributeService {
};
}
} else {
const profileEmployeeResult = await this.profileEmployeeRepo
// First, get the profileEmployee to check employeeClass
const profileEmployeeBasic = await this.profileEmployeeRepo
.createQueryBuilder("pe")
.leftJoinAndSelect("pe.current_holders", "epm")
.leftJoinAndSelect("epm.orgRoot", "org")
.leftJoinAndSelect("pm.orgChild1", "orgChild1")
.leftJoinAndSelect("pm.orgChild2", "orgChild2")
.leftJoinAndSelect("pm.orgChild3", "orgChild3")
.leftJoinAndSelect("pm.orgChild4", "orgChild4")
.where("pe.id = :profileId", { profileId })
.getOne();
if (
profileEmployeeResult &&
profileEmployeeResult.current_holders &&
profileEmployeeResult.current_holders.length > 0
) {
const currentPos = profileEmployeeResult.current_holders[0];
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
if (!profileEmployeeBasic) {
return {
profileId: profileEmployeeResult.id,
orgRootDnaId,
orgChild1DnaId,
orgChild2DnaId,
orgChild3DnaId,
orgChild4DnaId,
empType: profileEmployeeResult.employeeClass,
prefix: profileEmployeeResult.prefix,
profileId: null,
orgRootDnaId: null,
orgChild1DnaId: null,
orgChild2DnaId: null,
orgChild3DnaId: null,
orgChild4DnaId: null,
empType: null,
prefix: null,
};
}
// Check employeeClass to determine which table to query
const isPermEmployee = profileEmployeeBasic.employeeClass === "PERM";
if (isPermEmployee) {
// ลูกจ้างประจำ (PERM) - ใช้ EmployeePosMaster
const profileEmployeeResult = await this.profileEmployeeRepo
.createQueryBuilder("pe")
.leftJoinAndSelect("pe.current_holders", "epm")
.leftJoinAndSelect("epm.orgRoot", "org")
.leftJoinAndSelect("epm.orgChild1", "orgChild1")
.leftJoinAndSelect("epm.orgChild2", "orgChild2")
.leftJoinAndSelect("epm.orgChild3", "orgChild3")
.leftJoinAndSelect("epm.orgChild4", "orgChild4")
.where("pe.id = :profileId", { profileId })
.getOne();
if (
profileEmployeeResult &&
profileEmployeeResult.current_holders &&
profileEmployeeResult.current_holders.length > 0
) {
const currentPos = profileEmployeeResult.current_holders[0];
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
return {
profileId: profileEmployeeResult.id,
orgRootDnaId,
orgChild1DnaId,
orgChild2DnaId,
orgChild3DnaId,
orgChild4DnaId,
empType: profileEmployeeResult.employeeClass,
prefix: profileEmployeeResult.prefix,
};
}
} else {
// ลูกจ้างชั่วคราว (TEMP) - ใช้ EmployeeTempPosMaster
const profileEmployeeResult = await this.profileEmployeeRepo
.createQueryBuilder("pe")
.leftJoinAndSelect("pe.current_holderTemps", "etpm")
.leftJoinAndSelect("etpm.orgRoot", "org")
.leftJoinAndSelect("etpm.orgChild1", "orgChild1")
.leftJoinAndSelect("etpm.orgChild2", "orgChild2")
.leftJoinAndSelect("etpm.orgChild3", "orgChild3")
.leftJoinAndSelect("etpm.orgChild4", "orgChild4")
.where("pe.id = :profileId", { profileId })
.getOne();
if (
profileEmployeeResult &&
profileEmployeeResult.current_holderTemps &&
profileEmployeeResult.current_holderTemps.length > 0
) {
const currentPos = profileEmployeeResult.current_holderTemps[0];
const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
return {
profileId: profileEmployeeResult.id,
orgRootDnaId,
orgChild1DnaId,
orgChild2DnaId,
orgChild3DnaId,
orgChild4DnaId,
empType: profileEmployeeResult.employeeClass,
prefix: profileEmployeeResult.prefix,
};
}
}
}
return {
@ -313,6 +435,95 @@ export class KeycloakAttributeService {
}
}
/**
* Clear org DNA attributes in Keycloak for given profiles
* Sets all org DNA fields to empty strings
*
* @param profileIds - Array of profile IDs to clear
* @param profileType - 'PROFILE' for officers or 'PROFILE_EMPLOYEE' for employees
* @returns Object with success/failed counts and details
*/
async clearOrgDnaAttributes(
profileIds: string[],
profileType: "PROFILE" | "PROFILE_EMPLOYEE",
): Promise<{
total: number;
success: number;
failed: number;
details: Array<{ profileId: string; status: "success" | "failed"; error?: string }>;
}> {
const result = {
total: profileIds.length,
success: 0,
failed: 0,
details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
};
for (const profileId of profileIds) {
try {
// Get the keycloak userId from the profile
let keycloakUserId: string | null = null;
if (profileType === "PROFILE") {
const profile = await this.profileRepo.findOne({ where: { id: profileId } });
keycloakUserId = profile?.keycloak || "";
} else {
const profileEmployee = await this.profileEmployeeRepo.findOne({
where: { id: profileId },
});
keycloakUserId = profileEmployee?.keycloak || "";
}
if (!keycloakUserId) {
result.failed++;
result.details.push({
profileId,
status: "failed",
error: "No Keycloak user ID found",
});
continue;
}
// Clear org DNA attributes by setting them to empty strings
const clearedAttributes: Record<string, string[]> = {
orgRootDnaId: [""],
orgChild1DnaId: [""],
orgChild2DnaId: [""],
orgChild3DnaId: [""],
orgChild4DnaId: [""],
};
const success = await updateUserAttributes(keycloakUserId, clearedAttributes);
if (success) {
result.success++;
result.details.push({
profileId,
status: "success",
});
console.log(`Cleared org DNA attributes for profile ${profileId} (${profileType})`);
} else {
result.failed++;
result.details.push({
profileId,
status: "failed",
error: "Failed to update Keycloak attributes",
});
}
} catch (error: any) {
result.failed++;
result.details.push({
profileId,
status: "failed",
error: error.message || "Unknown error",
});
console.error(`Error clearing org DNA attributes for profile ${profileId}:`, error);
}
}
return result;
}
/**
* Batch sync multiple users with unlimited count and parallel processing
* Useful for initial sync or periodic updates