fix: new root
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m19s

This commit is contained in:
Warunee Tamkoo 2026-02-12 17:10:44 +07:00
parent d916334537
commit 22fd9152bf

View file

@ -7932,7 +7932,7 @@ export class OrganizationController extends Controller {
const currentRevisionId = currentRevision.id;
// ตรวจสอบว่ามี rootDnaId ในโครงสร้างร่าง และในโครงสร้างปัจจุบันหรือไม่
const [orgRootDraft, orgRootCurrent] = await Promise.all([
let [orgRootDraft, orgRootCurrent] = await Promise.all([
this.orgRootRepository.findOne({
where: {
ancestorDNA: rootDnaId,
@ -7950,333 +7950,331 @@ export class OrganizationController extends Controller {
]);
if (!orgRootDraft) return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโครงสร้างร่าง");
// if current record not found, create new one
if (!orgRootCurrent) {
// Create new current record using draft's ID
const newCurrentRoot = queryRunner.manager.create(OrgRoot, {
...orgRootDraft,
id: undefined, // Let database generate new ID
orgRevisionId: currentRevisionId, // Change to current revision
});
const savedRoot = await queryRunner.manager.save(OrgRoot, newCurrentRoot);
orgRootCurrent = savedRoot; // Use saved record for sync
}
// Part 1: Differential sync of organization structure (bottom-up)
// Build mapping incrementally as we process each level
if (orgRootCurrent) {
const allMappings: AllOrgMappings = {
orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() },
};
const allMappings: AllOrgMappings = {
orgRoot: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild1: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild2: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild3: { byAncestorDNA: new Map(), byDraftId: new Map() },
orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() },
};
// Track sync statistics for organization nodes
const orgSyncStats: Record<string, { deleted: number; updated: number; inserted: number }> =
{};
// Track sync statistics for organization nodes
const orgSyncStats: Record<string, { deleted: number; updated: number; inserted: number }> =
{};
// Process from top (Root) to bottom (Child4) to handle foreign key constraints
// OrgRoot (sync first - no parent dependencies)
const orgRootResult = await this.syncOrgLevel(
queryRunner,
OrgRoot,
this.orgRootRepository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgRoot = orgRootResult.mapping;
orgSyncStats.orgRoot = orgRootResult.counts;
// Process from top (Root) to bottom (Child4) to handle foreign key constraints
// OrgRoot (sync first - no parent dependencies)
const orgRootResult = await this.syncOrgLevel(
queryRunner,
OrgRoot,
this.orgRootRepository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgRoot = orgRootResult.mapping;
orgSyncStats.orgRoot = orgRootResult.counts;
// Child1 (parent OrgRoot already synced)
const child1Result = await this.syncOrgLevel(
queryRunner,
OrgChild1,
this.child1Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild1 = child1Result.mapping;
orgSyncStats.orgChild1 = child1Result.counts;
// Child1 (parent OrgRoot already synced)
const child1Result = await this.syncOrgLevel(
queryRunner,
OrgChild1,
this.child1Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild1 = child1Result.mapping;
orgSyncStats.orgChild1 = child1Result.counts;
// Child2 (parents OrgRoot and Child1 already synced)
const child2Result = await this.syncOrgLevel(
queryRunner,
OrgChild2,
this.child2Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild2 = child2Result.mapping;
orgSyncStats.orgChild2 = child2Result.counts;
// Child2 (parents OrgRoot and Child1 already synced)
const child2Result = await this.syncOrgLevel(
queryRunner,
OrgChild2,
this.child2Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild2 = child2Result.mapping;
orgSyncStats.orgChild2 = child2Result.counts;
// Child3 (parents OrgRoot, Child1, Child2 already synced)
const child3Result = await this.syncOrgLevel(
queryRunner,
OrgChild3,
this.child3Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild3 = child3Result.mapping;
orgSyncStats.orgChild3 = child3Result.counts;
// Child3 (parents OrgRoot, Child1, Child2 already synced)
const child3Result = await this.syncOrgLevel(
queryRunner,
OrgChild3,
this.child3Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild3 = child3Result.mapping;
orgSyncStats.orgChild3 = child3Result.counts;
// Child4 (parents OrgRoot, Child1, Child2, Child3 already synced)
const child4Result = await this.syncOrgLevel(
queryRunner,
OrgChild4,
this.child4Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild4 = child4Result.mapping;
orgSyncStats.orgChild4 = child4Result.counts;
// Child4 (parents OrgRoot, Child1, Child2, Child3 already synced)
const child4Result = await this.syncOrgLevel(
queryRunner,
OrgChild4,
this.child4Repository,
drafRevisionId,
currentRevisionId,
allMappings,
orgRootDraft?.id,
orgRootCurrent?.id,
);
allMappings.orgChild4 = child4Result.mapping;
orgSyncStats.orgChild4 = child4Result.counts;
// Part 2: Sync position data using new org IDs from Part 1
// 2.1 Clear current_holderId for affected positions (keep existing logic)
// Get draft organization IDs under the given rootDnaId to find positions to clear
const draftOrgIds = {
orgRoot: [...allMappings.orgRoot.byDraftId.keys()],
orgChild1: [...allMappings.orgChild1.byDraftId.keys()],
orgChild2: [...allMappings.orgChild2.byDraftId.keys()],
orgChild3: [...allMappings.orgChild3.byDraftId.keys()],
orgChild4: [...allMappings.orgChild4.byDraftId.keys()],
};
// Part 2: Sync position data using new org IDs from Part 1
// 2.1 Clear current_holderId for affected positions (keep existing logic)
// Get draft organization IDs under the given rootDnaId to find positions to clear
const draftOrgIds = {
orgRoot: [...allMappings.orgRoot.byDraftId.keys()],
orgChild1: [...allMappings.orgChild1.byDraftId.keys()],
orgChild2: [...allMappings.orgChild2.byDraftId.keys()],
orgChild3: [...allMappings.orgChild3.byDraftId.keys()],
orgChild4: [...allMappings.orgChild4.byDraftId.keys()],
};
// Get draft positions that belong to any org under the rootDnaId
const posMasterDraft = await this.posMasterRepository.find({
where: [
{ orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) },
{ orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) },
{ orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) },
{ orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) },
{ orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) },
],
// Get draft positions that belong to any org under the rootDnaId
const posMasterDraft = await this.posMasterRepository.find({
where: [
{ orgRevisionId: drafRevisionId, orgRootId: In(draftOrgIds.orgRoot) },
{ orgRevisionId: drafRevisionId, orgChild1Id: In(draftOrgIds.orgChild1) },
{ orgRevisionId: drafRevisionId, orgChild2Id: In(draftOrgIds.orgChild2) },
{ orgRevisionId: drafRevisionId, orgChild3Id: In(draftOrgIds.orgChild3) },
{ orgRevisionId: drafRevisionId, orgChild4Id: In(draftOrgIds.orgChild4) },
],
});
if (posMasterDraft.length <= 0)
return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง");
// Clear current_holderId for positions that will have new holders
const nextHolderIds = posMasterDraft
.filter((x) => x.next_holderId != null)
.map((x) => x.next_holderId) as string[];
if (nextHolderIds.length > 0) {
// FIX: Fetch positions first before updating (to avoid race condition)
const posMastersToUpdate = await queryRunner.manager.find(PosMaster, {
where: {
orgRevisionId: currentRevisionId,
current_holderId: In(nextHolderIds),
},
});
if (posMasterDraft.length <= 0)
return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งในโครงสร้างร่าง");
// Clear current_holderId for positions that will have new holders
const nextHolderIds = posMasterDraft
.filter((x) => x.next_holderId != null)
.map((x) => x.next_holderId) as string[];
if (nextHolderIds.length > 0) {
// FIX: Fetch positions first before updating (to avoid race condition)
const posMastersToUpdate = await queryRunner.manager.find(PosMaster, {
where: {
orgRevisionId: currentRevisionId,
current_holderId: In(nextHolderIds),
},
});
// Save history BEFORE clearing current_holderId
const historyOps = posMastersToUpdate
.filter((x) => x.orgRootId != orgRootCurrent?.id)
.map((pos) => ({
posMasterDnaId: pos.ancestorDNA,
profileId: null,
pm: null,
}));
await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps);
// Now clear current_holderId
await queryRunner.manager.update(
PosMaster,
{
orgRevisionId: currentRevisionId,
current_holderId: In(nextHolderIds),
},
{ current_holderId: null, isSit: false },
);
}
// 2.2 Fetch current positions for comparison
// Get current organization IDs from the mappings
const currentOrgIds = {
orgRoot: [...allMappings.orgRoot.byDraftId.values()],
orgChild1: [...allMappings.orgChild1.byDraftId.values()],
orgChild2: [...allMappings.orgChild2.byDraftId.values()],
orgChild3: [...allMappings.orgChild3.byDraftId.values()],
orgChild4: [...allMappings.orgChild4.byDraftId.values()],
};
const posMasterCurrent = await this.posMasterRepository.find({
where: [
{ orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) },
{ orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) },
{ orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) },
{ orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) },
{ orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) },
],
});
// Build lookup map
const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p]));
// 2.3 Batch DELETE: positions in current but not in draft
const toDelete = posMasterCurrent.filter(
(curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA),
);
if (toDelete.length > 0) {
const toDeleteIds = toDelete.map((p) => p.id);
// Cascade delete positions first
await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) });
// Then delete posMaster records
await queryRunner.manager.delete(PosMaster, toDeleteIds);
const deleteHistoryOps = toDelete.map((pos) => ({
// Save history BEFORE clearing current_holderId
const historyOps = posMastersToUpdate
.filter((x) => x.orgRootId != orgRootCurrent?.id)
.map((pos) => ({
posMasterDnaId: pos.ancestorDNA,
profileId: null,
pm: null,
}));
await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps);
}
await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps);
// 2.4 Process draft positions (UPDATE or INSERT)
const toUpdate: PosMaster[] = [];
const toInsert: any[] = [];
// Track draft PosMaster ID to current PosMaster ID mapping for position sync
// Type: Map<draftPosMasterId, [currentPosMasterId, nextHolderId]>
const posMasterMapping: Map<string, [string, string | null | undefined]> = new Map();
for (const draftPos of posMasterDraft) {
const current = currentByDNA.get(draftPos.ancestorDNA);
// Map organization IDs using new IDs from Part 1
const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot);
const orgChild1Id = this.resolveOrgId(
draftPos.orgChild1Id ?? null,
allMappings.orgChild1,
);
const orgChild2Id = this.resolveOrgId(
draftPos.orgChild2Id ?? null,
allMappings.orgChild2,
);
const orgChild3Id = this.resolveOrgId(
draftPos.orgChild3Id ?? null,
allMappings.orgChild3,
);
const orgChild4Id = this.resolveOrgId(
draftPos.orgChild4Id ?? null,
allMappings.orgChild4,
);
if (current) {
// UPDATE existing position
Object.assign(current, {
createdAt: draftPos.createdAt,
createdUserId: draftPos.createdUserId,
createdFullName: draftPos.createdFullName,
lastUpdatedAt: new Date(),
lastUpdateUserId: request.user.sub,
lastUpdateFullName: request.user.name,
posMasterNoPrefix: draftPos.posMasterNoPrefix,
posMasterNoSuffix: draftPos.posMasterNoSuffix,
posMasterNo: draftPos.posMasterNo,
posMasterOrder: draftPos.posMasterOrder,
orgRootId,
orgChild1Id,
orgChild2Id,
orgChild3Id,
orgChild4Id,
current_holderId: draftPos.next_holderId,
isSit: draftPos.isSit,
reason: draftPos.reason,
isDirector: draftPos.isDirector,
isStaff: draftPos.isStaff,
positionSign: draftPos.positionSign,
statusReport: "DONE",
isCondition: draftPos.isCondition,
conditionReason: draftPos.conditionReason,
});
toUpdate.push(current);
if (draftPos.next_holderId === null) {
await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null);
}
// Track mapping for position sync
posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]);
} else {
// INSERT new position
const newPosMaster = queryRunner.manager.create(PosMaster, {
...draftPos,
id: undefined,
orgRevisionId: currentRevisionId,
orgRootId,
orgChild1Id,
orgChild2Id,
orgChild3Id,
orgChild4Id,
current_holderId: draftPos.next_holderId,
statusReport: "DONE",
});
toInsert.push(newPosMaster);
}
}
// Batch save updates and inserts
if (toUpdate.length > 0) {
await queryRunner.manager.save(toUpdate);
}
if (toInsert.length > 0) {
const saved = await queryRunner.manager.save(toInsert);
// Track mapping for newly inserted posMasters
// saved is an array, map each to its draft ID
if (Array.isArray(saved)) {
for (let i = 0; i < saved.length; i++) {
const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i];
if (draftPos && saved[i]) {
posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]);
}
}
}
}
// 2.5 Sync positions table for all affected posMasters (BATCH operation for performance)
const positionSyncStats = await this.syncAllPositionsBatch(
queryRunner,
posMasterMapping,
drafRevisionId,
currentRevisionId,
// Now clear current_holderId
await queryRunner.manager.update(
PosMaster,
{
orgRevisionId: currentRevisionId,
current_holderId: In(nextHolderIds),
},
{ current_holderId: null, isSit: false },
);
// Build comprehensive summary
const summary = {
message: "ย้ายโครงสร้างสำเร็จ",
organization: {
orgRoot: orgSyncStats.orgRoot,
orgChild1: orgSyncStats.orgChild1,
orgChild2: orgSyncStats.orgChild2,
orgChild3: orgSyncStats.orgChild3,
orgChild4: orgSyncStats.orgChild4,
},
positionMaster: {
deleted: toDelete.length,
updated: toUpdate.length,
inserted: toInsert.length,
},
position: positionSyncStats,
};
await queryRunner.commitTransaction();
return new HttpSuccess(summary);
}
return new HttpSuccess({});
// 2.2 Fetch current positions for comparison
// Get current organization IDs from the mappings
const currentOrgIds = {
orgRoot: [...allMappings.orgRoot.byDraftId.values()],
orgChild1: [...allMappings.orgChild1.byDraftId.values()],
orgChild2: [...allMappings.orgChild2.byDraftId.values()],
orgChild3: [...allMappings.orgChild3.byDraftId.values()],
orgChild4: [...allMappings.orgChild4.byDraftId.values()],
};
const posMasterCurrent = await this.posMasterRepository.find({
where: [
{ orgRevisionId: currentRevisionId, orgRootId: In(currentOrgIds.orgRoot) },
{ orgRevisionId: currentRevisionId, orgChild1Id: In(currentOrgIds.orgChild1) },
{ orgRevisionId: currentRevisionId, orgChild2Id: In(currentOrgIds.orgChild2) },
{ orgRevisionId: currentRevisionId, orgChild3Id: In(currentOrgIds.orgChild3) },
{ orgRevisionId: currentRevisionId, orgChild4Id: In(currentOrgIds.orgChild4) },
],
});
// Build lookup map
const currentByDNA = new Map(posMasterCurrent.map((p) => [p.ancestorDNA, p]));
// 2.3 Batch DELETE: positions in current but not in draft
const toDelete = posMasterCurrent.filter(
(curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA),
);
if (toDelete.length > 0) {
const toDeleteIds = toDelete.map((p) => p.id);
// Cascade delete positions first
await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) });
// Then delete posMaster records
await queryRunner.manager.delete(PosMaster, toDeleteIds);
const deleteHistoryOps = toDelete.map((pos) => ({
posMasterDnaId: pos.ancestorDNA,
profileId: null,
pm: null,
}));
await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps);
}
// 2.4 Process draft positions (UPDATE or INSERT)
const toUpdate: PosMaster[] = [];
const toInsert: any[] = [];
// Track draft PosMaster ID to current PosMaster ID mapping for position sync
// Type: Map<draftPosMasterId, [currentPosMasterId, nextHolderId]>
const posMasterMapping: Map<string, [string, string | null | undefined]> = new Map();
for (const draftPos of posMasterDraft) {
const current = currentByDNA.get(draftPos.ancestorDNA);
// Map organization IDs using new IDs from Part 1
const orgRootId = this.resolveOrgId(draftPos.orgRootId ?? null, allMappings.orgRoot);
const orgChild1Id = this.resolveOrgId(draftPos.orgChild1Id ?? null, allMappings.orgChild1);
const orgChild2Id = this.resolveOrgId(draftPos.orgChild2Id ?? null, allMappings.orgChild2);
const orgChild3Id = this.resolveOrgId(draftPos.orgChild3Id ?? null, allMappings.orgChild3);
const orgChild4Id = this.resolveOrgId(draftPos.orgChild4Id ?? null, allMappings.orgChild4);
if (current) {
// UPDATE existing position
Object.assign(current, {
createdAt: draftPos.createdAt,
createdUserId: draftPos.createdUserId,
createdFullName: draftPos.createdFullName,
lastUpdatedAt: new Date(),
lastUpdateUserId: request.user.sub,
lastUpdateFullName: request.user.name,
posMasterNoPrefix: draftPos.posMasterNoPrefix,
posMasterNoSuffix: draftPos.posMasterNoSuffix,
posMasterNo: draftPos.posMasterNo,
posMasterOrder: draftPos.posMasterOrder,
orgRootId,
orgChild1Id,
orgChild2Id,
orgChild3Id,
orgChild4Id,
current_holderId: draftPos.next_holderId,
isSit: draftPos.isSit,
reason: draftPos.reason,
isDirector: draftPos.isDirector,
isStaff: draftPos.isStaff,
positionSign: draftPos.positionSign,
statusReport: "DONE",
isCondition: draftPos.isCondition,
conditionReason: draftPos.conditionReason,
});
toUpdate.push(current);
if (draftPos.next_holderId === null) {
await SavePosMasterHistoryOfficer(queryRunner, draftPos.ancestorDNA, null, null);
}
// Track mapping for position sync
posMasterMapping.set(draftPos.id, [current.id, draftPos.next_holderId]);
} else {
// INSERT new position
const newPosMaster = queryRunner.manager.create(PosMaster, {
...draftPos,
id: undefined,
orgRevisionId: currentRevisionId,
orgRootId,
orgChild1Id,
orgChild2Id,
orgChild3Id,
orgChild4Id,
current_holderId: draftPos.next_holderId,
statusReport: "DONE",
});
toInsert.push(newPosMaster);
}
}
// Batch save updates and inserts
if (toUpdate.length > 0) {
await queryRunner.manager.save(toUpdate);
}
if (toInsert.length > 0) {
const saved = await queryRunner.manager.save(toInsert);
// Track mapping for newly inserted posMasters
// saved is an array, map each to its draft ID
if (Array.isArray(saved)) {
for (let i = 0; i < saved.length; i++) {
const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i];
if (draftPos && saved[i]) {
posMasterMapping.set(draftPos.id, [saved[i].id, draftPos.next_holderId]);
}
}
}
}
// 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 = {
message: "ย้ายโครงสร้างสำเร็จ",
organization: {
orgRoot: orgSyncStats.orgRoot,
orgChild1: orgSyncStats.orgChild1,
orgChild2: orgSyncStats.orgChild2,
orgChild3: orgSyncStats.orgChild3,
orgChild4: orgSyncStats.orgChild4,
},
positionMaster: {
deleted: toDelete.length,
updated: toUpdate.length,
inserted: toInsert.length,
},
position: positionSyncStats,
};
await queryRunner.commitTransaction();
return new HttpSuccess(summary);
} catch (error) {
console.error("Error moving draft to current:", error);
await queryRunner.rollbackTransaction();
@ -8465,7 +8463,7 @@ export class OrganizationController extends Controller {
for (const draft of toInsert) {
const newNode: any = queryRunner.manager.create(entityClass, {
...draft,
id: undefined,
id: draft.id,
orgRevisionId: currentRevisionId,
});