Functions/AzureDevOps/Add-ADOAreaPath.ps1

function Add-ADOAreaPath
{
    <#
    .Synopsis
        Adds an Azure DevOps AreaPath
    .Description
        Adds an Azure DevOps AreaPath. AreaPaths are used to logically group work items within a project.
    .Example
        Add-ADOAreaPath -Organization MyOrg -Project MyProject -AreaPath MyAreaPath
    .Example
        Add-ADOAreaPath -Organization MyOrg -Project MyProject -AreaPath MyAreaPath\MyNestedPath
    .Link
        Get-ADOAreaPath
    .Link
        Remove-ADOAreaPath
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('PSDevOps.AreaPath')]
    param(
    # The Organization.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
    [Alias('Org')]
    [string]
    $Organization,

    # The Project.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
    [string]
    $Project,

    # The AreaPath.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
    [string]
    $AreaPath,

    # The server. By default https://dev.azure.com/.
    # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs).
    [Parameter(ValueFromPipelineByPropertyName)]
    [uri]
    $Server = "https://dev.azure.com/",

    # The api version. By default, 5.1.
    # If targeting TFS, this will need to change to match your server version.
    # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops
    [string]
    $ApiVersion = "5.1-preview")
    dynamicParam { . $GetInvokeParameters -DynamicParameter }
    begin {
        #region Copy Invoke-ADORestAPI parameters
        $invokeParams = . $getInvokeParameters $PSBoundParameters
        #endregion Copy Invoke-ADORestAPI parameters

        $q = [Collections.Queue]::new() # Collect input in a queue
    }

    process {
        $q.Enqueue(@{} + $psboundParameters) # enqueue new input.
    }

    end {
        $c,$t, $id = 0, $q.Count, [Random]::new().Next()
        $allAreaPaths = @{}
        #region Until the Queue is Empty
        while ($q.Count) {
            $qi = $q.Dequeue() # Dequeue each item.
            $getAreaPathParams = @{} + $qi # Prepare input for Get-ADOAreaPath
            $getAreaPathParams.Remove('AreaPath')
            $getAreaPathParams.Remove('WhatIf')
            $getAreaPathParams.Remove('Confirm')
            foreach ($kv in $qi.GetEnumerator()) { # Repopulate the input parameters.
                $ExecutionContext.SessionState.PSVariable.Set($kv.Key, $kv.Value)
            }
            if ($t -gt 1) {
                $c++
                Write-Progress "Adding Area Paths" "$AreaPath " -PercentComplete ($c * 100 / $t) -Id $id
            }
            $areaPathParts= @($areaPath -split '\\')
            $getAreaPathParams.Depth = $areaPathParts.Length + 2
            if (-not $allAreaPaths["$Organization/$Project"]) { # Cache ADO Area Paths
                $allAreaPaths["$Organization/$Project"] = @(Get-ADOAreaPath @getAreaPathParams)
            }

            $existingAreaPath = $null
            $closestAreaPath  = $null
            $areaPathCache    = $allAreaPaths["$Organization/$Project"]
            foreach ($path in $areaPathCache) { # See If the path already exists, or where we can put it in the hierarchy.
                if ($path.Path -eq "\$project\Area\$AreaPath") {
                    $existingAreaPath = $path
                }
                elseif ($areaPathParts.Count -gt 1) {
                    for ($i = ($areaPathParts.Count - 2); $i -ge 0; $i--) {
                        if ($path.Path -like "\$Project\Area\$($areaPathParts[0..$i] -join '\')") {
                            $closestAreaPath = $path.Path
                            break
                        }
                    }
                }
            }

            if ($existingAreaPath) { # If it already existed, write it to Verbose.
                Write-Verbose "AreaPath: '\$Project\Area\$areaPath' already exists"
                continue
            }

            $body = @{name = $areaPathParts[-1]} # Prepare the input body

            $base = "/{Organization}/{Project}/_apis/wit/classificationNodes/Areas"
            if ($closestAreaPath) { # If we had a closest path,

                $firstSlash, $projectPart, $areaPart, $closestPart = # split up the AreaPath
                    $closestAreaPath -split '\\', 4
                $base += "/$closestPart" # and add the subpath to the base.
            }

             $uri =
                $Server.ToString().TrimEnd('/') +
                (. $ReplaceRouteParameter $base) +
                '?' + $(
                    if ($Server -ne 'https://dev.azure.com' -and -not $psBoundParameters['apiVersion']) {
                        $apiVersion = "2.0"
                    }
                    if ($ApiVersion) {
                        "api-version=$ApiVersion"
                    }
                )

            $invokeParams.Uri = $uri
            $invokeParams.Method = 'POST'
            $invokeParams.Body = $body

            if ($WhatIfPreference) { # If we passed -WhatIf
                $invokeParams.Remove('PersonalAccessToken') # remove the PersonalAccessToken
                $invokeParams # and return other parameters.
                continue
            }
            if (-not $PSCmdlet.ShouldProcess("Add AreaPath $AreaPath")) { continue } # Check ShouldProcess
            $typeName = "$Organization.AreaPath", "$Organization.$project.AreaPath", "PSDevOps.AreaPath"

            # Invoke the REST API.
            Invoke-ADORestAPI @invokeParams -PSTypeName $typeName -Property @{
                Organization = $organization
                Project = $Project
                Server = $server
            }
        }
        #endregion Until the Queue is Empty
        if ($t -gt 1) {
            Write-Progress "Adding Area Paths" ' ' -Completed -Id $id
        }
    }
}