diff --git a/Formatting/PSDevOps.SharedQuery.format.ps1 b/Formatting/PSDevOps.SharedQuery.format.ps1 new file mode 100644 index 00000000..78326ab2 --- /dev/null +++ b/Formatting/PSDevOps.SharedQuery.format.ps1 @@ -0,0 +1 @@ +Write-FormatView -TypeName PSDevOps.SharedQuery -Property IsPublic, Path, Wiql -Wrap -GroupByProperty Project diff --git a/Get-ADOWorkItem.ps1 b/Get-ADOWorkItem.ps1 index 1044c3fa..ac79b474 100644 --- a/Get-ADOWorkItem.ps1 +++ b/Get-ADOWorkItem.ps1 @@ -93,6 +93,7 @@ # If provided, will only return the first N results from a query. [Parameter(ParameterSetName='/{Organization}/{Project}/{Team}/_apis/wit/wiql',ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName='/{Organization}/{Project}/_apis/wit/queries',ValueFromPipelineByPropertyName)] [Alias('Top')] [uint32] $First, @@ -103,6 +104,33 @@ [switch] $WorkItemType, + # If set, will return work item shared queries + [Parameter(Mandatory,ParameterSetName='/{Organization}/{Project}/_apis/wit/queries',ValueFromPipelineByPropertyName)] + [switch] + $SharedQuery, + + # If set, will return shared queries that have been deleted. + [Parameter(ParameterSetName='/{Organization}/{Project}/_apis/wit/queries',ValueFromPipelineByPropertyName)] + [switch] + $IncludeDeleted, + + # If provided, will return shared queries up to a given depth. + [Parameter(ParameterSetName='/{Organization}/{Project}/_apis/wit/queries',ValueFromPipelineByPropertyName)] + [ValidateRange(0,2)] + [int] + $Depth, + + # If provided, will filter the shared queries returned + [Parameter(ParameterSetName='/{Organization}/{Project}/_apis/wit/queries',ValueFromPipelineByPropertyName)] + [int] + $SharedQueryFilter, + + # Determines how data from shared queries will be expanded. By default, expands all data. + [Parameter(ParameterSetName='/{Organization}/{Project}/_apis/wit/queries',ValueFromPipelineByPropertyName)] + [ValidateSet('All','Clauses','Minimal','None', 'Wiql')] + [string] + $ExpandSharedQuery = 'All', + # One or more fields. [Alias('Fields','Select')] [string[]] @@ -170,6 +198,31 @@ } #endregion Output Work Item + + #region ExpandSharedQueries + $expandSharedQueries = { + param([Parameter(ValueFromPipeline)]$node) + process { + if (-not $node) { return } + $node.pstypenames.clear() + foreach ($typeName in "$organization.SharedQuery", + "$organization.$Project.SharedQuery", + "PSDevOps.SharedQuery" + ) { + $node.pstypenames.Add($typeName) + } + $node | + Add-Member NoteProperty Organization $organization -Force -PassThru | + Add-Member NoteProperty Project $Project -Force -PassThru | + Add-Member NoteProperty Server $Server -Force -PassThru + if ($node.haschildren) { + $node.children | + & $MyInvocation.MyCommand.ScriptBlock + } + } + } + #endregion ExpandSharedQueries + $allIDS = [Collections.ArrayList]::new() } @@ -181,6 +234,19 @@ $selfSplat.Query = "Select [System.ID] from WorkItems Where [System.Title] contains '$title'" Get-ADOWorkItem @selfSplat } + elseif ($psCmdlet.ParameterSetName -eq '/{Organization}/{Project}/_apis/wit/queries') { + $myInvokeParams = @{} + $invokeParams + $myInvokeParams.Url = "$Server".TrimEnd('/') + $psCmdlet.ParameterSetName + + $myInvokeParams.QueryParameter = @{'$expand'= $ExpandSharedQuery} + $myInvokeParams.UrlParameter = @{} + $psBoundParameters + if ($IncludeDeleted) { $myInvokeParams.QueryParameter.'$includeDeleted' = $true } + if ($First) { $myInvokeParams.QueryParameter.'$top' = $First} + if ($Depth) { $myInvokeParams.QueryParameter.'$depth' = $Depth} + $myInvokeParams.Property = @{Organization = $Organization;Project=$Project} + Invoke-ADORestAPI @myInvokeParams | & $expandSharedQueries + return + } elseif ( $PSCmdlet.ParameterSetName -in '/{Organization}/{Project}/_apis/wit/workitems/{id}', @@ -198,7 +264,7 @@ elseif ($PSCmdlet.ParameterSetName -eq '/{Organization}/{Project}/{Team}/_apis/wit/wiql') { $uri = "$Server".TrimEnd('/') + (. $ReplaceRouteParameter $PSCmdlet.ParameterSetName) + '?' - $uri += + $uri += @(if ($First) { "`$top=$First" } @@ -214,7 +280,6 @@ $realQuery += ' AND ' } - $realQuery += @( if ($Project) { @@ -261,6 +326,7 @@ "api-version=$ApiVersion" } $invokeParams.Uri = $uri + $invokeParams.Property = @{Organization = $Organization} $workItemTypes = Invoke-ADORestAPI @invokeParams $workItemTypes -replace '"":', '"_blank":' | ConvertFrom-Json | diff --git a/New-ADOWorkItem.ps1 b/New-ADOWorkItem.ps1 index 9128a4a5..8478682b 100644 --- a/New-ADOWorkItem.ps1 +++ b/New-ADOWorkItem.ps1 @@ -11,22 +11,55 @@ .Link Invoke-ADORestAPI #> - [CmdletBinding(DefaultParameterSetName='ByID',SupportsShouldProcess=$true)] + [CmdletBinding(DefaultParameterSetName='WorkItem',SupportsShouldProcess=$true)] [OutputType('PSDevOps.WorkItem')] param( # The InputObject - [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] + [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName='WorkItem')] [PSObject] $InputObject, # The type of the work item. - [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Parameter(Mandatory, ParameterSetName='WorkItem',ValueFromPipelineByPropertyName)] [Alias('WorkItemType')] [string] $Type, + # If set, will create a shared query for work items. The -InputObject will be passed to the body. + [Parameter(Mandatory,ParameterSetName='SharedQuery',ValueFromPipelineByPropertyName)] + [string] + $QueryName, + + # If provided, will create shared queries beneath a given folder. + [Parameter(ParameterSetName='SharedQuery',ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName='SharedQueryFolder',ValueFromPipelineByPropertyName)] + [string] + $QueryPath, + + # If provided, create a shared query with a given WIQL. + [Parameter(Mandatory, ParameterSetName='SharedQuery',ValueFromPipelineByPropertyName)] + [string] + $WIQL, + + # If provided, the shared query created may be hierchical + [Parameter(ParameterSetName='SharedQuery',ValueFromPipelineByPropertyName)] + [ValidateSet('Flat','OneHop', 'Tree')] + [string] + $QueryType, + + # The recursion option for use in a tree query. + [Parameter(ParameterSetName='SharedQuery',ValueFromPipelineByPropertyName)] + [ValidateSet('childFirst','parentFirst')] + [string] + $QueryRecursiveOption, + + # If provided, create a shared query folder. + [Parameter(Mandatory, ParameterSetName='SharedQueryFolder',ValueFromPipelineByPropertyName)] + [string] + $FolderName, + # The work item ParentID - [Parameter(ValueFromPipelineByPropertyName)] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WorkItem')] [string] $ParentID, @@ -42,13 +75,13 @@ $Project, # A collection of relationships for the work item. - [Parameter(ValueFromPipelineByPropertyName)] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WorkItem')] [Alias('Relationships')] [Collections.IDictionary] $Relationship, # A list of comments to be added to the work item. - [Parameter(ValueFromPipelineByPropertyName)] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WorkItem')] [PSObject[]] $Comment, @@ -58,7 +91,7 @@ $Tag, # If set, will not validate rules. - [Parameter(ValueFromPipelineByPropertyName)] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WorkItem')] [Alias('BypassRules','NoRules','NoRule')] [switch] $BypassRule, @@ -70,7 +103,7 @@ $ValidateOnly, # If set, will only validate rules, but will not update the work item. - [Parameter(ValueFromPipelineByPropertyName)] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='WorkItem')] [Alias('SuppressNotifications','SkipNotification','SkipNotifications','NoNotify')] [switch] $SupressNotification, @@ -160,6 +193,9 @@ } } #endregion Output Work Item + + + $q = [Collections.Queue]::new() } @@ -177,14 +213,89 @@ $c++ Write-Progress "Creating" "$type [$c/$t]" -PercentComplete ($c * 100 / $t) -Id $progId - + $orgAndProject = @{Organization=$Organization;Project=$Project} $validFields = if ($script:ADOFieldCache.$uribase) { $script:ADOFieldCache.$uribase } else { - Get-ADOField -Organization $Organization -Project $Project -Server $Server @invokeParams + Get-ADOField @orgAndProject -Server $Server @invokeParams } + if ($psParameterSet -in 'SharedQuery', 'SharedQueryFolder') { + if ($Server -ne 'https://dev.azure.com/' -and + -not $PSBoundParameters.ApiVersion) { + $ApiVersion = '2.0' + } + + $queryPathParts = @($QueryPath -split '/') + $sharedQueries = $null + foreach ($qp in $queryPathParts) { + if (-not $qp) { continue } + if (-not ($qp -as [guid])) { + $sharedQueries = Get-ADOWorkItem -SharedQuery @orgAndProject -Depth 2 + break + } + } + + if ($sharedQueries) { + $queryPathId = $sharedQueries | + Where-Object Path -eq $QueryPath | + Select-Object -ExpandProperty ID + if (-not $queryPathId) { + Write-Error "Unable to find Query Path '$QueryPath'" + continue + } else { + $QueryPath = $queryPathId + } + } + + $uri = $uriBase, "_apis/wit/queries", $(if ($QueryPath) { $QueryPath }) -ne '' -join '/' + $uri = $uri.ToString().TrimEnd('/') + $uri += '?' + + (@( + if ($ApiVersion) { "api-version=$ApiVersion" } + if ($validateOnly) { "validateWiqlOnly=true" } + ) -join '&') + $invokeParams.uri = $uri + + $queryObject = @{} + if ($psParameterSet -eq 'SharedQueryFolder') { + $queryObject['name'] = $FolderName + $queryObject['isFolder'] = $true + if ($QueryType) { + $queryObject['queryType'] = $QueryType + } + if ($queryRecursionOption) { + $queryObject['queryRecursionOption'] = $queryRecursionOption + } + + } else { + $queryObject['name'] = $QueryName + $queryObject['wiql'] = $WIQL + + } + + $invokeParams.Body = ConvertTo-Json $queryObject -Depth 100 + $invokeParams.Method = 'POST' + $invokeParams.ContentType = 'application/json' + $invokeParams.PSTypeName = @( + "$Organization.$psParameterSet" + "$Organization.$project.$psParameterSet" + "PSDevOps.$psParameterSet" + ) + if ($WhatIfPreference) { + $invokeParams.Remove('PersonalAccessToken') + $invokeParams + continue + } + + if (-not $PSCmdlet.ShouldProcess("POST $uri with $($invokeParams.body)")) { continue } + $restResponse = Invoke-ADORestAPI @invokeParams 2>&1 + $restResponse + continue + } + + $validFieldTable = $validFields | Group-Object ReferenceName -AsHashTable $uri = $uriBase, "_apis/wit/workitems", "`$$($Type)?" -join '/' if ($Server -ne 'https://dev.azure.com/' -and diff --git a/PSDevOps.format.ps1xml b/PSDevOps.format.ps1xml index 9b673974..fc1344da 100644 --- a/PSDevOps.format.ps1xml +++ b/PSDevOps.format.ps1xml @@ -973,6 +973,41 @@ $($ParentNode.CustomControl.CustomEntries.CustomEntry.CustomItem.ExpressionBindi + + PSDevOps.SharedQuery + + PSDevOps.SharedQuery + + + Project + + + + + + + + + + + + + + + + IsPublic + + + Path + + + Wiql + + + + + + PSDevOps.Team diff --git a/PSDevOps.tests.ps1 b/PSDevOps.tests.ps1 index 33080cc3..58e5374a 100644 --- a/PSDevOps.tests.ps1 +++ b/PSDevOps.tests.ps1 @@ -1,4 +1,4 @@ -param( +param( [string] $TestOrg = 'StartAutomating', [string] @@ -291,7 +291,7 @@ describe 'Calling REST APIs' { } it 'Can set team -DefaultAreaPath and -AreaPath' { - $whatIf = + $whatIf = Get-ADOTeam -Organization StartAutomating -Project PSDevOps -TeamID 'PSDevOps Team' -PersonalAccessToken $testPat | Set-ADOTeam -DefaultAreaPath "MyAreaPath" -WhatIf -AreaPath "An\AreaPath", "Another\AreaPath" @@ -299,8 +299,8 @@ describe 'Calling REST APIs' { $whatIf.Uri | Should -BeLike '*teamFieldvalue*' $whatIf.Body.defaultValue | Should -Be MyAreaPath - - } + + } } context Repositories { @@ -692,11 +692,9 @@ describe 'Working with Work Items' { it 'Will not use workitemsbatch when using an old version of the REST api' { $queryResults = Get-ADOWorkItem -Organization StartAutomating -Project PSDevOps -Query 'Select [System.ID] from WorkItems Where [System.WorkItemType] = "Epic"' -PersonalAccessToken $testPat -ApiVersion '3.0' $queryResults[0].'System.WorkItemType' | should -be Epic - } + } } - - it 'Can create, update, and remove a work item' { $splat = @{Organization = $TestOrg; Project = $TestProject; PersonalAccessToken = $testPat } $wi = New-ADOWorkItem -InputObject @{Title = 'Test-WorkItem' } -Type Issue -ParentID 1 @splat -Tag 'PSDevOpsUnitTest' -Comment 'Added while unit testing' @@ -708,6 +706,8 @@ describe 'Working with Work Items' { Remove-ADOWorkItem @splat -Query "select [System.ID] from WorkItems Where [System.Title] = 'Test-WorkItem'" -Confirm:$false } + + it 'Can get work proccesses' { Get-ADOWorkProcess -Organization $TestOrg -PersonalAccessToken $testPat | Select-Object -First 1 -ExpandProperty name | diff --git a/PSDevOps.types.ps1xml b/PSDevOps.types.ps1xml index 2fec89db..78d9f20f 100644 --- a/PSDevOps.types.ps1xml +++ b/PSDevOps.types.ps1xml @@ -1083,6 +1083,24 @@ $bitMask + + PSDevOps.SharedQuery + + + QueryID + ID + + + + + Deserialized.PSDevOps.SharedQuery + + + QueryID + ID + + + PSDevOps.State diff --git a/Remove-ADOWorkItem.ps1 b/Remove-ADOWorkItem.ps1 index 7f02eb33..84a13eb6 100644 --- a/Remove-ADOWorkItem.ps1 +++ b/Remove-ADOWorkItem.ps1 @@ -38,6 +38,12 @@ [string] $Query, + # If set, will return work item shared queries + [Parameter(Mandatory,ParameterSetName='/{Organization}/{Project}/_apis/wit/queries/{QueryID}',ValueFromPipelineByPropertyName)] + [string] + $QueryID, + + # 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)] @@ -58,7 +64,12 @@ } process { - if ($PSCmdlet.ParameterSetName -eq 'ByID') { # If we're removing by ID + $psParameterSet = $PSCmdlet.ParameterSetName + $in = $_ + if ($in.QueryID) { + $psParameterSet = '/{Organization}/{Project}/_apis/wit/queries/{QueryID}' + } + if ($psParameterSet -eq 'ByID') { # If we're removing by ID $uriBase = "$Server".TrimEnd('/'), $Organization, $Project -join '/' $uri = $uriBase, "_apis/wit/workitems", "${ID}?" -join '/' @@ -75,7 +86,7 @@ $invokeParams.Method = 'DELETE' if (-not $PSCmdlet.ShouldProcess("Remove Work Item $ID")) { return } Invoke-ADORestAPI @invokeParams - } elseif ($PSCmdlet.ParameterSetName -eq 'ByQuery') { + } elseif ($psParameterSet -eq 'ByQuery') { $uri = "$Server".TrimEnd('/'), $Organization, $Project, "_apis/wit/wiql?" -join '/' @@ -99,5 +110,15 @@ Write-Progress "Updating Work Items" "Complete" -Completed -Id $progId } + elseif ($psParameterSet -eq '/{Organization}/{Project}/_apis/wit/queries/{QueryID}') { + $invokeParams.Method = "DELETE" + $invokeParams["Uri"] = "$Server".TrimEnd('/') + $psParameterSet + $invokeParams.QueryParameter = @{"api-version"="$ApiVersion"} + if ($WhatIfPreference) { + $invokeParams.Remove('PersonalAccessToken') + return $invokeParams + } + Invoke-ADORestAPi @invokeParams + } } } diff --git a/Types/PSDevOps.SharedQuery/Alias.psd1 b/Types/PSDevOps.SharedQuery/Alias.psd1 new file mode 100644 index 00000000..6aa24e7d --- /dev/null +++ b/Types/PSDevOps.SharedQuery/Alias.psd1 @@ -0,0 +1,3 @@ +@{ + QueryID = 'ID' +}