diff --git a/.docs/Get-VSTeamWiql.md b/.docs/Get-VSTeamWiql.md new file mode 100644 index 000000000..cd593f990 --- /dev/null +++ b/.docs/Get-VSTeamWiql.md @@ -0,0 +1,93 @@ + + +# Get-VSTeamWiqlItem + +## SYNOPSIS + + + +## SYNTAX + +## DESCRIPTION + + + +## EXAMPLES + +### -------------------------- EXAMPLE 1 -------------------------- + +```PowerShell +PS C:\> Get-VSTeamWiql -Query "Select [System.Id], [System.Title], [System.State] From WorkItems" -Team "MyProject Team" -Project "MyProject" -Expand +``` + +This command gets work items via a WIQL query and expands the return work items with only the selected fields System.Id, System.Title and System.State. + +### -------------------------- EXAMPLE 2 -------------------------- + +```PowerShell +PS C:\> Get-VSTeamWiql -Query "Select [System.Id], [System.Title], [System.State] From WorkItems" -Team "MyProject Team" -Project "MyProject" +``` + +This command gets work items via a WIQL query and returns the WIQL query result with only work item IDs. + +## PARAMETERS + +### -Id + +The id query to return work items for. This is the ID of any saved query within a team in a project + +```yaml +Type: Int32 +Parameter Sets: ByID +Required: True +``` + +### -Query + +The WIQL query. For the syntax check [the official documentation](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops). + +```yaml +Type: String +Parameter Sets: ByQuery +Required: True +``` + +### -Top + +The max number of results to return. + +```yaml +Type: String +Required: False +Default value: 100 +``` + +### -TimePrecision + +Whether or not to use time precision. + +```yaml +Type: Switch +``` + +### -Expand + +The expand the work items with the selected attributes in the WIQL query. + +```yaml +Type: Switch +``` + +## INPUTS + +### System.String + +ProjectName + +## OUTPUTS + +## NOTES + +If you do not set the default project by called Set-VSTeamDefaultProject before calling Get-VSTeamWiql you will have to type in the names. + +## RELATED LINKS diff --git a/.docs/synopsis/Get-VSTeamWiql.md b/.docs/synopsis/Get-VSTeamWiql.md new file mode 100644 index 000000000..fe760e523 --- /dev/null +++ b/.docs/synopsis/Get-VSTeamWiql.md @@ -0,0 +1 @@ +Returns work items from the given WIQL query or a saved query by ID from your projects team. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a187dbb1..ec999799c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.4.0 + +Added Get-VSTeamWiql to get work items via [WIQL](https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/wiql?view=azure-devops-rest-5.1) and also to expand the returned work items with all fields selected. + ## 6.3.6 Merged [Pull Request](https://github.com/DarqueWarrior/vsteam/pull/200) from [Chris Gardner](https://github.com/ChrisLGardner) which included the following: diff --git a/Source/Private/applyTypes.ps1 b/Source/Private/applyTypes.ps1 index 91bf9324f..8e55440f5 100644 --- a/Source/Private/applyTypes.ps1 +++ b/Source/Private/applyTypes.ps1 @@ -25,6 +25,13 @@ function _applyTypesToWorkItem { } } +function _applyTypesToWiql { + param($item) + if ($item) { + $item.PSObject.TypeNames.Insert(0, 'Team.Wiql') + } +} + function _applyTypesToUser { param( [Parameter(Mandatory = $true)] diff --git a/Source/Private/common.ps1 b/Source/Private/common.ps1 index b756b75de..464a236b3 100644 --- a/Source/Private/common.ps1 +++ b/Source/Private/common.ps1 @@ -65,6 +65,7 @@ function _hasAccount { function _buildRequestURI { [CmdletBinding()] param( + [string]$team, [string]$resource, [string]$area, [string]$id, @@ -90,6 +91,10 @@ function _buildRequestURI { $sb.Append("/$projectName") | Out-Null } + if ($team) { + $sb.Append("/$team") | Out-Null + } + $sb.Append("/_apis/") | Out-Null if ($area) { @@ -588,8 +593,9 @@ function _callAPI { [object]$body, [string]$InFile, [string]$OutFile, - [string]$ContentType, + [string]$ContentType, [string]$ProjectName, + [string]$Team, [string]$Url, [object]$QueryString ) @@ -628,7 +634,7 @@ function _callAPI { } # We have to remove any extra parameters not used by Invoke-RestMethod - $extra = 'Area', 'Resource', 'SubDomain', 'Id', 'Version', 'JSON', 'ProjectName', 'Url', 'QueryString' + $extra = 'Area', 'Resource', 'SubDomain', 'Id', 'Version', 'JSON', 'ProjectName', 'Team', 'Url', 'QueryString' foreach ($e in $extra) { $params.Remove($e) | Out-Null } try { diff --git a/Source/Public/Get-VSTeamWiql.ps1 b/Source/Public/Get-VSTeamWiql.ps1 new file mode 100644 index 000000000..c0b5b24ca --- /dev/null +++ b/Source/Public/Get-VSTeamWiql.ps1 @@ -0,0 +1,81 @@ +function Get-VSTeamWiql { + [CmdletBinding(DefaultParameterSetName = 'ByID')] + param( + [Parameter(ParameterSetName = 'ByID', Mandatory = $true, Position = 0)] + [string] $Id, + + [Parameter(ParameterSetName = 'ByQuery', Mandatory = $true, Position = 0)] + [string] $Query, + + [Parameter(Mandatory = $true, Position = 1)] + [string] $Team, + + [int] $Top = 100, + + [Switch] $TimePrecision, + + [Switch] $Expand + ) + DynamicParam { + #$arrSet = Get-VSTeamProject | Select-Object -ExpandProperty Name + _buildProjectNameDynamicParam -mandatory $true #-arrSet $arrSet + } + + Process { + + # Bind the parameter to a friendly variable + $ProjectName = $PSBoundParameters["ProjectName"] + + $QueryString = @{ + '$top' = $Top + timePrecision = $TimePrecision + } + + # Call the REST API + if ($Query) { + + $body = (@{ + query = $Query + }) | ConvertTo-Json + + $resp = _callAPI -ProjectName $ProjectName -Team $Team -Area 'wit' -Resource 'wiql' ` + -method "POST" -ContentType "application/json" ` + -Version $([VSTeamVersions]::Core) ` + -Querystring $QueryString ` + -Body $body + } + else { + $resp = _callAPI -ProjectName $ProjectName -Team $Team -Area 'wit' -Resource 'wiql' ` + -Version $([VSTeamVersions]::Core) -id "$Id" ` + -Querystring $QueryString + } + + if ($Expand) { + + [array]$Ids = $resp.workItems.id + $Fields = $resp.columns.referenceName + + $resp.workItems = @() + #splitting id array by 200, since a maximum of 200 ids are allowed per call + $countIds = $Ids.Count + $resp.workItems = for ($beginRange = 0; $beginRange -lt $countIds; $beginRange += 200) { + + $endRange = ($beginRange + 199) + + if ($endRange -gt $countIds) { + $idArray = $Ids[$beginRange..($countIds - 1)] + } + else { + $idArray = $Ids[$beginRange..($endRange)] + } + + (Get-VSTeamWorkItem -Fields $Fields -Ids $idArray).value + } + + } + + _applyTypesToWiql -item $resp + + return $resp + } +} \ No newline at end of file diff --git a/Source/VSTeam.psd1 b/Source/VSTeam.psd1 index 68c96ca65..746cdda3b 100644 --- a/Source/VSTeam.psd1 +++ b/Source/VSTeam.psd1 @@ -12,7 +12,7 @@ RootModule = 'VSTeam.psm1' # Version number of this module. - ModuleVersion = '6.3.6' + ModuleVersion = '6.4.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/Source/formats/Team.Wiql.ListView.ps1xml b/Source/formats/Team.Wiql.ListView.ps1xml new file mode 100644 index 000000000..cdde42ec8 --- /dev/null +++ b/Source/formats/Team.Wiql.ListView.ps1xml @@ -0,0 +1,28 @@ + + + + + Team.Wiql.ListView + + Team.Wiql + + + + + + + queryType + + + WorkItemIDs + + + workItems + + + + + + + + \ No newline at end of file diff --git a/Source/formats/Team.Wiql.TableView.ps1xml b/Source/formats/Team.Wiql.TableView.ps1xml new file mode 100644 index 000000000..c8c54361a --- /dev/null +++ b/Source/formats/Team.Wiql.TableView.ps1xml @@ -0,0 +1,39 @@ + + + + + Team.Wiql.TableView + + Team.Wiql + + + + + + + + + + + + + + + + + + queryType + + + WorkItemIDs + + + workItems + + + + + + + + \ No newline at end of file diff --git a/Source/formats/_formats.json b/Source/formats/_formats.json index 009abec4a..c965e5393 100644 --- a/Source/formats/_formats.json +++ b/Source/formats/_formats.json @@ -89,6 +89,8 @@ "Team.PSDrive.Leaf.Default.TableView.ps1xml", "Team.WorkItemType.TableView.ps1xml", "Team.WorkItemType.ListView.ps1xml", + "Team.Wiql.TableView.ps1xml", + "Team.Wiql.ListView.ps1xml", "Team.WorkItem.TableView.ps1xml", "Team.WorkItem.ListView.ps1xml", "Team.JobRequest.TableView.ps1xml" diff --git a/Source/types/Team.Wiql.ps1xml b/Source/types/Team.Wiql.ps1xml new file mode 100644 index 000000000..fab3e0d4d --- /dev/null +++ b/Source/types/Team.Wiql.ps1xml @@ -0,0 +1,49 @@ + + + + Team.Wiql + + + QueryType + $this.PSObject.Properties['queryType'].Value + + + QueryResultType + $this.PSObject.Properties['queryResultType'].Value + + + AsOf + $this.PSObject.Properties['asOf'].Value + + + Columns + $this.PSObject.Properties['columns'].Value + + + SortColumns + $this.PSObject.Properties['sortColumns'].Value + + + WorkItemIDs + [int[]]$this.PSObject.Properties['workItems'].Value.id + + + WorkItems + $this.PSObject.Properties['workItems'].Value + + + PSStandardMembers + + + DefaultDisplayPropertySet + + queryType + WorkItemIDs + workItems + + + + + + + \ No newline at end of file diff --git a/unit/test/wiql.Tests.ps1 b/unit/test/wiql.Tests.ps1 new file mode 100644 index 000000000..16739121d --- /dev/null +++ b/unit/test/wiql.Tests.ps1 @@ -0,0 +1,168 @@ +Set-StrictMode -Version Latest + +InModuleScope VSTeam { + [VSTeamVersions]::Account = 'https://dev.azure.com/test' + + Describe 'wiql' { + # Mock the call to Get-Projects by the dynamic parameter for ProjectName + Mock Invoke-RestMethod { return @() } -ParameterFilter { + $Uri -like "*_apis/projects*" + } + + . "$PSScriptRoot\mocks\mockProjectNameDynamicParamNoPSet.ps1" + + $workItem = @{ + id = 47 + url = "https://dev.azure.com/test/_apis/wit/workItems/47" + } + + $column = @{ + referenceName = "System.Id" + name = "ID" + url = "https://dev.azure.com/razorspoint-test/_apis/wit/fields/System.Id" + } + + $sortColumn = @{ + field = $column + descending = $false + } + + $wiqlResult = @{ + querytype = "flat" + queryTypeResult = "worItem" + asOf = "2019-10-03T18:35:09.117Z" + columns = @($column) + sortColumns = @($sortColumn) + workItems = @($workItem,$workItem) + } + + $expandedWorkItems = @{ + count = 1 + value = @($workItem,$workItem) + } + + Context 'Get-Wiql' { + Mock Invoke-RestMethod { + # If this test fails uncomment the line below to see how the mock was called. + Write-Host $args + + return $wiqlResult + } + + # function is mocked because it is used when switch 'Expanded' is being used. + Mock Get-VSTeamWorkItem { + # If this test fails uncomment the line below to see how the mock was called. + #Write-Host $args + + return $expandedWorkItems + } + + It 'Get work items with custom WIQL query' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + $wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems" + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Query $wiqlQuery + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Post' -and + $Body -like '`{*' -and # Make sure the body is an object + $Body -like "*[System.Id]*" -and + $Body -like "*[System.Title]*" -and + $Body -like "*[System.State]*" -and + $Body -like '*`}' -and # Make sure the body is an object + $ContentType -eq 'application/json' -and + $Uri -eq "https://dev.azure.com/test/test/test team/_apis/wit/wiql/?api-version=$([VSTeamVersions]::Core)&`$top=100" + } + } + + It 'Get work items with custom WIQL query with -Top 250' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + $wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems" + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Query $wiqlQuery -Top 250 + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Post' -and + $Body -like '`{*' -and # Make sure the body is an object + $Body -like "*[System.Id]*" -and + $Body -like "*[System.Title]*" -and + $Body -like "*[System.State]*" -and + $Body -like '*`}' -and # Make sure the body is an object + $ContentType -eq 'application/json' -and + $Uri -eq "https://dev.azure.com/test/test/test team/_apis/wit/wiql/?api-version=$([VSTeamVersions]::Core)&`$top=250" + } + } + + It 'Get work items with custom WIQL query with -Top 0' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + $wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems" + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Query $wiqlQuery -Top 0 + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Post' -and + $Body -like '`{*' -and # Make sure the body is an object + $Body -like "*[System.Id]*" -and + $Body -like "*[System.Title]*" -and + $Body -like "*[System.State]*" -and + $Body -like '*`}' -and # Make sure the body is an object + $ContentType -eq 'application/json' -and + $Uri -eq "https://dev.azure.com/test/test/test team/_apis/wit/wiql/?api-version=$([VSTeamVersions]::Core)" + } + } + + It 'Get work items with custom WIQL query with expanded work items' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + $wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems" + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Query $wiqlQuery -Expand + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Post' -and + $Body -like '`{*' -and # Make sure the body is an object + $Body -like "*[System.Id]*" -and + $Body -like "*[System.Title]*" -and + $Body -like "*[System.State]*" -and + $Body -like '*`}' -and # Make sure the body is an object + $ContentType -eq 'application/json' -and + $Uri -eq "https://dev.azure.com/test/test/test team/_apis/wit/wiql/?api-version=$([VSTeamVersions]::Core)&`$top=100" + } + } + + It 'Get work items with custom WIQL query with time precision' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + $wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems" + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Query $wiqlQuery -TimePrecision + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Post' -and + $Body -like '`{*' -and # Make sure the body is an object + $Body -like "*[System.Id]*" -and + $Body -like "*[System.Title]*" -and + $Body -like "*[System.State]*" -and + $Body -like '*`}' -and # Make sure the body is an object + $ContentType -eq 'application/json' -and + $Uri -like "*timePrecision=True*" + $Uri -like "*`$top=100*" + $Uri -like "https://dev.azure.com/test/test/test team/_apis/wit/wiql/?api-version=$([VSTeamVersions]::Core)*" + } + } + + It 'Get work items with query ID query' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Id 1 + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/test/test team/_apis/wit/wiql/1?api-version=$([VSTeamVersions]::Core)&`$top=100" + } + } + + It 'Get work items with query ID query with expanded work items' { + $Global:PSDefaultParameterValues.Remove("*:projectName") + Get-VSTeamWiql -ProjectName "test" -Team "test team" -Id 1 -Expand + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Uri -eq "https://dev.azure.com/test/test/test team/_apis/wit/wiql/1?api-version=$([VSTeamVersions]::Core)&`$top=100" + } + } + + + } + } +} \ No newline at end of file