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:
parent
94edcf5320
commit
af2bd5054f
1 changed files with 64 additions and 24 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue