Function Convertfrom-Subattributes { <# .SYNOPSIS Function to convert returned data from CTKAPI with subattributes into powershell objects .DESCRIPTION Internal function Function iterates through returned data and converts any datatype called subattributes into an Array of PSObjects and writes that back to the original object. This is designed to work on one level of subattribute, more than one level is not supported. SKU's are treated seperatly as they have 2 levels of subattributes so there seperate code inside the cuntion to convert these. .PARAMETER AllResults Mandatory Data retruned from CTKCore Query .EXAMPLE Example of processing Computers With the results returned from $AllResults = Find-CoreTicket -State All -Tickets @("170124-06428") -Attributes Computers The content of $AllResults.result.Computers is: subject number Computers created ------- ------ ------- ------- Test ticket 170124-06428 @{subattributes=; attribute=computers} 2017-01-24 08:27:44 Within Computers it's structured: subattributes attribute ------------- --------- @{660537=; 560559=; 560570=} computers within each Computers the structure is 660537 ------ @{os_type=Microsoft Windows; os_name=Windows 2012 R2 Datacenter - 64 bit; nickname=DB1; number=660537; name=660537-DB1.rc.com} Device attributes are listed under Computers.subattributes. This function will copy the Computers attributes from the subattribute level and replace Computers with an array of pso's. This simplifies the data structure and allows you to access the device attributes from $AllTickets.result.Computers .EXAMPLE Example of processing subtickets With the results returned from $AllResult = Find-CoreTicket -State Open -Tickets @("170124-06428") -Subattributes Subtickets The content of $AllTickets.result is: created subject number subtickets ------- ------- ------ ---------- 2017-01-24 08:27:44 Test ticket 170124-06428 @{subattributes=System.Object[]; attribute=subtickets} Within subtickets it's structured: subattributes attribute ------------- --------- {@{subject=Test ticket; number=170124-06441; created=2017-01-24 08:29:32}, @{subject=Test ticket; number=170124-06438; created=2017-01-24 08:28:51}} subtickets within each ticket the structure is created number subject ------- ------ ------- 2017-01-24 08:29:32 170124-06441 Test ticket 2017-01-24 08:28:51 170124-06438 Test ticket 2017-01-24 08:29:32 170124-06441 Test ticket 2017-01-24 08:28:51 170124-06438 Test ticket .OUTPUTS Result object from Core API with aubattributes converted to arrays of PSO's. .NOTES Written by Mark Wichall #> Param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $AllResult ) Try { #Create a temporay object for All results. $TmpAllResult = $AllResult.PsObject.Copy() Write-Verbose "Start of $($MyInvocation.MyCommand)" #Loop through each item retruned in the data e.g. each ticket for ticketwhere ForEach ($Result in $TmpAllResult.result) { #Get the note properties of the current item in the array one level deep for ticket it would be subject, number, created... $NoteProperties = $result | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue #Loop these properties checking for subattributes ForEach ($NoteProperty in $NoteProperties) { #Write-Verbose "$($NoteProperty.name) = $($Result.$($NoteProperty.name))" #checks if any item named subattributes exist, if none exist gm would error which is why error action slient continue is used. $PropertyNames = $SubattributeCount = $null $PropertyNames = ($($Result.$($NoteProperty.name)) | Get-Member -ErrorAction SilentlyContinue -MemberType NoteProperty).Name $SubattributeCount = ( $PropertyNames | Where-object {$_ -eq 'subattributes'}).count #If property is SKUS then run this code spefically for SKU's to itterate through the 2 levels of sub attributes. If ($($NoteProperty.name) -eq "skus") { Write-Verbose "SKUs found" $Skus = $Result."skus".subattributes [Array]$SKUarr = $Null ForEach ($Sku in $SKUs) { $SKUarr += New-Object PSObject -Property @{ SKU = $Sku.Sku Value = $Sku.part_attributes.subattributes.value ValueLabel = $Sku.part_attributes.subattributes."sku_attribute.description" Label = $sku.label Description = $sku.Description } } $Result."skus" = $SKUarr #Write-Verbose "skus" } Else { #If the item subattributes exists ruin this code If ($SubattributeCount -gt 0) { Write-Verbose "Subattributes name found" #Attemtps to get any subattributes, if none exist gm would error which is why error action slient continue is used. $SubattributeName = $($Result.$($NoteProperty.name)).subattributes | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue #If Subattributes name exists and items are found inside subattributes copy them to an array of PSO's and write them back to $result If ($($SubattributeName.count) -gt 0) { #Write-Verbose "Subattributes found under $($NoteProperty.name)" #Array of subattribute names to loop through [Array]$SubNames = ($SubattributeName.name) #New Array of PSOs to add Subatributes to [Array]$ArraySubatributes = $null #Count of NoteProperties under subattribute $SubattributeFeildNote = $($Result.$($NoteProperty.name)).subattributes.$($SubNames[0]) | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue #If count eq 0 then item is most likely an array of text #If count gt 0 then one item is a noteproperty If ($SubattributeFeildNote.count -gt 0) { #Loop through subattribute names for example if the subattribute was Computers it loops through the device id's ForEach ($Name in $SubNames) { #Write-Verbose "Processing Subattributes Names Array" #Gets the Subbattribute feild names e.g. for Computers it might be device id and status e.g. online/complete $SubattributeFeildNames = $($Result.$($NoteProperty.name)).subattributes.$Name | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue #Create new PSO to add to $ArraySubatributes $PSOSubatributes = New-Object PSObject ForEach ($Field in $SubattributeFeildNames) { #Write-Verbose "$($Field.name) = $($($Result.$($NoteProperty.name)).subattributes.$Name.$($Field.name))" #Adding Subattribute data to PSO Add-Member -InputObject $PSOSubatributes -MemberType NoteProperty -Name "$($Field.name)" -Value "$($($Result.$($NoteProperty.name)).subattributes.$Name.$($Field.name))" -Force } #Add PSO of subattribute to array $ArraySubatributes += $PSOSubatributes } } Else { #Write-Verbose "Processing Subattributes Names ArrayList" #Loop through each item in array of subattributes e.g. each subticket retruned. ForEach ($row in $($($Result.$($NoteProperty.name)).subattributes)) { #Get names of each c$(ollumn .Name) $SubattributeFeildNames = $row | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue #Create new PSO to add to $ArraySubatributes $PSOSubatributes = New-Object PSObject ForEach ($Name in $($SubattributeFeildNames.name)) { #Write-Verbose "$name = $($row.$($name))" #Adding Subattribute data to PSO Add-Member -InputObject $PSOSubatributes -MemberType NoteProperty -Name "$name" -Value "$($row.$($name))" -Force } #Add PSO of subattribute to array $ArraySubatributes += $PSOSubatributes } } Write-Verbose "Writting exspanded subattributes back to $($NoteProperty.name)" #Updates orignal object exspanded sub attributes $Result.$($NoteProperty.name) = $ArraySubatributes } Else { Write-Verbose "Writting blank subattributes back e.g. .$($NoteProperty.name)='' " #No items found under subattributes to blank it out. $Result.$($NoteProperty.name) = "" } } } } } Write-Verbose "End of $($MyInvocation.MyCommand)" #Return the result Return $TmpAllResult } Catch { $Reason = "ERROR: converting returned data from core to Powershell Objects, line number: $($_.InvocationInfo.ScriptLineNumber) Msg:: $($_.Exception.Message)" $Params = @{ FunctionName = "$($MyInvocation.MyCommand)" CallingScript = "$($MyInvocation.ScriptName)" Line = "$($_.invocationInfo.ScriptLineNumber)" ErrorMsg = "$($_.exception.message)" CommandName = "$($_.exception.CommandName)" Variables = $(Get-Variable -Scope Local -ErrorAction SilentlyContinue) Version = "$version" Reason = "$Reason" } Export-PowershellError @Params Throw "$Reason" } } Function Invoke-CoreQuery { <# .SYNOPSIS Main internal function to perform querys against CTKAPI .DESCRIPTION Internal function Main function used to perform Core API calls. You should not call this function directly. Use the wrapper functions like Invoke-CoreWherewrapper and Invoke-CoreMethodWrapper as you are likely to get the syntax incorrect calling this directly, its intentially light on its param validation as this is done further up the script This function was designed to take away the need to manually type out the json for a CTKAPI call. These are prone to error, difficult and frustrating to work with. By using Powershell objects all the way down, it makes working with the CTKAPI a lot more Powershell friendly and reliable. Other advantages: 1) Easier to work with Powershell object 2) Validation when converting to JSON 3) Can update offset values in a Where query easily There are 3 query types in Core: attribute querys, methods (actions) and setting attributes This function takes a Powershell JSON object in order to perform these 3 different queries Core documentation: WhereStatement: https://ws.core.rackspace.com/ctkapi/doc/query/#attributes Method: https://ws.core.rackspace.com/ctkapi/doc/query/#method SetAttribute :https://ws.core.rackspace.com/ctkapi/doc/query/#set_attribute This function: - Validates various types of input - Retrieves and validates the Core token - Builds the Powershell object - Converts said object to the CTKAPI structured JSON - If a where statement it will handle limit and offset to avoid timeouts .PARAMETER JsonHashtable Mandatory This is the powershell object that will be converted to JSON for the CTKAPI call. This is created by the Wrapper functions, so you would need to review the logic in those in order to get a full understanding. However here is the rough structre to the object for 3 types of call: ==================== WhereStatement: ==================== $Class = "Ticket" $Attributes = Get-CoreTicketAttributes $SearchTermsArray = [System.Collections.ArrayList]@("account","in",[System.Collections.ArrayList]@(1103359)) $QueryLimit = 100 [Hashtable]$JsonHashtable = @{ "class" = "$Class.$Class"; "attributes" = $Attributes "load_arg" = @{ "class" = "$Class.$Class`Where" "values" = $SearchTermsArray "offset" = 0 "limit" = $QueryLimit } } $Class is the CTKAPI class you are searching, like Ticket $Attributes is a hashtable of attributes to return. Use Get-CoreTicketAttributes to get this $SearchTermsArray is the array of search terms. the Main Find-Core functions generate this, but here is an example: $JsonHashtable.load_arg Name Value ---- ----- offset 0 values {account, in, 1103359} limit 100 class Ticket.TicketWhere These can be pretty complex, depending on how accurate you want the query to be ==================== Method: ==================== [Hashtable]$JsonHashtable = @{ "class" = "$Class.$Subclass"; "method" = $Method "load_arg" = $LoadArgument "keyword_args" = $KeywordArgs "args" = @() #required by core even though blank } $Class is the CTKAPI class you are searching, like Ticket $SubClass is the CTKAPI class under the main $Class you are searching, like Queue $Method is the name of the method, like setStatusByName $KeywordArgs is a hashtable of the arguments for the method. This can be blank if it doesnt have any setStatusByName has one: @{status_name = "Confirm Solved"} args is added but left blank, this is a requirement to keep Core happy Here is an example of the output of $JsonHashtable Name Value ---- ----- load_arg 170131-09161 method setStatusByName args {} class Ticket.Queue keyword_args {status_name} ==================== SetAttribute ==================== .PARAMETER QueryLimit The number of results to return before offsetting and breaking up the query output Default 100 .PARAMETER Url Set the Core URL to use in invoke-restmethod Defaults to the CTKAPI URL You will need to specify this if you are using Get or some other non-standard API call Used in Find-CoreAttributeValues .PARAMETER Method Set the Core query method to use in invoke-restmethod Defaults to post You will need to specify this if you are using Get or some other non-standard API call Used in Find-CoreAttributeValues .EXAMPLE Its hard to show an example due to the complex nature, but here is an example of a Where query $Class = "Ticket" $Attributes = Get-CoreTicketAttributes $SearchTermsArray = [System.Collections.ArrayList]@("account","in",[System.Collections.ArrayList]@(1103359)) $QueryLimit = 100 [Hashtable]$JsonHashtable = @{ "class" = "$Class.$Class"; "attributes" = $Attributes "load_arg" = @{ "class" = "$Class.$Class`Where" "values" = $SearchTermsArray "offset" = 0 "limit" = $QueryLimit } } Invoke-CoreQuery -JsonHashtable $JsonHashtable .EXAMPLE A method call [Hashtable]$JsonHashtable = @{ "class" = "Ticket.Queue"; "method" = "setStatusByName" "load_arg" = "170131-09161" "keyword_args" = @{status_name = "Confirm Solved"} "args" = @() #required by core even though blank } Invoke-CoreQuery -JsonHashtable $JsonHashtable .EXAMPLE A get instead of a post This would get the attribute values for a Ticket Classifications Invoke-CoreQuery -Url "https://ws.core.rackspace.com/ctkapi/values/Ticket.Classification" -Method Get .OUTPUTS A CTKAPI object Example: count : 1 load_arg : @{limit=100; values=System.Object[]; class=Ticket.TicketWhere; offset=0} limit : 100 result : {@{queue_name=INTL Windows; number=161125-03022; private=0; assignee_name=Martin Howlett; queue_id=683; submitter=Martin Howlett; subject=Test ticket}} offset : 0 class : Ticket.Ticket .NOTES Written by Martin Howlett #> [CmdletBinding()] Param ( [Parameter()] [ValidateNotNullOrEmpty()] [Hashtable]$JsonHashtable, [Parameter()] [ValidateRange(1,249)] [int]$QueryLimit = 100, [Parameter()] [ValidateNotNullOrEmpty()] [String]$Url = "https://ws.core.rackspace.com/ctkapi/query", [Parameter()] [Validateset("Post","Get")] [ValidateNotNullOrEmpty()] [String]$Method = "Post" ) Write-Verbose "Start of $($MyInvocation.MyCommand)" #Retrieve our core token Try { $CoreToken = $null $CoreToken = Get-PoshCoreToken -AsPlainText } catch { Throw "Get-PoshCoreToken threw error. Line number: $($_.InvocationInfo.ScriptLineNumber) Msg:: $($_.Exception.Message)" } if(-not $CoreToken) { $Reason = "Get-PoshCoreToken did not return valid Core token" $Params = @{ FunctionName = "$($MyInvocation.MyCommand)" CallingScript = "$($MyInvocation.ScriptName)" Line = "$($_.invocationInfo.ScriptLineNumber)" ErrorMsg = "$($_.exception.message)" CommandName = "$($_.exception.CommandName)" Variables = $(Get-Variable -Scope Local -ErrorAction SilentlyContinue) Version = "$version" Reason = "$Reason" } Export-PowershellError @Params Throw "$Reason" } #check if the load_arg includes a where statement. if it is we know its a where query $WhereAttribute = $false if($JsonHashtable.load_arg.class -match "Where") { $WhereAttribute = $true } <# this is not a [System.Collections.ArrayList] because if the queury returns an array under the limit we end up adding an array to an array, which breaks everything later. By using this we rebuilt the array each time so it is consistant in all examples #> [Array]$Results = @() <# limit the returned results to number in $QueryLimit and loop them until we get all results this stops timeouts by only getting X objects returned per query and not everything intensive queries need a pretty low query limit, but we default to 100 #> $OutputOffset = 0 $TotalQueryTime = 0 do { if($WhereAttribute) { #in where statement queries, we set the offset and limit to control how much data we get back in one query $JsonHashtable.load_arg.offset = $OutputOffset $JsonHashtable.load_arg.limit = $QueryLimit Write-Verbose "offset: $OutputOffset" Write-Verbose "QueryLimit: $QueryLimit" } #################### JSON processiong - BEGIN #################### #region #only process json if we have are doing a post if($Method -eq "Post") { Try { $Json = $JsonHashtable | ConvertTo-Json -Depth 10 -ErrorAction Stop } catch { $Reason = "Failed to convert JSON $Json. Cannot continue" $Params = @{ FunctionName = "$($MyInvocation.MyCommand)" CallingScript = "$($MyInvocation.ScriptName)" Line = "$($_.invocationInfo.ScriptLineNumber)" ErrorMsg = "$($_.exception.message)" CommandName = "$($_.exception.CommandName)" Variables = $(Get-Variable -Scope Local -ErrorAction SilentlyContinue) Version = "$version" Reason = "$Reason" } Export-PowershellError @Params Throw "$Reason" } Try { #ConvertTo-Json escapes '\' and '"' to '\\' and '\"'. That is fine. But it also escapes, say, '&' #to '\u0026'. We need those unescaped, but we need to leave the simple escapes alone. #IAWW-1664 $Regex = New-Object regex ( "\\u(?[a-zA-Z0-9]{4})", [System.Text.RegularExpressions.RegexOptions]::Compiled ) #Matches unicode escape sequences, e.g. '\u0026'. Puts hexadecimal part of match into named group 'Value' $MatchEvaluator = { <# .Synopsis Return replacement text when used in Regex.Replace(). .Link https://msdn.microsoft.com/en-us/library/cft8645c(v=vs.110).aspx #> param( [System.Text.RegularExpressions.Match]$Match ) $CharValue = [int]::Parse( $Match.Groups["Value"].Value, [System.Globalization.NumberStyles]::HexNumber ) return ([char]$CharValue).ToString() } #Each regex match object found in $Json gets passed to the match evaluator $Json = $Regex.Replace( $Json, $MatchEvaluator ) } catch { $Reason = "Failed to unescape JSON $Json. Cannot continue" $Params = @{ FunctionName = "$($MyInvocation.MyCommand)" CallingScript = "$($MyInvocation.ScriptName)" Line = "$($_.invocationInfo.ScriptLineNumber)" ErrorMsg = "$($_.exception.message)" CommandName = "$($_.exception.CommandName)" Variables = $(Get-Variable -Scope Local -ErrorAction SilentlyContinue) Version = "$version" Reason = "$Reason" } Export-PowershellError @Params Throw "$Reason" } #Search each line of the JSON, if load_arg is blank or NULL then cancel ForEach ($line in $JSON) { IF (($line -like "*load_arg*`"`"*") -OR ($line -like "*load_arg*`"NULL`"*")) { Throw "Blank or null value detected in line $line in PoshCore module. Cannot continue" } } #only write to screen if we hit out limit (and thus have to loop multiple times) if($OutputOffset -gt 0) { Write-Host "Invoking additional query from Core due to large query - results $OutputOffset to $($OutputOffset + $QueryLimit)..." } else { #only write the json out once, even with verbose output, else it floods the screen Write-Verbose "JSON:" Write-Verbose $Json } } #endregion #################### JSON processiong - END ##################### #################### Throttling - BEGIN #################### #region #Throttling Vars [String]$Key = "PoshCore" [Int]$RequestLimit = 14 #Check if registry path exists If (-Not (Test-Path "HKCU:\Software\$Key")) { Try { #Create reg keys for throtelling if they don't exist New-item "HKCU:\Software\$Key\" -Force -ErrorAction Stop New-ItemProperty -Path "HKCU:\Software\$Key\" -Name "FirstRequest" -Value "$(Get-Date)" -Force -ErrorAction Stop New-ItemProperty -Path "HKCU:\Software\$Key\" -Name "NumberOfRequests" -Value "1" -Force -ErrorAction Stop } Catch { $Reason = "ERROR: Error creating registry path and keys for throttling 'HKCU:\Software\$Key\'" $Params = @{ FunctionName = "$($MyInvocation.MyCommand)" CallingScript = "$($MyInvocation.ScriptName)" Line = "$($_.invocationInfo.ScriptLineNumber)" ErrorMsg = "$($_.exception.message)" CommandName = "$($_.exception.CommandName)" Variables = $(Get-Variable -Scope Local -ErrorAction SilentlyContinue) Version = "$version" Reason = "$Reason" } Export-PowershellError @Params Throw "$Reason" } } Else { #Gets item properties [DateTime]$FirstRequest = (Get-ItemProperty -Path HKCU:\Software\$Key -Name FirstRequest).FirstRequest [Int]$NumberOfRequests = (Get-ItemProperty -Path HKCU:\Software\$Key -Name NumberOfRequests).NumberOfRequests #10 second time span for requests $timespan = new-timespan -seconds 10 #checks the current date minus the first request time is less then the time span $currentdate = get-date $DateDiff = ($currentdate) - $FirstRequest If ($DateDiff -gt $timespan) { #timespan over 10 seconds Set-ItemProperty -Path HKCU:\Software\$Key -Name FirstRequest -Value "$(Get-Date)" -Force -ErrorAction Stop Set-ItemProperty -Path HKCU:\Software\$Key -Name NumberOfRequests -Value 1 -Force } Else { #timespan under 10 seconds #increments NumberOfRequests by 1 $NumberOfRequests = $NumberOfRequests + 1 If ($NumberOfRequests -gt $RequestLimit) { # 10 seconds minus the current number of seconds plus 2 #For example if 15 requests occure in 8 seconds, the delayed would be the remained from 10 e.g. 2 seconds plus an additional 2 second delay. $PauseLength = (10 - $($DateDiff.Seconds)) + 2 Write-Warning "Throttling: $(Get-Date) Number of requests over $RequestLimit since $FirstRequest Paused for $PauseLength Seconds" Start-Sleep $PauseLength #reset the values Set-ItemProperty -Path HKCU:\Software\$Key -Name FirstRequest -Value "$(Get-Date)" -Force -ErrorAction Stop Set-ItemProperty -Path HKCU:\Software\$Key -Name NumberOfRequests -Value 1 -Force } Else { #increment number of requests by 1 Set-ItemProperty -Path HKCU:\Software\$Key -Name NumberOfRequests -Value $NumberOfRequests -Force } } } #endregion #################### Throttling - END ##################### Write-Verbose "Core query start: $(Get-Date)" [Hashtable]$QueryParams = @{ Uri = $Url Method = $Method Headers = @{"X-Auth"="$CoreToken"} TimeoutSec = 180 ErrorAction = "Stop" } #add body if doing post if($Method -eq "Post") { $QueryParams.Add("Body",$JSON) } #Loop and pause if 503 error recived from core. [Int]$TryCount = 0 Do { [Switch]$503Error = $False $QueryResult = $null $TryCount++ Try { $Time = Measure-Command {$QueryResult = Invoke-RestMethod @QueryParams} } catch { $Params = @{ FunctionName = "$($MyInvocation.MyCommand)" CallingScript = "$($MyInvocation.ScriptName)" Line = "$($_.invocationInfo.ScriptLineNumber)" ErrorMsg = "$($_.exception.message)" CommandName = "$($_.exception.CommandName)" Variables = $(Get-Variable -Scope Local -ErrorAction SilentlyContinue) Version = "$version" Reason = "" } #Set to true if there is a terminating error [Switch]$TerminatingError = $false switch -Wildcard ($_) { "*Invalid Token*" {$Reason = "ERROR PoshCore: Invoke-Query :: Invalid Racker token";$TerminatingError = $true} "*401*" {$Reason = "ERROR PoshCore: Invoke-Query :: 401 Unauthorized";$TerminatingError = $true} "*404*" {$Reason = "ERROR PoshCore: Invoke-Query :: 404 URL $URL not found";$TerminatingError = $true} "*500*" {$Reason = "ERROR PoshCore: Invoke-Query :: 500 Internal Server Error";$TerminatingError = $true} "*503*" { If($503Error) { #Retryed request and still failed with a 503 $Reason = "ERROR PoshCore: Invoke-Query :: 503 Throttling - Server Unavailable, will pause and retry request" $TerminatingError = $true } Else { #Failed on first try with a 503, not terminating error. Retry the query and fail second time if it fails again. $503Error=$true Write-Warning "ERROR PoshCore: Invoke-Query :: 503 Throttling - Server Unavailable, will pause and retry request" } } default {$Reason = "ERROR PoshCore: $_";$TerminatingError = $true} } #If a terminating error occures export error and throw. If ($TerminatingError) { $Params.Reason = "$Reason" Export-PowershellError @Params Throw "$Reason" } } #If Error 503 hit pause for 10 seconds then retry query #Also updates regirst to 15 requests and current time so will throttle all other threads. If ($503Error) { Write-Warning "Throttling: Pausing for 10 seconds before retrying request to Core" Set-ItemProperty -Path HKCU:\Software\$Key -Name FirstRequest -Value "$(Get-Date)" -Force -ErrorAction Stop Set-ItemProperty -Path HKCU:\Software\$Key -Name NumberOfRequests -Value 15 -Force Start-Sleep 10 } } While ($503Error -and $TryCount -lt 2) #Repeat again if error 503 hit and first attempt Write-Verbose "Core query end: $(Get-Date)" Write-Verbose "Total time of query: $($Time.TotalSeconds)" #Add-Member -InputObject $QueryResult -MemberType NoteProperty -Name QueryTime -Value $Time.TotalSeconds -Force #Checks the retruned object has data, to stop adding member to fail with error If (($QueryResult | Measure-Object).count -gt 0) { $QueryResult | Add-Member -MemberType NoteProperty -Name QueryTime -Value $($Time.TotalSeconds) } #update our total time $TotalQueryTime += $Time.TotalSeconds #collate our results $Results += $QueryResult #increase the offset $OutputOffset = $OutputOffset + $QueryLimit } Until ((($QueryResult | Select-Object Count).Count -lt $QueryLimit) -or (-not $WhereAttribute)) #we want to loop until the result is less then the query limit (there are no more results) or if its not a where query, then we dont need to loop at all Write-Verbose "TOTAL QUERY TIME: $TotalQueryTime" Write-Verbose "End of $($MyInvocation.MyCommand)" return $Results }