internal/functions/invoke-adoclassificationnodemigrationrecursive.ps1


<#
    .SYNOPSIS
        Migrates classification nodes (Areas or Iterations) recursively from source to target Azure DevOps organization.
         
    .DESCRIPTION
        This function handles the migration of classification nodes by checking for existing nodes in the target
        organization and updating them if necessary. It preserves the hierarchy and attributes of the nodes.
         
    .PARAMETER SourceOrganization
        The name of the source Azure DevOps organization.
    .PARAMETER TargetOrganization
        The name of the target Azure DevOps organization.
    .PARAMETER SourceToken
        Personal Access Token for the source Azure DevOps organization with work item tracking permissions.
    .PARAMETER TargetToken
        Personal Access Token for the target Azure DevOps organization with work item tracking permissions.
    .PARAMETER TargetProjectName
        The name of the target Azure DevOps project.
    .PARAMETER SourceNode
        The source classification node to migrate.
    .PARAMETER TargetParentNodes
        The collection of target parent nodes to check for existing nodes.
    .PARAMETER StructureGroup
        The classification node type: 'Areas' or 'Iterations'.
    .PARAMETER ApiVersion
        The version of the Azure DevOps REST API to use.
    .PARAMETER ParentPath
        The path of the parent node for building the full path.
         
    .EXAMPLE
        Invoke-ADOClassificationNodeMigrationRecursive -SourceOrganization "sourceorg" -TargetOrganization "targetorg" -SourceToken $sourcePat -TargetToken $targetPat -TargetProjectName "TargetProject" -SourceNode $sourceNode -TargetParentNodes $targetParentNodes -StructureGroup 'Areas' -ApiVersion '7.2-preview.2'
         
        Migrates the specified source classification node and its children recursively to the target project.
         
    .NOTES
        This function is part of the ADO Tools module and adheres to the conventions used in the module for logging, error handling, and API interaction.
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-ADOClassificationNodeMigrationRecursive {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param(
        [Parameter(Mandatory)][string]$SourceOrganization,
        [Parameter(Mandatory)][string]$TargetOrganization,
        [Parameter(Mandatory)][string]$SourceToken,
        [Parameter(Mandatory)][string]$TargetToken,
        [Parameter(Mandatory)][string]$TargetProjectName,
        [Parameter(Mandatory)][pscustomobject]$SourceNode,
        [Parameter()][array]$TargetParentNodes,
        [Parameter(Mandatory)][string]$StructureGroup,
        [Parameter(Mandatory)][string]$ApiVersion,
        [Parameter()][string]$ParentPath = ''
    )

    $migratedCount = 0
    $skippedCount = 0
    $errorCount = 0

    try {
        # Check if node already exists in target
        $existingNode = $null
        if ($TargetParentNodes) {
            $existingNode = $TargetParentNodes | Where-Object { $_.name -eq $SourceNode.name } | Select-Object -First 1
        }

        $currentPath = if ($ParentPath) { "$ParentPath/$($SourceNode.name)" } else { $SourceNode.name }

        if ($existingNode) {
            Write-PSFMessage -Level Verbose -Message "$StructureGroup node '$($SourceNode.name)' already exists at path '$currentPath'. Checking for updates..."
            
            # Check if we need to update attributes (for iterations with dates)
            $needsUpdate = $false
            if ($SourceNode.attributes -and $SourceNode.attributes.PSObject.Properties.Count -gt 0) {
                if (-not $existingNode.attributes) {
                    $needsUpdate = $true
                } else {
                    # Compare key attributes
                    foreach ($prop in $SourceNode.attributes.PSObject.Properties) {
                        if (-not $existingNode.attributes.PSObject.Properties[$prop.Name] -or 
                            $existingNode.attributes.($prop.Name) -ne $prop.Value) {
                            $needsUpdate = $true
                            break
                        }
                    }
                }
            }

            if ($needsUpdate) {
                Write-PSFMessage -Level Host -Message "Updating $StructureGroup node '$($SourceNode.name)' with new attributes..."
                
                $updateParams = @{
                    Organization = $TargetOrganization
                    Token = $TargetToken
                    Project = $TargetProjectName
                    StructureGroup = $StructureGroup
                    Path = $currentPath
                    Name = $SourceNode.name
                    ApiVersion = $ApiVersion
                }
                
                # Add attributes if present (typically for iterations with start/finish dates)
                if ($SourceNode.attributes) {
                    if ($SourceNode.attributes.startDate) {
                        $updateParams.StartDate = $SourceNode.attributes.startDate
                    }
                    if ($SourceNode.attributes.finishDate) {
                        $updateParams.FinishDate = $SourceNode.attributes.finishDate
                    }
                    if ($SourceNode.attributes -and $SourceNode.attributes.PSObject.Properties.Count -gt 0) {
                        # Convert PSCustomObject to Hashtable
                        $attributesHashtable = @{}
                        foreach ($property in $SourceNode.attributes.PSObject.Properties) {
                            $attributesHashtable[$property.Name] = $property.Value
                        }
                        $updateParams.Attributes = $attributesHashtable
                    }
                }
                
                $updated = Update-ADOClassificationNode @updateParams
                if ($updated) {
                    $migratedCount++
                    Write-PSFMessage -Level Verbose -Message "Successfully updated $StructureGroup node '$($SourceNode.name)'"
                } else {
                    $errorCount++
                }
            } else {
                Write-PSFMessage -Level Verbose -Message "$StructureGroup node '$($SourceNode.name)' is up to date. Skipping..."
                $skippedCount++
            }
            
            # Use existing node for children migration
            $targetNode = $existingNode
        } else {
            Write-PSFMessage -Level Host -Message "Creating $StructureGroup node '$($SourceNode.name)' at path '$currentPath'..."
            
            $addParams = @{
                Organization = $TargetOrganization
                Token = $TargetToken
                Project = $TargetProjectName
                StructureGroup = $StructureGroup
                Name = $SourceNode.name
                ApiVersion = $ApiVersion
            }
            
            # Set path for parent if not empty
            if ($ParentPath) {
                $addParams.Path = $ParentPath
            }
            
            # Add attributes if present (typically for iterations with start/finish dates)
            if ($SourceNode.attributes) {
                if ($SourceNode.attributes.startDate) {
                    $addParams.StartDate = $SourceNode.attributes.startDate
                }
                if ($SourceNode.attributes.finishDate) {
                    $addParams.FinishDate = $SourceNode.attributes.finishDate
                }
                if ($SourceNode.attributes -and $SourceNode.attributes.PSObject.Properties.Count -gt 0) {
                    # Convert PSCustomObject to Hashtable
                    $attributesHashtable = @{}
                    foreach ($property in $SourceNode.attributes.PSObject.Properties) {
                        $attributesHashtable[$property.Name] = $property.Value
                    }
                    $addParams.Attributes = $attributesHashtable
                }
            }
            
            $targetNode = Add-ADOClassificationNode @addParams
            
            if ($targetNode) {
                $migratedCount++
                Write-PSFMessage -Level Verbose -Message "Successfully created $StructureGroup node '$($SourceNode.name)'"
            } else {
                $errorCount++
                Write-PSFMessage -Level Error -Message "Failed to create $StructureGroup node '$($SourceNode.name)'"
                return @{ Migrated = $migratedCount; Skipped = $skippedCount; Errors = $errorCount }
            }
        }

        # Migrate children recursively
        if ($SourceNode.children -and $SourceNode.children.Count -gt 0) {
            Write-PSFMessage -Level Verbose -Message "Migrating $($SourceNode.children.Count) child nodes for '$($SourceNode.name)'..."
            
            foreach ($childNode in $SourceNode.children) {
                $childResult = Invoke-ADOClassificationNodeMigrationRecursive -SourceOrganization $SourceOrganization -TargetOrganization $TargetOrganization -SourceToken $SourceToken -TargetToken $TargetToken -TargetProjectName $TargetProjectName -SourceNode $childNode -TargetParentNodes $targetNode.children -StructureGroup $StructureGroup -ApiVersion $ApiVersion -ParentPath $currentPath
                $migratedCount += $childResult.Migrated
                $skippedCount += $childResult.Skipped
                $errorCount += $childResult.Errors
            }
        }

    } catch {
        Write-PSFMessage -Level Error -Message "Failed to migrate $StructureGroup node '$($SourceNode.name)': $($_.Exception.Message)"
        $errorCount++
    }

    return @{
        Migrated = $migratedCount
        Skipped = $skippedCount
        Errors = $errorCount
    }
}