From c5e600900c490abad67cc7a4206fd9beb208df12 Mon Sep 17 00:00:00 2001 From: waruneeauy Date: Wed, 11 Feb 2026 13:10:42 +0700 Subject: [PATCH] fix time out --- src/controllers/OrganizationController.ts | 199 +++++++++++++++++++--- 1 file changed, 178 insertions(+), 21 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 992fb053..1b233b4b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8197,25 +8197,13 @@ export class OrganizationController extends Controller { } } - // 2.5 Sync positions table for all affected posMasters - const positionSyncStats: { deleted: number; updated: number; inserted: number } = { - deleted: 0, - updated: 0, - inserted: 0, - }; - for (const [draftPosMasterId, [currentPosMasterId, nextHolderId]] of posMasterMapping) { - const stats = await this.syncPositionsForPosMaster( - queryRunner, - draftPosMasterId, - currentPosMasterId, - drafRevisionId, - currentRevisionId, - nextHolderId, - ); - positionSyncStats.deleted += stats.deleted; - positionSyncStats.updated += stats.updated; - positionSyncStats.inserted += stats.inserted; - } + // 2.5 Sync positions table for all affected posMasters (BATCH operation for performance) + const positionSyncStats = await this.syncAllPositionsBatch( + queryRunner, + posMasterMapping, + drafRevisionId, + currentRevisionId, + ); // Build comprehensive summary const summary = { @@ -8512,13 +8500,15 @@ export class OrganizationController extends Controller { /** * Helper function: Sync positions for a PosMaster * Handles DELETE/UPDATE/INSERT for positions associated with a posMaster + * + * @deprecated Kept as fallback - use syncAllPositionsBatch for better performance */ private async syncPositionsForPosMaster( queryRunner: any, draftPosMasterId: string, currentPosMasterId: string, - draftRevisionId: string, - currentRevisionId: string, + _draftRevisionId: string, + _currentRevisionId: string, nextHolderId: string | null | undefined, ): Promise<{ deleted: number; updated: number; inserted: number }> { // Fetch draft and current positions for this posMaster @@ -8607,4 +8597,171 @@ export class OrganizationController extends Controller { return { deleted: toDelete.length, updated: updatedCount, inserted: insertedCount }; } + + /** + * Batch version: Sync positions for ALL posMasters in a single operation + * This significantly reduces database round trips for large organizations + */ + private async syncAllPositionsBatch( + queryRunner: any, + posMasterMapping: Map, + _draftRevisionId: string, + _currentRevisionId: string, + ): Promise<{ deleted: number; updated: number; inserted: number }> { + // Extract draft and current posMaster IDs + const draftPosMasterIds = Array.from(posMasterMapping.keys()); + const currentPosMasterIds = Array.from(posMasterMapping.values()).map(([currentId]) => currentId); + + if (draftPosMasterIds.length === 0) { + return { deleted: 0, updated: 0, inserted: 0 }; + } + + // Fetch ALL positions for ALL posMasters in just 2 queries + const [allDraftPositions, allCurrentPositions] = await Promise.all([ + queryRunner.manager.find(Position, { + where: { + posMasterId: In(draftPosMasterIds), + }, + order: { orderNo: "ASC" }, + }), + queryRunner.manager.find(Position, { + where: { + posMasterId: In(currentPosMasterIds), + }, + }), + ]); + + // Group positions by posMasterId for processing + const draftPositionsByMaster = new Map(); + for (const pos of allDraftPositions) { + if (!draftPositionsByMaster.has(pos.posMasterId)) { + draftPositionsByMaster.set(pos.posMasterId, []); + } + draftPositionsByMaster.get(pos.posMasterId)!.push(pos); + } + + const currentPositionsByMaster = new Map(); + for (const pos of allCurrentPositions) { + if (!currentPositionsByMaster.has(pos.posMasterId)) { + currentPositionsByMaster.set(pos.posMasterId, []); + } + currentPositionsByMaster.get(pos.posMasterId)!.push(pos); + } + + // Collect all operations + const allToDelete: string[] = []; + const allToUpdate: Array<{ id: string; data: any }> = []; + const allToInsert: Array = []; + const profileUpdates: Map = new Map(); + + // Process each posMaster mapping + for (const [draftPosMasterId, [currentPosMasterId, nextHolderId]] of posMasterMapping) { + const draftPositions = draftPositionsByMaster.get(draftPosMasterId) || []; + const currentPositions = currentPositionsByMaster.get(currentPosMasterId) || []; + + // If no draft positions, mark all current positions for deletion + if (draftPositions.length === 0) { + allToDelete.push(...currentPositions.map((p: any) => p.id)); + continue; + } + + // Build map for tracking + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); + + // Mark for deletion: current positions not in draft (by orderNo) + for (const currentPos of currentPositions) { + if (!draftOrderNos.has(currentPos.orderNo)) { + allToDelete.push(currentPos.id); + } + } + + // Process UPDATE and INSERT + for (const draftPos of draftPositions) { + const current = currentByOrderNo.get(draftPos.orderNo); + + if (current) { + // UPDATE existing position - collect for batch update + allToUpdate.push({ + id: current.id, + data: { + positionName: draftPos.positionName, + positionField: draftPos.positionField, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + posExecutiveId: draftPos.posExecutiveId, + positionExecutiveField: draftPos.positionExecutiveField, + positionArea: draftPos.positionArea, + isSpecial: draftPos.isSpecial, + orderNo: draftPos.orderNo, + positionIsSelected: draftPos.positionIsSelected, + lastUpdateFullName: draftPos.lastUpdateFullName, + lastUpdatedAt: new Date(), + }, + }); + } else { + // INSERT new position - collect for batch insert + allToInsert.push({ + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }); + } + + // Collect profile update for the selected position + if (nextHolderId != null && draftPos.positionIsSelected) { + profileUpdates.set(nextHolderId, { + position: draftPos.positionName, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }); + } + } + } + + // Execute bulk operations + let deletedCount = 0; + let updatedCount = 0; + let insertedCount = 0; + + // Bulk DELETE + if (allToDelete.length > 0) { + await queryRunner.manager.delete(Position, allToDelete); + deletedCount = allToDelete.length; + } + + // Bulk UPDATE (batch by 500 to avoid query size limits) + if (allToUpdate.length > 0) { + const batchSize = 500; + for (let i = 0; i < allToUpdate.length; i += batchSize) { + const batch = allToUpdate.slice(i, i + batchSize); + await Promise.all( + batch.map(({ id, data }) => queryRunner.manager.update(Position, id, data)) + ); + } + updatedCount = allToUpdate.length; + } + + // Bulk INSERT + if (allToInsert.length > 0) { + const batchSize = 500; + for (let i = 0; i < allToInsert.length; i += batchSize) { + const batch = allToInsert.slice(i, i + batchSize); + await queryRunner.manager.save(Position, batch); + } + insertedCount = allToInsert.length; + } + + // Bulk UPDATE profiles + if (profileUpdates.size > 0) { + const profileUpdateEntries = Array.from(profileUpdates.entries()); + await Promise.all( + profileUpdateEntries.map(([profileId, data]) => + queryRunner.manager.update(Profile, profileId, data) + ) + ); + } + + return { deleted: deletedCount, updated: updatedCount, inserted: insertedCount }; + } }