Private/ConvertTo-PSUComponentData.ps1

function ConvertTo-PSUComponentData {
    <#
    .SYNOPSIS
        Turns whatever a query returned into rows a component can send to the browser.
    .DESCRIPTION
        Accepts a System.Data.DataTable or DataView (SqlClient, ODBC, OleDb), DataRow objects,
        PSCustomObjects (Invoke-RestMethod, Select-Object, Import-Csv), hashtables, scalars, or a script
        block that returns any of these. Returns an array of PSCustomObjects:
        - DBNull and $null become $null, so the browser sees null instead of an empty object
        - DateTime and DateTimeOffset become ISO 8601 strings, so dates survive serialization
        - TimeSpan becomes seconds (a number), enums their name
        - scalars (numbers, dates, GUIDs, strings) become one column "value"
        - column order follows the source
        PSCustomObjects because PSU serializes an [ordered] dictionary as an array of key/value pairs
        (measured on PSU 2026.3.4). Shared by every component module of this repository; compiled
        into each module at build time.
    .PARAMETER InputObject
        The data, or a script block returning it.
    .EXAMPLE
        ConvertTo-PSUComponentData -InputObject $dataTable
    .EXAMPLE
        ConvertTo-PSUComponentData -InputObject { Invoke-RestMethod 'https://api.example.com/items' }
    .OUTPUTS
        System.Management.Automation.PSCustomObject[]
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param([AllowNull()][object]$InputObject)

    if ($null -eq $InputObject) { return , @() }
    if ($InputObject -is [scriptblock]) { $InputObject = & $InputObject }

    $rows = if ($InputObject -is [System.Data.DataTable]) { $InputObject.Rows }
    elseif ($InputObject -is [System.Data.DataView]) { $InputObject | ForEach-Object Row }
    else { @($InputObject) }

    $result = @(foreach ($row in $rows) {
            if ($null -eq $row -or $row -is [System.DBNull]) { continue }
            $out = [ordered]@{}
            if ($row -is [System.Data.DataRowView]) { $row = $row.Row }
            if ($row -is [System.Data.DataRow]) {
                foreach ($col in $row.Table.Columns) { $out[$col.ColumnName] = ConvertTo-PSUComponentValue $row[$col] }
            }
            elseif ($row -is [System.Collections.IDictionary]) {
                foreach ($key in $row.Keys) { $out[[string]$key] = ConvertTo-PSUComponentValue $row[$key] }
            }
            elseif ($row -is [string] -or $row -is [valuetype]) {
                # numbers, dates, GUIDs, enums, booleans: one column "value" instead of their properties
                $out['value'] = ConvertTo-PSUComponentValue $row
            }
            else {
                foreach ($prop in $row.PSObject.Properties) {
                    if ($prop.MemberType -in 'NoteProperty', 'Property', 'AliasProperty', 'ScriptProperty') {
                        $out[$prop.Name] = ConvertTo-PSUComponentValue $prop.Value
                    }
                }
            }
            [pscustomobject]$out
        })
    , $result
}

function ConvertTo-PSUComponentValue {
    <#
    .SYNOPSIS
        Converts one cell value to a JSON-safe value (see ConvertTo-PSUComponentData).
    .PARAMETER Value
        The value.
    .EXAMPLE
        ConvertTo-PSUComponentValue ([DBNull]::Value)
    #>

    [CmdletBinding()]
    param([AllowNull()][object]$Value)
    if ($null -eq $Value -or $Value -is [System.DBNull]) { return $null }
    if ($Value -is [datetime] -or $Value -is [datetimeoffset]) { return $Value.ToString('o') }
    if ($Value -is [timespan]) { return $Value.TotalSeconds }
    if ($Value -is [enum]) { return [string]$Value }
    $Value
}