hrms-api-org/docs/move-draft-to-current.md

357 lines
10 KiB
Markdown
Raw Permalink Normal View History

# 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<string, string>; // ancestorDNA → current ID
byDraftId: Map<string, string>; // 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<void> {
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