functions/remove-adoclassificationnode.ps1


<#
    .SYNOPSIS
        Deletes a classification node (Area or Iteration).
    .DESCRIPTION
        Uses the Azure DevOps Work Item Tracking REST API (Classification Nodes - Delete) to
        permanently remove a classification node. Supports optional reclassification of work items
        to a different node before deletion. This operation cannot be undone.
    .OUTPUTS
        System.String
    .PARAMETER Organization
        Azure DevOps organization name (e.g. contoso).
    .PARAMETER Project
        Project name or id.
    .PARAMETER Token
        Personal Access Token (PAT) with vso.work_write scope.
    .PARAMETER StructureGroup
        Areas | Iterations - specifies whether this is an Area or Iteration node.
    .PARAMETER Path
        Path of the classification node to delete (e.g. 'ParentArea\ChildArea').
        Leave empty to target root level nodes.
    .PARAMETER ReclassifyId
        Optional ID of target classification node for reclassifying work items before deletion.
        If not provided, work items may lose their area/iteration assignment.
    .PARAMETER PassThru
        Return the deleted path when deletion succeeds.
    .PARAMETER ApiVersion
        API version (default 7.2-preview.2).
    .PARAMETER Confirm
        Confirmation prompt (SupportsShouldProcess).
    .PARAMETER WhatIf
        Show what would happen without deleting.
    .EXAMPLE
        PS> Remove-ADOClassificationNode -Organization contoso -Project WebApp -Token $pat -StructureGroup Areas -Path "Frontend Team" -Confirm:$false
         
        Deletes the "Frontend Team" area node without confirmation.
         
    .EXAMPLE
        PS> Remove-ADOClassificationNode -Organization contoso -Project WebApp -Token $pat -StructureGroup Iterations -Path "Old Sprint" -ReclassifyId 12345 -PassThru
         
        Deletes "Old Sprint" iteration after reclassifying work items to node ID 12345, returns the deleted path.
         
    .EXAMPLE
        PS> Remove-ADOClassificationNode -Organization contoso -Project WebApp -Token $pat -StructureGroup Areas -Path "Development\Legacy" -WhatIf
         
        Shows what would be deleted without actually performing the deletion.
         
    .LINK
        https://learn.microsoft.com/azure/devops
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Remove-ADOClassificationNode {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([string])]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Organization,

        [Parameter(Mandatory=$true)]
        [string]$Project,

        [Parameter(Mandatory=$true)]
        [string]$Token,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Areas','Iterations')]
        [string]$StructureGroup,

        [Parameter(Mandatory=$true)]
        [string]$Path,

        [Parameter()]
        [int]$ReclassifyId,

        [Parameter()]
        [switch]$PassThru,

        [Parameter()]
        [string]$ApiVersion = '7.2-preview.2'
    )

    begin {
        Write-PSFMessage -Level Verbose -Message "Starting deletion of classification node '$Path' (Group: $StructureGroup) (Org: $Organization / Project: $Project)"
        Invoke-TimeSignal -Start
    }

    process {
        if (Test-PSFFunctionInterrupt) { return }
        try {
            # Build API URI with encoded path
            $basePath = "$Project/_apis/wit/classificationnodes/$StructureGroup"
            if ($Path) {
                # URL encode the path segments
                $pathSegments = $Path.Split('\', [StringSplitOptions]::RemoveEmptyEntries)
                $encodedSegments = $pathSegments | ForEach-Object { [System.Uri]::EscapeDataString($_) }
                $basePath += '/' + ($encodedSegments -join '/')
            }

            # Add query parameters
            $query = @{}
            if ($ReclassifyId) {
                $query['$reclassifyId'] = $ReclassifyId
            }

            $apiUri = $basePath
            if ($query.Count -gt 0) {
                $pairs = foreach ($kv in $query.GetEnumerator()) { "{0}={1}" -f $kv.Key,$kv.Value }
                $apiUri += '?' + ($pairs -join '&')
            }

            Write-PSFMessage -Level Verbose -Message "API URI: $apiUri"

            # Confirmation prompt
            $target = "$StructureGroup node '$Path'"
            if ($ReclassifyId) {
                $target += " (reclassifying work items to ID $ReclassifyId)"
            }
            
            if ($PSCmdlet.ShouldProcess($target, "Delete classification node")) {
                # Make API call
                Invoke-ADOApiRequest -Organization $Organization `
                                     -Token $Token `
                                     -ApiUri $apiUri `
                                     -Method 'DELETE' `
                                     -Headers @{'Content-Type' = 'application/json'} `
                                     -ApiVersion $ApiVersion

                Write-PSFMessage -Level Verbose -Message "Successfully deleted classification node '$Path'"

                if ($PassThru) {
                    return $Path
                }
            }
        }
        catch {
            Write-PSFMessage -Level Error -Message "Failed to delete classification node '$Path': $($_.Exception.Message)" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors" -Target $PSCmdlet.MyInvocation.MyCommand.Name -ErrorRecord $_
        }
    }

    end {
        Write-PSFMessage -Level Verbose -Message "Completed classification node deletion operation"
        Invoke-TimeSignal -End
    }
}