feat: clear menu and role cache when organization structure is published

Add Redis cache clearing to handler_org function to clear all menu_* and role_* keys
after successfully publishing organization structure changes. This ensures users
see updated permissions and menus immediately after publish.

- Add promisify import and Redis client setup
- Add clearMenuAndRoleCache helper function
- Call cache clearing before successful return

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Warunee Tamkoo 2026-05-14 11:37:57 +07:00
parent 94edcf5320
commit af2bd5054f

View file

@ -1,5 +1,6 @@
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import amqp from "amqplib"; import amqp from "amqplib";
import { promisify } from "util";
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import { Command } from "../entities/Command"; import { Command } from "../entities/Command";
import { chunkArray, commandTypePath } from "../interfaces/utils"; import { chunkArray, commandTypePath } from "../interfaces/utils";
@ -29,6 +30,10 @@ import { PayloadSendNoti } from "../interfaces/utils";
import { PermissionProfile } from "../entities/PermissionProfile"; import { PermissionProfile } from "../entities/PermissionProfile";
import { PosMasterHistory } from "../entities/PosMasterHistory"; import { PosMasterHistory } from "../entities/PosMasterHistory";
const redis = require("redis");
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = process.env.REDIS_PORT;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
function scheduleReconnect() { function scheduleReconnect() {
@ -143,7 +148,9 @@ function createConsumer( //----> consumer
console.log("[AMQ] Process Consumer success"); console.log("[AMQ] Process Consumer success");
return channel.ack(msg); return channel.ack(msg);
} }
console.error(`[AMQ] Process Consumer failed on queue ${queue}, acknowledging without retry`); console.error(
`[AMQ] Process Consumer failed on queue ${queue}, acknowledging without retry`,
);
return channel.ack(msg); return channel.ack(msg);
} catch (error) { } catch (error) {
console.error(`[AMQ] Consumer processing error on queue ${queue}:`, error); console.error(`[AMQ] Consumer processing error on queue ${queue}:`, error);
@ -547,19 +554,19 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
const repoPosmaster = AppDataSource.getRepository(PosMaster); const repoPosmaster = AppDataSource.getRepository(PosMaster);
const posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign); const posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign);
const posMasterActRepository = AppDataSource.getRepository(PosMasterAct); const posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile); // const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile);
const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster); // const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster);
const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster); // const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster);
const repoProfile = AppDataSource.getRepository(Profile); const repoProfile = AppDataSource.getRepository(Profile);
const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee); // const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee);
const employeePositionRepository = AppDataSource.getRepository(EmployeePosition); // const employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
const repoOrgRevision = AppDataSource.getRepository(OrgRevision); const repoOrgRevision = AppDataSource.getRepository(OrgRevision);
const orgRootRepository = AppDataSource.getRepository(OrgRoot); // const orgRootRepository = AppDataSource.getRepository(OrgRoot);
const child1Repository = AppDataSource.getRepository(OrgChild1); // const child1Repository = AppDataSource.getRepository(OrgChild1);
const child2Repository = AppDataSource.getRepository(OrgChild2); // const child2Repository = AppDataSource.getRepository(OrgChild2);
const child3Repository = AppDataSource.getRepository(OrgChild3); // const child3Repository = AppDataSource.getRepository(OrgChild3);
const child4Repository = AppDataSource.getRepository(OrgChild4); // const child4Repository = AppDataSource.getRepository(OrgChild4);
const { data, token, user } = JSON.parse(msg.content.toString()); const { data, user } = JSON.parse(msg.content.toString());
const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data; const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data;
console.log(`[AMQ] Received message - revisionId: ${id}, status: ${status}`); console.log(`[AMQ] Received message - revisionId: ${id}, status: ${status}`);
@ -994,7 +1001,7 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
if (posMasterUpdates.length > 0) { if (posMasterUpdates.length > 0) {
const chunks = chunkArray(posMasterUpdates, 500); const chunks = chunkArray(posMasterUpdates, 500);
const posMasterTableName = repoPosmaster.metadata.tableName; const posMasterTableName = repoPosmaster.metadata.tableName;
for (const chunk of chunks as typeof posMasterUpdates[]) { for (const chunk of chunks as (typeof posMasterUpdates)[]) {
const caseClauses = chunk.map(() => "WHEN ? THEN ?").join(" "); const caseClauses = chunk.map(() => "WHEN ? THEN ?").join(" ");
const wherePlaceholders = chunk.map(() => "?").join(", "); const wherePlaceholders = chunk.map(() => "?").join(", ");
const params = chunk.flatMap((update: (typeof posMasterUpdates)[number]) => [ const params = chunk.flatMap((update: (typeof posMasterUpdates)[number]) => [
@ -1207,13 +1214,15 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
? x.id ? x.id
: x.ancestorDNA, : x.ancestorDNA,
})); }));
const _orgemployeeTempPosMaster: EmployeeTempPosMaster[] = orgemployeeTempPosMaster.map((x) => ({ const _orgemployeeTempPosMaster: EmployeeTempPosMaster[] = orgemployeeTempPosMaster.map(
...x, (x) => ({
ancestorDNA: ...x,
x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000" ancestorDNA:
? x.id x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
: x.ancestorDNA, ? x.id
})); : x.ancestorDNA,
}),
);
console.time("[AMQ] insert_employeePosMaster"); console.time("[AMQ] insert_employeePosMaster");
await repoEmployeePosmaster await repoEmployeePosmaster
@ -1316,7 +1325,10 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
lastUpdateFullName: "System Administrator", lastUpdateFullName: "System Administrator",
lastUpdatedAt: timestamp, lastUpdatedAt: timestamp,
}); });
const buildColumnData = <T extends object>(repository: Repository<T>, source: T): Partial<T> => { const buildColumnData = <T extends object>(
repository: Repository<T>,
source: T,
): Partial<T> => {
const row = {} as Partial<T>; const row = {} as Partial<T>;
const target = row as Record<string, unknown>; const target = row as Record<string, unknown>;
const sourceRecord = source as Record<string, unknown>; const sourceRecord = source as Record<string, unknown>;
@ -1363,8 +1375,7 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
...buildColumnData(employeePositionRepository, position), ...buildColumnData(employeePositionRepository, position),
id: randomUUID(), id: randomUUID(),
posMasterId: positionParentKey === "posMasterId" ? parentId : undefined, posMasterId: positionParentKey === "posMasterId" ? parentId : undefined,
posMasterTempId: posMasterTempId: positionParentKey === "posMasterTempId" ? parentId : undefined,
positionParentKey === "posMasterTempId" ? parentId : undefined,
...buildAuditFields(positionTimestamp), ...buildAuditFields(positionTimestamp),
}); });
} }
@ -1409,7 +1420,8 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
const dataId = x.id; const dataId = x.id;
const matchedOrgRoot = findMatchedNodeByAncestorDNA(orgRootCurrent, x); const matchedOrgRoot = findMatchedNodeByAncestorDNA(orgRootCurrent, x);
const filteredEmployeePosMaster = employeePosMasterByNode.get(getNodeKey("root", dataId)) ?? []; const filteredEmployeePosMaster =
employeePosMasterByNode.get(getNodeKey("root", dataId)) ?? [];
await cloneEmployeeNodeBatch( await cloneEmployeeNodeBatch(
filteredEmployeePosMaster, filteredEmployeePosMaster,
@ -1664,6 +1676,8 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
console.log(`[AMQ] handler_org SUCCESS - Total time: ${Date.now() - startTime}ms`); console.log(`[AMQ] handler_org SUCCESS - Total time: ${Date.now() - startTime}ms`);
console.timeEnd("[AMQ] handler_org_total"); console.timeEnd("[AMQ] handler_org_total");
await clearMenuAndRoleCache();
return true; return true;
} catch (error) { } catch (error) {
const totalTime = Date.now() - startTime; const totalTime = Date.now() - startTime;
@ -1683,6 +1697,32 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
} }
} }
async function clearMenuAndRoleCache(): Promise<void> {
const redisClient = redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
const keysAsync = promisify(redisClient.keys).bind(redisClient);
const delAsync = promisify(redisClient.del).bind(redisClient);
try {
const menuKeys = await keysAsync("menu_*");
if (menuKeys.length > 0) {
await delAsync(...menuKeys);
console.log(`[AMQ] Cleared ${menuKeys.length} menu cache keys`);
}
const roleKeys = await keysAsync("role_*");
if (roleKeys.length > 0) {
await delAsync(...roleKeys);
console.log(`[AMQ] Cleared ${roleKeys.length} role cache keys`);
}
} finally {
redisClient.quit();
}
}
async function handler_org_draft(msg: amqp.ConsumeMessage): Promise<boolean> { async function handler_org_draft(msg: amqp.ConsumeMessage): Promise<boolean> {
const { data, token, user } = JSON.parse(msg.content.toString()); const { data, token, user } = JSON.parse(msg.content.toString());
const { requestBody, request, revision } = data; const { requestBody, request, revision } = data;