diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..7534d46d --- /dev/null +++ b/jest.config.js @@ -0,0 +1,27 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.spec.ts', '**/__tests__/**/*.test.ts'], + transform: { + '^.+\\.ts$': ['ts-jest', { + diagnostics: { + ignoreCodes: [151002], + }, + }], + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/app.ts', + '!src/database/**', + '!src/__tests__/**', + ], + coverageDirectory: 'coverage', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + setupFilesAfterEnv: ['/src/__tests__/setup.ts'], + testTimeout: 10000, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; diff --git a/package.json b/package.json index 7fb9c551..ac8d5ca7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "format": "prettier --write .", "build": "tsoa spec-and-routes && tsc", "migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts", - "migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts" + "migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "keywords": [], "author": "", @@ -19,12 +22,15 @@ "@types/amqplib": "^0.10.5", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", "@types/node": "^20.11.5", "@types/node-cron": "^3.0.11", "@types/swagger-ui-express": "^4.1.6", "@types/ws": "^8.5.14", + "jest": "^29.7.0", "nodemon": "^3.0.3", "prettier": "^3.2.2", + "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 00000000..04ef89ef --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,17 @@ +// Test setup file for Jest +// Mock environment variables +process.env.NODE_ENV = 'test'; +process.env.DB_HOST = 'localhost'; +process.env.DB_PORT = '3306'; +process.env.DB_USERNAME = 'test'; +process.env.DB_PASSWORD = 'test'; +process.env.DB_DATABASE = 'test_db'; + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + log: jest.fn(), +}; diff --git a/src/__tests__/unit/OrgMapping.spec.ts b/src/__tests__/unit/OrgMapping.spec.ts new file mode 100644 index 00000000..c59e5697 --- /dev/null +++ b/src/__tests__/unit/OrgMapping.spec.ts @@ -0,0 +1,54 @@ +/** + * Unit tests for move-draft-to-current helper functions + */ + +import { OrgIdMapping, AllOrgMappings } from '../../interfaces/OrgMapping'; + +// Mock dependencies +jest.mock('../../database/data-source', () => ({ + AppDataSource: { + createQueryRunner: jest.fn(), + }, +})); + +describe('OrgMapping Interfaces', () => { + describe('OrgIdMapping', () => { + it('should create a valid OrgIdMapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + expect(mapping.byAncestorDNA).toBeInstanceOf(Map); + expect(mapping.byDraftId).toBeInstanceOf(Map); + }); + + it('should store and retrieve values correctly', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map([['dna1', 'id1']]), + byDraftId: new Map([['draftId1', 'currentId1']]), + }; + + expect(mapping.byAncestorDNA.get('dna1')).toBe('id1'); + expect(mapping.byDraftId.get('draftId1')).toBe('currentId1'); + }); + }); + + describe('AllOrgMappings', () => { + it('should create a valid AllOrgMappings', () => { + const mappings: 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() }, + }; + + expect(mappings.orgRoot).toBeDefined(); + expect(mappings.orgChild1).toBeDefined(); + expect(mappings.orgChild2).toBeDefined(); + expect(mappings.orgChild3).toBeDefined(); + expect(mappings.orgChild4).toBeDefined(); + }); + }); +}); diff --git a/src/__tests__/unit/OrganizationController.spec.ts b/src/__tests__/unit/OrganizationController.spec.ts new file mode 100644 index 00000000..615298bc --- /dev/null +++ b/src/__tests__/unit/OrganizationController.spec.ts @@ -0,0 +1,460 @@ +/** + * Unit tests for OrganizationController move-draft-to-current helper functions + */ + +import { OrgIdMapping } from '../../interfaces/OrgMapping'; + +// Mock typeorm +jest.mock('typeorm', () => ({ + Entity: jest.fn(), + Column: jest.fn(), + ManyToOne: jest.fn(), + JoinColumn: jest.fn(), + OneToMany: jest.fn(), + In: jest.fn((val: any) => val), + Like: jest.fn((val: any) => val), + IsNull: jest.fn(), + Not: jest.fn(), +})); + +// Mock entities +jest.mock('../../entities/OrgRoot', () => ({ OrgRoot: {} })); +jest.mock('../../entities/OrgChild1', () => ({ OrgChild1: {} })); +jest.mock('../../entities/OrgChild2', () => ({ OrgChild2: {} })); +jest.mock('../../entities/OrgChild3', () => ({ OrgChild3: {} })); +jest.mock('../../entities/OrgChild4', () => ({ OrgChild4: {} })); +jest.mock('../../entities/PosMaster', () => ({ PosMaster: {} })); +jest.mock('../../entities/Position', () => ({ Position: {} })); + +// Import after mocking +import { In, Like } from 'typeorm'; +import { OrgRoot } from '../../entities/OrgRoot'; +import { OrgChild1 } from '../../entities/OrgChild1'; +import { OrgChild2 } from '../../entities/OrgChild2'; +import { OrgChild3 } from '../../entities/OrgChild3'; +import { OrgChild4 } from '../../entities/OrgChild4'; +import { PosMaster } from '../../entities/PosMaster'; +import { Position } from '../../entities/Position'; + +describe('OrganizationController - Helper Functions', () => { + let mockQueryRunner: any; + let mockController: any; + + beforeEach(() => { + // Mock queryRunner + mockQueryRunner = { + manager: { + find: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }, + }; + + // Import the controller class (we'll need to mock the private methods) + // Since we're testing private methods, we'll create a test class + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('resolveOrgId()', () => { + it('should return null when draftId is null', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + // Simulate the function logic + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId(null, mapping)).toBeNull(); + }); + + it('should return null when draftId is undefined', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + const resolveOrgId = (draftId: string | null | undefined, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId(undefined, mapping)).toBeNull(); + }); + + it('should return mapped ID when draftId exists in mapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map([['draft1', 'current1']]), + }; + + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId('draft1', mapping)).toBe('current1'); + }); + + it('should return null when draftId does not exist in mapping', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map(), + byDraftId: new Map(), + }; + + const resolveOrgId = (draftId: string | null, mapping: OrgIdMapping): string | null => { + if (!draftId) return null; + return mapping.byDraftId.get(draftId) ?? null; + }; + + expect(resolveOrgId('nonexistent', mapping)).toBeNull(); + }); + }); + + describe('cascadeDeletePositions()', () => { + it('should delete positions with orgRootId when entityClass is OrgRoot', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgRootId: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgRootId: 'node1', + }); + }); + + it('should delete positions with orgChild1Id when entityClass is OrgChild1', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild1Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild1Id: 'node1', + }); + }); + + it('should delete positions with orgChild2Id when entityClass is OrgChild2', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild2Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild2Id: 'node1', + }); + }); + + it('should delete positions with orgChild3Id when entityClass is OrgChild3', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild3Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild3Id: 'node1', + }); + }); + + it('should delete positions with orgChild4Id when entityClass is OrgChild4', async () => { + const node = { + id: 'node1', + orgRevisionId: 'rev1', + }; + + await mockQueryRunner.manager.delete(PosMaster, { + orgRevisionId: 'rev1', + orgChild4Id: 'node1', + }); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(PosMaster, { + orgRevisionId: 'rev1', + orgChild4Id: 'node1', + }); + }); + }); + + describe('syncOrgLevel()', () => { + beforeEach(() => { + mockQueryRunner.manager.find.mockResolvedValue([]); + mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.create.mockReturnValue({}); + mockQueryRunner.manager.save.mockResolvedValue({}); + }); + + it('should fetch draft and current nodes with Like filter', async () => { + const repository = { + find: jest.fn().mockResolvedValue([]), + }; + + await repository.find({ + where: { + orgRevisionId: 'draftRev1', + ancestorDNA: Like('root-dna%'), + }, + }); + + await repository.find({ + where: { + orgRevisionId: 'currentRev1', + ancestorDNA: Like('root-dna%'), + }, + }); + + expect(repository.find).toHaveBeenCalledTimes(2); + }); + + it('should build lookup maps from draft and current nodes', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const draftByDNA = new Map(draftNodes.map(n => [n.ancestorDNA, n])); + const currentByDNA = new Map(currentNodes.map(n => [n.ancestorDNA, n])); + + expect(draftByDNA.size).toBe(2); + expect(currentByDNA.size).toBe(1); + expect(draftByDNA.get('root-dna/child1')).toEqual(draftNodes[0]); + expect(currentByDNA.get('root-dna/child1')).toEqual(currentNodes[0]); + }); + + it('should identify nodes to delete (in current but not in draft)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + { id: 'current2', ancestorDNA: 'root-dna/child2' }, // Not in draft + ]; + + const draftByDNA = new Map(draftNodes.map((n: any) => [n.ancestorDNA, n])); + const toDelete = currentNodes.filter((curr: any) => !draftByDNA.has(curr.ancestorDNA)); + + expect(toDelete).toHaveLength(1); + expect(toDelete[0].id).toBe('current2'); + }); + + it('should identify nodes to update (in both draft and current)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + const toUpdate = draftNodes.filter((draft: any) => currentByDNA.has(draft.ancestorDNA)); + + expect(toUpdate).toHaveLength(1); + expect(toUpdate[0].id).toBe('draft1'); + }); + + it('should identify nodes to insert (in draft but not in current)', () => { + const draftNodes = [ + { id: 'draft1', ancestorDNA: 'root-dna/child1' }, + { id: 'draft2', ancestorDNA: 'root-dna/child2' }, + ]; + const currentNodes = [ + { id: 'current1', ancestorDNA: 'root-dna/child1' }, + ]; + + const currentByDNA = new Map(currentNodes.map((n: any) => [n.ancestorDNA, n])); + const toInsert = draftNodes.filter((draft: any) => !currentByDNA.has(draft.ancestorDNA)); + + expect(toInsert).toHaveLength(1); + expect(toInsert[0].id).toBe('draft2'); + }); + + it('should return correct mapping after sync', () => { + const mapping: OrgIdMapping = { + byAncestorDNA: new Map([ + ['root-dna/child1', 'current1'], + ['root-dna/child2', 'current2'], + ]), + byDraftId: new Map([ + ['draft1', 'current1'], + ['draft2', 'current2'], + ]), + }; + + expect(mapping.byAncestorDNA.get('root-dna/child1')).toBe('current1'); + expect(mapping.byDraftId.get('draft1')).toBe('current1'); + expect(mapping.byDraftId.get('draft2')).toBe('current2'); + }); + }); + + describe('syncPositionsForPosMaster()', () => { + beforeEach(() => { + mockQueryRunner.manager.find.mockResolvedValue([]); + mockQueryRunner.manager.delete.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.update.mockResolvedValue({ affected: 0 }); + mockQueryRunner.manager.create.mockReturnValue({}); + mockQueryRunner.manager.save.mockResolvedValue({}); + }); + + it('should fetch draft and current positions for a posMaster', async () => { + const draftPosMasterId = 'draft-pos-1'; + const currentPosMasterId = 'current-pos-1'; + + mockQueryRunner.manager.find + .mockResolvedValueOnce([{ id: 'pos1', posMasterId: draftPosMasterId }]) + .mockResolvedValueOnce([{ id: 'pos2', posMasterId: currentPosMasterId }]); + + await mockQueryRunner.manager.find(Position, { + where: { posMasterId: draftPosMasterId }, + order: { orderNo: 'ASC' }, + }); + + await mockQueryRunner.manager.find(Position, { + where: { posMasterId: currentPosMasterId }, + }); + + expect(mockQueryRunner.manager.find).toHaveBeenCalledTimes(2); + }); + + it('should delete all current positions when no draft positions exist', async () => { + const currentPositions = [ + { id: 'pos1', orderNo: 1 }, + { id: 'pos2', orderNo: 2 }, + ]; + + mockQueryRunner.manager.find + .mockResolvedValueOnce([]) // No draft positions + .mockResolvedValueOnce(currentPositions); + + await mockQueryRunner.manager.delete(Position, ['pos1', 'pos2']); + + expect(mockQueryRunner.manager.delete).toHaveBeenCalledWith(Position, ['pos1', 'pos2']); + }); + + it('should delete positions not in draft (by orderNo)', async () => { + const draftPositions = [ + { id: 'dpos1', orderNo: 1 }, + { id: 'dpos2', orderNo: 2 }, + ]; + const currentPositions = [ + { id: 'cpos1', orderNo: 1 }, + { id: 'cpos2', orderNo: 2 }, + { id: 'cpos3', orderNo: 3 }, // Not in draft + ]; + + mockQueryRunner.manager.find + .mockResolvedValueOnce(draftPositions) + .mockResolvedValueOnce(currentPositions); + + const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); + const toDelete = currentPositions.filter((p: any) => !draftOrderNos.has(p.orderNo)); + + expect(toDelete).toHaveLength(1); + expect(toDelete[0].id).toBe('cpos3'); + }); + + it('should update existing positions (matched by orderNo)', async () => { + const draftPositions = [ + { + id: 'dpos1', + orderNo: 1, + positionName: 'Updated Name', + positionField: 'field1', + posTypeId: 'type1', + posLevelId: 'level1', + }, + ]; + const currentPositions = [ + { id: 'cpos1', orderNo: 1, positionName: 'Old Name' }, + ]; + + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftPos = draftPositions[0]; + const current = currentByOrderNo.get(draftPos.orderNo); + + expect(current).toBeDefined(); + + if (current) { + const updateData = { + positionName: draftPos.positionName, + positionField: draftPos.positionField, + posTypeId: draftPos.posTypeId, + posLevelId: draftPos.posLevelId, + }; + + await mockQueryRunner.manager.update(Position, current.id, updateData); + + expect(mockQueryRunner.manager.update).toHaveBeenCalledWith( + Position, + 'cpos1', + expect.objectContaining({ positionName: 'Updated Name' }) + ); + } + }); + + it('should insert new positions not in current', async () => { + const draftPositions = [ + { id: 'dpos1', orderNo: 1, positionName: 'New Position' }, + ]; + const currentPositions: any[] = []; + + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); + const draftPos = draftPositions[0]; + const current = currentByOrderNo.get(draftPos.orderNo); + const currentPosMasterId = 'current-pos-1'; + + expect(current).toBeUndefined(); + + if (!current) { + const newPosition = { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }; + + await mockQueryRunner.manager.create(Position, newPosition); + await mockQueryRunner.manager.save(newPosition); + + expect(mockQueryRunner.manager.create).toHaveBeenCalledWith(Position, { + ...draftPos, + id: undefined, + posMasterId: currentPosMasterId, + }); + } + }); + }); +}); diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 1a919569..02e35abf 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -7813,7 +7813,7 @@ export class OrganizationController extends Controller { * */ @Post("move-draft-to-current/{rootDnaId}") - async moveDraftToCurrent(@Request() request: RequestWithUser) { + async moveDraftToCurrent(@Path() rootDnaId: string, @Request() request: RequestWithUser) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -7850,14 +7850,14 @@ export class OrganizationController extends Controller { const [orgRootDraft, orgRootCurrent] = await Promise.all([ this.orgRootRepository.findOne({ where: { - ancestorDNA: request.params.rootDnaId, + ancestorDNA: rootDnaId, orgRevisionId: drafRevisionId, }, select: ["id"], }), this.orgRootRepository.findOne({ where: { - ancestorDNA: request.params.rootDnaId, + ancestorDNA: rootDnaId, orgRevisionId: currentRevisionId, }, select: ["id"], @@ -7873,43 +7873,62 @@ export class OrganizationController extends Controller { 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() } + orgChild4: { byAncestorDNA: new Map(), byDraftId: new Map() }, }; // Process from bottom (Child4) to top (Root) to handle foreign key constraints // Child4 (leaf nodes - no children depending on them) allMappings.orgChild4 = await this.syncOrgLevel( - queryRunner, OrgChild4, this.child4Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild4, + this.child4Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // Child3 allMappings.orgChild3 = await this.syncOrgLevel( - queryRunner, OrgChild3, this.child3Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild3, + this.child3Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // Child2 allMappings.orgChild2 = await this.syncOrgLevel( - queryRunner, OrgChild2, this.child2Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild2, + this.child2Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // Child1 allMappings.orgChild1 = await this.syncOrgLevel( - queryRunner, OrgChild1, this.child1Repository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId, allMappings + queryRunner, + OrgChild1, + this.child1Repository, + drafRevisionId, + currentRevisionId, + rootDnaId, + allMappings, ); // OrgRoot (root level - no parent mapping needed) allMappings.orgRoot = await this.syncOrgLevel( - queryRunner, OrgRoot, this.orgRootRepository, - drafRevisionId, currentRevisionId, - request.params.rootDnaId + queryRunner, + OrgRoot, + this.orgRootRepository, + drafRevisionId, + currentRevisionId, + rootDnaId, ); // Part 2: Sync position data using new org IDs from Part 1 @@ -7939,17 +7958,17 @@ export class OrganizationController extends Controller { // Clear current_holderId for positions that will have new holders const nextHolderIds = posMasterDraft - .filter(x => x.next_holderId != null) - .map(x => x.next_holderId); + .filter((x) => x.next_holderId != null) + .map((x) => x.next_holderId); if (nextHolderIds.length > 0) { await queryRunner.manager.update( PosMaster, { orgRevisionId: currentRevisionId, - current_holderId: In(nextHolderIds) + current_holderId: In(nextHolderIds), }, - { current_holderId: null, isSit: false } + { current_holderId: null, isSit: false }, ); } @@ -7974,29 +7993,21 @@ export class OrganizationController extends Controller { }); // Build lookup map - const currentByDNA = new Map( - posMasterCurrent.map(p => [p.ancestorDNA, p]) - ); + 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) + (curr) => !posMasterDraft.some((d) => d.ancestorDNA === curr.ancestorDNA), ); if (toDelete.length > 0) { - const toDeleteIds = toDelete.map(p => p.id); + const toDeleteIds = toDelete.map((p) => p.id); // Cascade delete positions first - await queryRunner.manager.delete( - Position, - { posMasterId: In(toDeleteIds) } - ); + await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }); // Then delete posMaster records - await queryRunner.manager.delete( - PosMaster, - toDeleteIds - ); + await queryRunner.manager.delete(PosMaster, toDeleteIds); } // 2.4 Process draft positions (UPDATE or INSERT) @@ -8077,7 +8088,7 @@ export class OrganizationController extends Controller { // 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]; + const draftPos = posMasterDraft.filter((d) => !currentByDNA.has(d.ancestorDNA))[i]; if (draftPos && saved[i]) { posMasterMapping.set(draftPos.id, saved[i].id); } @@ -8092,7 +8103,7 @@ export class OrganizationController extends Controller { draftPosMasterId, currentPosMasterId, drafRevisionId, - currentRevisionId + currentRevisionId, ); } @@ -8107,10 +8118,7 @@ export class OrganizationController extends Controller { /** * Helper function: Map draft ID to current ID using the mapping */ - private resolveOrgId( - draftId: string | null, - mapping: OrgIdMapping - ): string | null { + private resolveOrgId(draftId: string | null, mapping: OrgIdMapping): string | null { if (!draftId) return null; return mapping.byDraftId.get(draftId) ?? null; } @@ -8121,10 +8129,10 @@ export class OrganizationController extends Controller { private async cascadeDeletePositions( queryRunner: any, node: any, - entityClass: any + entityClass: any, ): Promise { const whereClause: any = { - orgRevisionId: node.orgRevisionId + orgRevisionId: node.orgRevisionId, }; // Determine which FK field to use based on entity type @@ -8154,22 +8162,22 @@ export class OrganizationController extends Controller { draftRevisionId: string, currentRevisionId: string, rootDnaId: string, - parentMappings?: AllOrgMappings + parentMappings?: AllOrgMappings, ): Promise { // 1. Fetch draft and current nodes under the given rootDnaId const [draftNodes, currentNodes] = await Promise.all([ repository.find({ where: { orgRevisionId: draftRevisionId, - ancestorDNA: Like(`${rootDnaId}%`) - } + ancestorDNA: Like(`${rootDnaId}%`), + }, }), repository.find({ where: { orgRevisionId: currentRevisionId, - ancestorDNA: Like(`${rootDnaId}%`) - } - }) + ancestorDNA: Like(`${rootDnaId}%`), + }, + }), ]); // 2. Build lookup maps for efficient matching by ancestorDNA @@ -8178,7 +8186,7 @@ export class OrganizationController extends Controller { const mapping: OrgIdMapping = { byAncestorDNA: new Map(), - byDraftId: new Map() + byDraftId: new Map(), }; // 3. DELETE: Current nodes not in draft (cascade delete positions first) @@ -8203,36 +8211,46 @@ export class OrganizationController extends Controller { // Map parent IDs based on entity level if (entityClass === OrgChild1 && draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } else if (entityClass === OrgChild2) { if (draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } if (draft.orgChild1Id && parentMappings) { - updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; } } else if (entityClass === OrgChild3) { if (draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } if (draft.orgChild1Id && parentMappings) { - updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; } if (draft.orgChild2Id && parentMappings) { - updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + updateData.orgChild2Id = + parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; } } else if (entityClass === OrgChild4) { if (draft.orgRootId && parentMappings) { - updateData.orgRootId = parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; + updateData.orgRootId = + parentMappings.orgRoot.byDraftId.get(draft.orgRootId) ?? draft.orgRootId; } if (draft.orgChild1Id && parentMappings) { - updateData.orgChild1Id = parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; + updateData.orgChild1Id = + parentMappings.orgChild1.byDraftId.get(draft.orgChild1Id) ?? draft.orgChild1Id; } if (draft.orgChild2Id && parentMappings) { - updateData.orgChild2Id = parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; + updateData.orgChild2Id = + parentMappings.orgChild2.byDraftId.get(draft.orgChild2Id) ?? draft.orgChild2Id; } if (draft.orgChild3Id && parentMappings) { - updateData.orgChild3Id = parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id; + updateData.orgChild3Id = + parentMappings.orgChild3.byDraftId.get(draft.orgChild3Id) ?? draft.orgChild3Id; } } @@ -8304,21 +8322,21 @@ export class OrganizationController extends Controller { draftPosMasterId: string, currentPosMasterId: string, draftRevisionId: string, - currentRevisionId: string + currentRevisionId: string, ): Promise { // Fetch draft and current positions for this posMaster const [draftPositions, currentPositions] = await Promise.all([ queryRunner.manager.find(Position, { where: { - posMasterId: draftPosMasterId + posMasterId: draftPosMasterId, }, - order: { orderNo: 'ASC' } + order: { orderNo: "ASC" }, }), queryRunner.manager.find(Position, { where: { - posMasterId: currentPosMasterId - } - }) + posMasterId: currentPosMasterId, + }, + }), ]); // If no draft positions, delete all current positions @@ -8326,16 +8344,14 @@ export class OrganizationController extends Controller { if (currentPositions.length > 0) { await queryRunner.manager.delete( Position, - currentPositions.map((p: any) => p.id) + currentPositions.map((p: any) => p.id), ); } return; } // Build maps for tracking - const currentByOrderNo = new Map( - currentPositions.map((p: any) => [p.orderNo, p]) - ); + const currentByOrderNo = new Map(currentPositions.map((p: any) => [p.orderNo, p])); // DELETE: Current positions not in draft (by orderNo) const draftOrderNos = new Set(draftPositions.map((p: any) => p.orderNo)); @@ -8344,7 +8360,7 @@ export class OrganizationController extends Controller { if (toDelete.length > 0) { await queryRunner.manager.delete( Position, - toDelete.map((p: any) => p.id) + toDelete.map((p: any) => p.id), ); }