functions/add-adoworkitemquery.ps1


<#
    .SYNOPSIS
        Creates a new work item query/folder or moves an existing one.
    .DESCRIPTION
        Wraps the Azure DevOps Queries - Create endpoint to:
            - Create a folder ( -Folder )
            - Create a WIQL query ( -Wiql provided, not -Folder )
            - Move an existing query/folder ( -Id parameter set )
            - Validate WIQL only ( -ValidateWiqlOnly )
    .OUTPUTS
        ADO.TOOLS.QueryHierarchyItem
    .PARAMETER Organization
        Azure DevOps organization name.
    .PARAMETER Project
        Project name or id.
    .PARAMETER Token
        Personal Access Token (PAT) with vso.work_write scope.
    .PARAMETER ParentPath
        Destination parent path (e.g. 'Shared Queries' or 'My Queries/Sub').
    .PARAMETER Name
        Name of the new query/folder (Create set).
    .PARAMETER Folder
        Switch indicating a folder should be created.
    .PARAMETER Wiql
        WIQL text for the query (omit when creating a folder).
    .PARAMETER QueryType
        flat | tree | oneHop (optional override).
    .PARAMETER Columns
        Field reference names for query columns.
    .PARAMETER SortColumns
        Sort definitions (Field or Field:desc).
    .PARAMETER Public
        Make the created item public.
    .PARAMETER Id
        Existing query/folder id to move (Move set).
    .PARAMETER ValidateWiqlOnly
        Validate WIQL without persisting the query.
    .PARAMETER ApiVersion
        API version (default 7.1).
    .EXAMPLE
        PS> Add-ADOWorkItemQuery -Organization org -Project proj -Token $pat -ParentPath 'Shared Queries' -Name 'All Bugs' -Wiql "Select ..."
         
        Creates a flat WIQL query under Shared Queries.
    .EXAMPLE
        PS> Add-ADOWorkItemQuery -Organization org -Project proj -Token $pat -ParentPath 'Shared Queries' -Name 'Release' -Folder
         
        Creates a folder named 'Release'.
    .EXAMPLE
        PS> Add-ADOWorkItemQuery -Organization org -Project proj -Token $pat -ParentPath 'My Queries' -Id 8a8c8212-...-d581
         
        Moves an existing folder/query to My Queries.
    .EXAMPLE
        PS> Add-ADOWorkItemQuery -Organization org -Project proj -Token $pat -ParentPath 'Shared Queries' -Name 'Check' -Wiql 'Select ...' -ValidateWiqlOnly
         
        Validates WIQL without creating the query.
    .LINK
        https://learn.microsoft.com/azure/devops/boards/queries/wiql-syntax
    .NOTES
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Add-ADOWorkItemQuery {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions","")]
    [CmdletBinding(DefaultParameterSetName='Create')]
    param(
        [Parameter(Mandatory=$true)] [string]$Organization,
        [Parameter(Mandatory=$true)] [string]$Project,
        [Parameter(Mandatory=$true)] [string]$Token,

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

        # Create
        [Parameter(Mandatory=$true, ParameterSetName='Create')]
        [string]$Name,
        [Parameter(ParameterSetName='Create')]
        [switch]$Folder,
        [Parameter(ParameterSetName='Create')]
        [string]$Wiql,
        [Parameter(ParameterSetName='Create')]
        [ValidateSet('flat','tree','oneHop')]
        [string]$QueryType,
        [Parameter(ParameterSetName='Create')]
        [string[]]$Columns,
        [Parameter(ParameterSetName='Create')]
        [string[]]$SortColumns,
        [Parameter(ParameterSetName='Create')]
        [switch]$Public,

        # Move
        [Parameter(Mandatory=$true, ParameterSetName='Move')]
        [string]$Id,

        # Common
        [Parameter()] [switch]$ValidateWiqlOnly,
        [Parameter()] [string]$ApiVersion = '7.1'
    )

    begin {
        Write-PSFMessage -Level Verbose -Message "Starting Add/Move WorkItem Query (Set: $($PSCmdlet.ParameterSetName)) Parent: $ParentPath Project: $Project"
        Invoke-TimeSignal -Start
    }

    process {
        if (Test-PSFFunctionInterrupt) { return }
        try {
            # Encode each path segment but preserve '/'
            $escapedParent = ($ParentPath -split '/' | ForEach-Object { [System.Uri]::EscapeDataString($_) }) -join '/'
            $apiUri = "$Project/_apis/wit/queries/$escapedParent"
            if ($ValidateWiqlOnly) { $apiUri += "?validateWiqlOnly=true" }

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

            $bodyHash = @{}
            if ($PSCmdlet.ParameterSetName -eq 'Move') {
                $bodyHash.id = $Id
            }
            else {
                $bodyHash.name = $Name
                if ($Folder) {
                    $bodyHash.isFolder = $true
                }
                else {
                    if ($Wiql) { $bodyHash.wiql = $Wiql }
                    if ($QueryType) { $bodyHash.queryType = $QueryType }
                    if ($Columns) {
                        $bodyHash.columns = @()
                        foreach ($col in $Columns) {
                            $bodyHash.columns += @{ referenceName = $col }
                        }
                    }
                    if ($SortColumns) {
                        $bodyHash.sortColumns = @()
                        foreach ($sc in $SortColumns) {
                            $parts = $sc.Split(':')
                            $ref = $parts[0]
                            $desc = ($parts.Count -gt 1 -and $parts[1].ToLower() -eq 'desc')
                            $bodyHash.sortColumns += @{
                                field = @{ referenceName = $ref }
                                descending = $desc
                            }
                        }
                    }
                }
                if ($Public) { $bodyHash.isPublic = $true }
            }

            $body = $bodyHash | ConvertTo-Json -Depth 6

            $response = Invoke-ADOApiRequest -Organization $Organization `
                                             -Token $Token `
                                             -ApiUri $apiUri `
                                             -Method 'POST' `
                                             -Body $body `
                                             -Headers @{'Content-Type'='application/json'} `
                                             -ApiVersion $ApiVersion

            Write-PSFMessage -Level Verbose -Message "Successfully processed query operation (Set: $($PSCmdlet.ParameterSetName))"
            return $response.Results | Select-PSFObject * -TypeName 'ADO.TOOLS.QueryHierarchyItem'
        }
        catch {
            Write-PSFMessage -Level Error -Message "Failed to process query operation: $($_.ErrorDetails.Message)" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
        }
    }

    end {
        Write-PSFMessage -Level Verbose -Message "Completed Add/Move WorkItem Query operation"
        Invoke-TimeSignal -End
    }
}