357 lines
10 KiB
Markdown
357 lines
10 KiB
Markdown
|
|
# 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
|