# Move Draft to Current - Differential Sync Implementation ## Overview This document describes the implementation of the improved `move-draft-to-current` function in `OrganizationController.ts`. The function synchronizes organization structure and position data from the **Draft Revision** to the **Current Revision** using a differential sync approach (instead of the previous "delete all and insert all" method). **API Endpoint:** `POST /api/v1/org/move-draft-to-current/{rootDnaId}` --- ## Architecture ### Data Models The organization structure consists of 5 hierarchical levels: ``` OrgRoot (Level 0) └── OrgChild1 (Level 1) └── OrgChild2 (Level 2) └── OrgChild3 (Level 3) └── OrgChild4 (Level 4) ``` Each level has: - Organization nodes with `ancestorDNA` for hierarchical tracking - Foreign key relationships to parent levels - Associated position records (`PosMaster`) ### Type Definitions Located in `src/interfaces/OrgMapping.ts`: ```typescript interface OrgIdMapping { byAncestorDNA: Map; // ancestorDNA → current ID byDraftId: Map; // draft ID → current ID } interface AllOrgMappings { orgRoot: OrgIdMapping; orgChild1: OrgIdMapping; orgChild2: OrgIdMapping; orgChild3: OrgIdMapping; orgChild4: OrgIdMapping; } ``` --- ## Implementation Workflow ### Phase 0: Preparation 1. **Get Revision IDs** - Fetch Draft Revision (`orgRevisionIsDraft: true`) - Fetch Current Revision (`orgRevisionIsCurrent: true`) 2. **Validate rootDnaId** - Check if rootDnaId exists in Draft Revision - Return error if not found ### Phase 1: Sync Organization Structure (Bottom-Up) **Processing Order:** `OrgChild4 → OrgChild3 → OrgChild2 → OrgChild1 → OrgRoot` **Why Bottom-Up?** Child nodes have no dependent children (only parent references), allowing safe deletion without FK violations. #### For Each Organization Level The `syncOrgLevel()` helper performs: 1. **FETCH** - Get all draft and current nodes under `rootDnaId` ```typescript where: { ancestorDNA: Like(`${rootDnaId}%`) } // All descendants ``` 2. **DELETE** - Remove current nodes not in draft - Cascade delete positions first (via `cascadeDeletePositions()`) - Delete the organization node 3. **UPDATE** - Update nodes that exist in both (matched by `ancestorDNA`) - Map parent IDs using `parentMappings` - Preserve original node ID 4. **INSERT** - Add draft nodes not in current - Create new node with mapped parent IDs - Return new ID for tracking 5. **RETURN** - Return `OrgIdMapping` for next level **Result:** `allMappings` contains draft ID → current ID mappings for all org levels ### Phase 2: Sync Position Data #### Step 2.1: Clear current_holderId ```typescript // Clear holders for positions that will have new holders await queryRunner.manager.update(PosMaster, { current_holderId: In(nextHolderIds) }, { current_holderId: null, isSit: false } ) ``` #### Step 2.2: Fetch Draft and Current Positions - Get draft positions using `draftOrgIds` from `allMappings` - Get current positions using `currentOrgIds` from `allMappings` #### Step 2.3: Batch DELETE ```typescript // Delete current positions not in draft (cascade delete positions first) await queryRunner.manager.delete(Position, { posMasterId: In(toDeleteIds) }) await queryRunner.manager.delete(PosMaster, toDeleteIds) ``` #### Step 2.4: Process UPDATE or INSERT For each draft position: 1. Map organization IDs using `resolveOrgId()` 2. If exists in current → **UPDATE** 3. If not exists → **INSERT** 4. Track `draftPosMasterId → currentPosMasterId` mapping #### Step 2.5: Sync Position Table For each mapped PosMaster: ```typescript await syncPositionsForPosMaster( queryRunner, draftPosMasterId, currentPosMasterId, draftRevisionId, currentRevisionId ) ``` --- ## Helper Functions ### `resolveOrgId(draftId, mapping)` Maps a draft organization ID to its current ID. ```typescript private resolveOrgId( draftId: string | null, mapping: OrgIdMapping ): string | null { if (!draftId) return null; return mapping.byDraftId.get(draftId) ?? null; } ``` ### `cascadeDeletePositions(queryRunner, node, entityClass)` Deletes positions associated with an organization node before deleting the node itself. ```typescript private async cascadeDeletePositions( queryRunner: any, node: any, entityClass: any ): Promise { const whereClause = { orgRevisionId: node.orgRevisionId }; // Set FK field based on entity type if (entityClass === OrgRoot) whereClause.orgRootId = node.id; else if (entityClass === OrgChild1) whereClause.orgChild1Id = node.id; // ... etc await queryRunner.manager.delete(PosMaster, whereClause); } ``` ### `syncOrgLevel(...)` Generic differential sync for each organization level. **Parameters:** - `queryRunner` - Database query runner - `entityClass` - Organization entity class (OrgRoot, OrgChild1, etc.) - `repository` - Repository for the entity - `draftRevisionId` - Draft revision ID - `currentRevisionId` - Current revision ID - `rootDnaId` - Root DNA ID to sync under - `parentMappings` - Mappings from child levels (for FK resolution) **Returns:** `OrgIdMapping` for this level ### `syncPositionsForPosMaster(...)` Syncs positions for a PosMaster record. **Parameters:** - `queryRunner` - Database query runner - `draftPosMasterId` - Draft PosMaster ID - `currentPosMasterId` - Current PosMaster ID - `draftRevisionId` - Draft revision ID - `currentRevisionId` - Current revision ID **Process:** 1. Fetch draft and current positions 2. Delete current positions not in draft (by `orderNo`) 3. Update existing positions 4. Insert new positions --- ## Transaction Management All operations are wrapped in a transaction: ```typescript const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // ... all sync operations await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "..."); } ``` --- ## Benefits of Differential Sync | Aspect | Old Approach (Delete All + Insert) | New Approach (Differential Sync) | |--------|-----------------------------------|----------------------------------| | **ID Preservation** | All IDs changed | Unchanged nodes keep original IDs | | **Performance** | N deletes + N inserts | Only changed data processed | | **Tracking** | Cannot track what changed | Can track additions/updates/deletes | | **Data Integrity** | Higher risk of data loss | Better integrity with cascade deletes | | **Scalability** | Poor for large datasets | Efficient with batch operations | --- ## Database Schema Relationships ``` orgRevision ├── orgRoot (FK: orgRevisionId) │ ├── orgChild1 (FK: orgRootId, orgRevisionId) │ │ ├── orgChild2 (FK: orgChild1Id, orgRootId, orgRevisionId) │ │ │ ├── orgChild3 (FK: orgChild2Id, orgChild1Id, orgRootId, orgRevisionId) │ │ │ │ └── orgChild4 (FK: orgChild3Id, orgChild2Id, orgChild1Id, orgRootId, orgRevisionId) │ │ │ │ │ │ │ └── posMaster (FK: orgRootId, orgChild1Id, orgChild2Id, orgChild3Id, orgChild4Id) │ │ │ └── position (FK: posMasterId) │ │ │ │ │ └── posMaster │ │ │ └── posMaster │ └── (current revision similar structure) ``` --- ## Error Handling | Error Condition | Response | |----------------|----------| | Draft/Current revision not found | 404 NOT_FOUND | | rootDnaId not found in draft | 404 NOT_FOUND | | No positions in draft structure | 404 NOT_FOUND | | Database error during sync | 500 INTERNAL_SERVER_ERROR (rollback) | --- ## Files Modified/Created ``` src/ ├── interfaces/ │ └── OrgMapping.ts [NEW] Type definitions ├── controllers/ │ └── OrganizationController.ts [MODIFIED] moveDraftToCurrent function + helpers docs/ └── move-draft-to-current.md [NEW] This documentation ``` --- ## Testing Considerations ### Unit Tests - Test `syncOrgLevel()` for each org level with various scenarios - Test `resolveOrgId()` mapping function - Test `cascadeDeletePositions()` function - Test `syncPositionsForPosMaster()` function ### Integration Tests 1. **Empty Draft** - Sync with no draft data 2. **Full Replacement** - All nodes changed 3. **Partial Update** - Some nodes added, some updated, some deleted 4. **Position Sync** - Verify position table syncs correctly 5. **Foreign Key Constraints** - Verify all FK relationships maintained ### Manual Testing Flow 1. Create draft structure with various changes: - Add new department - Modify existing department - Remove existing department - Add/modify/remove positions 2. Call `move-draft-to-current` API 3. Verify: - New departments appear in current - Modified departments are updated (not recreated) - Removed departments are gone (with positions cascade deleted) - Positions have correct new org IDs - Position table records sync correctly - All foreign key constraints satisfied --- ## Performance Optimization - **Batch Operations** - Use `In()` clause for multiple IDs - **Map Lookups** - Use `Map` for O(1) lookups instead of array searches - **Bottom-Up Processing** - Minimize FK constraint checks - **Parallel Queries** - Use `Promise.all()` for independent queries --- ## Future Improvements 1. **Parallel Processing** - Process independent org branches in parallel 2. **Incremental Sync** - Only sync changed subtrees 3. **Caching** - Cache org mappings for repeated operations 4. **Audit Log** - Track all changes for audit purposes 5. **Validation** - Add pre-sync validation to catch errors early --- ## References - TypeORM Documentation: https://typeorm.io/ - TSOA Documentation: https://tsoa-community.github.io/ - Project Repository: [Internal Git] --- **Last Updated:** 2025-02-09 **Author:** Claude Code **Version:** 1.0.0