lib/functions/Private/Invoke-OmadaViewLookupPipeline.ps1
|
function Invoke-OmadaViewLookupPipeline { <# .SYNOPSIS Run both halves of the "SQL Troubleshooting" view lookup - find the view, then fetch its rows - as one sequence, and return what the UI needs to apply it. .DESCRIPTION Issue #90, slice A. Get-SqlTroubleShooterView makes two dependent GetPagingData round-trips: the second needs the view id the first returns, so they cannot be fired off in parallel. That pair is what Update-DataConnectionList blocks on before it makes its own request, and what Update-QueryList blocks on whenever a "my queries" filter is on. Two chained background completions would work, and it is deliberately NOT what this is - for the reason Invoke-OmadaExecutePipeline gives at greater length and that issue #90 names as the dominant risk of this whole slice: between completions, Set-ActiveTabContext can repoint $Script:MainForm.Elements, $Script:RunTimeData and $Script:AppConfig onto a different tab. Two completions is a smaller version of that defect, not a different one. One background job means ONE completion, so the tab-context risk is removed rather than managed. Runspace-safe, on the same terms as Invoke-OmadaExecutePipeline: no $Script: reads, no logging, no WPF, and no calls into this module beyond New-OmadaPagingRequest and Invoke-OmadaRequestCore, which are equally pure. What it cannot do - write the log, touch a dropdown - it does not attempt; it returns a description of what happened and the UI thread applies it. .PARAMETER Context Plain values gathered on the UI thread: BaseUrl the tenant base URL Parameters the prepared Invoke-OmadaRestMethod splat (Uri, Method and Body are overwritten per step; everything else - SessionKey, authentication, redaction - carries) IncludeDataObjectHtml $true to also fetch the dataobjdlg.aspx page for the first row, which is where the data connection options live. This is Update-DataConnectionList's third round-trip, and it is a step of THIS job rather than a second background request chained off this one's completion - for the reason above. Update-QueryList needs only the first two steps and omits it. SqlQueryDoIdField the row property holding the data object id, from $Script:RunTimeData.DataobjdlgAspxAttributeMapping. Passed in because that mapping is UI-thread state. Required when IncludeDataObjectHtml is set. .OUTPUTS Hashtable: Rows the view's data object rows, or $null when the view was not found ViewId the id of the "SQL Troubleshooting" view, or $null ViewFound $false when the tenant answered but holds no such view - which is a legitimate answer, not a failure, and must not be reported as one DataObjectHtml the dataobjdlg.aspx response, when IncludeDataObjectHtml was set and there was a row to fetch it for; otherwise $null ErrorRecord the first failure, or $null FailedStep which step failed, or $null CompletedSteps how many requests came back without an error Steps an ordered trace of @{ Name; Method; Uri } Log an ordered list of log entries for the UI thread to replay #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$Context ) $Outcome = @{ Rows = $null ViewId = $null ViewFound = $false DataObjectHtml = $null ErrorRecord = $null FailedStep = $null Steps = [System.Collections.Generic.List[object]]::new() CompletedSteps = 0 Log = [System.Collections.Generic.List[object]]::new() } # The same two log shapes the execute pipeline uses, for the same reason: redaction # (ConvertTo-RedactedLogString) is a UI-thread function and must stay one, so the worker records # WHAT to log and the UI thread decides how much of it may be written down. $Log = { param($Level, $Text) $Outcome.Log.Add(@{ Level = $Level; Text = $Text }) } $LogObject = { param($Level, $Format, $Object, $ShapeOnly) $Outcome.Log.Add(@{ Level = $Level; Format = $Format; Redact = $Object; ShapeOnly = [bool]$ShapeOnly }) } # One local helper rather than a call out to another file: this runs in a worker runspace, and # every extra name it depends on is another thing that has to be dot-sourced there. $Invoke = { param($StepName, $Request) $Parameters = $Context.Parameters.Clone() $Parameters.Uri = $Request.Uri $Parameters.Method = $Request.Method if ($null -eq $Request.Body) { if ($Parameters.ContainsKey("Body")) { $Parameters.Remove("Body") } } else { $Parameters.Body = $Request.Body } $Outcome.Steps.Add(@{ Name = $StepName; Method = $Request.Method; Uri = $Request.Uri }) & $Log "DEBUG" ("QueryUrl: {0}" -f $Request.Uri) if ($null -ne $Request.Body) { & $LogObject "VERBOSE" "Body: {0}" $Request.Body $true } & $LogObject "VERBOSE" "Parameters: {0}" $Parameters $false $StepOutcome = Invoke-OmadaRequestCore -Parameters $Parameters if ($null -eq $StepOutcome.ErrorRecord) { $Outcome.CompletedSteps = $Outcome.CompletedSteps + 1 & $LogObject "VERBOSE" "Result: {0}" $StepOutcome.Result $false } else { # DEBUG, not ERROR: the pipeline reports its failure through the outcome, and the caller # decides how loudly to say so. An ERROR written from here would be written twice. & $Log "DEBUG" ("Step '{0}' failed: {1}" -f $StepName, $StepOutcome.ErrorRecord.Exception.Message) } return $StepOutcome } try { # --- 1. Find the "SQL Troubleshooting" view ------------------------------------------------ & $Log "DEBUG" "Retrieve data connections" $Private:ViewLookup = & $Invoke "FindView" (New-OmadaPagingRequest -DataType "Views" -DataTypeArgs @{ OwnerShipType = "Both" } -SearchString "SQL Troubleshooting" -BaseUrl $Context.BaseUrl) if ($null -ne $Private:ViewLookup.ErrorRecord) { $Outcome.ErrorRecord = $Private:ViewLookup.ErrorRecord $Outcome.FailedStep = "FindView" return $Outcome } # Records greater than zero before reading Rows, exactly as Get-SqlTroubleShooterView has # always checked. The search is a contains-match on the name, so the result may hold other # views; the exact-name filter is what picks this one out. $Private:View = $null if ($null -ne $Private:ViewLookup.Result -and $Private:ViewLookup.Result.d.Records -gt 0) { $Private:View = $Private:ViewLookup.Result.d.Rows | Where-Object { $_.Name -eq "SQL Troubleshooting" } | Select-Object -First 1 } if ($null -eq $Private:View) { # A tenant without the view is not a failed request. Reporting it through ErrorRecord # would send the caller down the retry-on-the-UI-thread path, which would ask the same # question again and get the same answer. & $Log "DEBUG" "The 'SQL Troubleshooting' view was not found." return $Outcome } $Outcome.ViewFound = $true $Outcome.ViewId = $Private:View.Id # --- 2. Fetch the view's rows --------------------------------------------------------------- $Private:RowRequest = New-OmadaPagingRequest -DataType "DataObjects" -BaseUrl $Context.BaseUrl -DataTypeArgs ([ordered]@{ viewId = ("{0}" -f $Private:View.Id) pageQueryString = ("{0}/dataobjlst.aspx?view={1}" -f $Context.BaseUrl, $Private:View.Id) readOnlyMode = $false countRows = $false }) $Private:Rows = & $Invoke "FetchViewRows" $Private:RowRequest if ($null -ne $Private:Rows.ErrorRecord) { $Outcome.ErrorRecord = $Private:Rows.ErrorRecord $Outcome.FailedStep = "FetchViewRows" return $Outcome } # Normalised to an array, and never left as $null. A jqGrid payload for a view with nothing in # it has a null .d.Rows, and @($null).Count is 1 - so "did I get rows?" answered yes for an # empty view, and the caller reported it as a failed fetch and disabled the dropdown. Callers # should not have to know that; the outcome carries an array or an empty array. $Outcome.Rows = @($Private:Rows.Result.d.Rows | Where-Object { $null -ne $_ }) # --- 3. The data connection page, when the caller wants it --------------------------------- if (-not $Context.IncludeDataObjectHtml) { return $Outcome } $Private:FirstRow = @($Outcome.Rows) | Select-Object -First 1 if ($null -eq $Private:FirstRow) { # No rows means no data object to open, which Update-DataConnectionList already treats as # "cannot change the data connection". Not an error. & $Log "DEBUG" "The SQL Troubleshooting view returned no rows; no data connection page to fetch." return $Outcome } $Private:DataObjectId = $Private:FirstRow.$($Context.SqlQueryDoIdField) $Private:Html = & $Invoke "FetchDataConnectionPage" (@{ Method = "GET" Uri = "{0}/dataobjdlg.aspx?DOID={1}" -f $Context.BaseUrl, $Private:DataObjectId Body = $null }) if ($null -ne $Private:Html.ErrorRecord) { $Outcome.ErrorRecord = $Private:Html.ErrorRecord $Outcome.FailedStep = "FetchDataConnectionPage" return $Outcome } $Outcome.DataObjectHtml = $Private:Html.Result return $Outcome } catch { # A throw from here would surface as a worker that died rather than as an answer, and the # caller cannot classify that. Report it the same way a failed step is reported. $Outcome.ErrorRecord = $_ if ($null -eq $Outcome.FailedStep) { $Outcome.FailedStep = "ViewLookup" } return $Outcome } } |