Private/Objects.ps1

# Objects the commands output, and the lookups they share.

function ConvertTo-PaAlias {
    <#
    .SYNOPSIS
    A PagerArchive.Alias from the API's alias row.

    .DESCRIPTION
    Its properties are the import's parameters (guidelines section 3), so
    Get-PagerArchiveAlias | Export-Csv writes the import format.
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Alias')]
    param(
        # The API's row.
        [Parameter(Mandatory, ValueFromPipeline)]
        [object] $Row
    )
    process {
        [pscustomobject]@{
            PSTypeName = 'PagerArchive.Alias'
            Pattern    = $Row.pattern
            Name       = $Row.name
            Template   = if ($Row.template) { $Row.template } else { '' }
            Colour     = if ($Row.colour) { $Row.colour } else { '' }
            Ignored    = [bool]$Row.ignored
            Id         = [long]$Row.id
        }
    }
}

function ConvertTo-PaTemplate {
    <#
    .SYNOPSIS
    A PagerArchive.Template from the API's template row.
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Template')]
    param(
        # The API's row.
        [Parameter(Mandatory, ValueFromPipeline)]
        [object] $Row
    )
    process {
        [pscustomobject]@{
            PSTypeName    = 'PagerArchive.Template'
            Name          = $Row.name
            Agency        = $Row.agency
            Colour        = if ($Row.colour) { $Row.colour } else { '' }
            AgencyProfile = if ($Row.agency_profile) { $Row.agency_profile } else { '' }
            HasIcon       = [bool]($Row.PSObject.Properties['icon'] -and $Row.icon)
            AliasCount    = [int]$Row.aliases
            Id            = [long]$Row.id
        }
    }
}

function ConvertTo-PaMessage {
    <#
    .SYNOPSIS
    A PagerArchive.Message from the API's message (R63a: what the detail view shows).
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Message')]
    param(
        # The API's message.
        [Parameter(Mandatory, ValueFromPipeline)]
        [object] $Row
    )
    process {
        $location = $null
        if ($Row.location) {
            $location = [pscustomobject]@{
                PSTypeName  = 'PagerArchive.Location'
                Longitude   = [double]$Row.location.lon
                Latitude    = [double]$Row.location.lat
                Approximate = [bool]$Row.location.approximate
                Source      = $Row.location.source
            }
        }
        $fields = [ordered]@{}
        if ($Row.extra) {
            foreach ($p in $Row.extra.PSObject.Properties) {
                $fields[$p.Name] = $p.Value
            }
        }
        $flags = $Row.PSObject.Properties['flags'] -and $Row.flags
        [pscustomobject]@{
            PSTypeName       = 'PagerArchive.Message'
            Id               = [long]$Row.id
            SentAt           = [datetimeoffset]$Row.sent_at
            Capcode          = $Row.capcode
            Alias            = $Row.alias
            Agency           = $Row.agency
            Priority         = if ($Row.priority) { $Row.priority } else { 'None' }
            Text             = $Row.display_text
            RawText          = $Row.text
            Group            = $Row.group
            JobNumber        = $Row.job_number
            EventType        = $Row.event_type
            Category         = $Row.category
            Description      = $Row.description
            Address          = $Row.address
            Locality         = $Row.locality
            GridRef          = $Row.grid_ref
            Units            = @($Row.units | Where-Object -FilterScript { $_ })
            Fields           = [pscustomobject]$fields
            Location         = $location
            ParseFailed      = if ($flags) { [bool]$Row.flags.parse_failed } else { $false }
            AddressUnsplit   = if ($flags) { [bool]$Row.flags.address_unsplit } else { $false }
            LocationMismatch = if ($flags) { [bool]$Row.flags.location_mismatch } else { $false }
            Heartbeat        = [bool]$Row.heartbeat
            Ignored          = [bool]$Row.ignored
            Receptions       = @(foreach ($r in @($Row.PSObject.Properties['receptions'] ? $Row.receptions : @())) {
                    [pscustomobject]@{
                        PSTypeName = 'PagerArchive.Reception'
                        Client     = $r.client
                        ReceivedAt = [datetimeoffset]$r.received_at
                    }
                })
        }
    }
}

function ConvertTo-PaLikePattern {
    <#
    .SYNOPSIS
    Turns a PowerShell wildcard into a case-insensitive LIKE pattern.

    .DESCRIPTION
    * becomes %, ? becomes _, and %, _ and \ are escaped, so an exact name
    matches only itself. A [set] becomes _ (any one character), and Exact is
    $false so the caller also checks the wildcard itself.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        # The wildcard.
        [Parameter(Mandatory)]
        [string] $Wildcard
    )
    $like = [System.Text.StringBuilder]::new()
    $exact = $true
    for ($i = 0; $i -lt $Wildcard.Length; $i++) {
        $c = $Wildcard[$i]
        switch ($c) {
            '`' {
                if ($i + 1 -lt $Wildcard.Length) {
                    $i++
                    $n = $Wildcard[$i]
                    if ($n -in [char]'%', [char]'_', [char]'\') { $null = $like.Append('\') }
                    $null = $like.Append($n)
                }
                break
            }
            '*' { $null = $like.Append('%'); break }
            '?' { $null = $like.Append('_'); break }
            '[' {
                $close = $Wildcard.IndexOf(']', $i + 1)
                if ($close -lt 0) {
                    $null = $like.Append('[')
                } else {
                    $null = $like.Append('_')
                    $exact = $false
                    $i = $close
                }
                break
            }
            { $_ -in [char]'%', [char]'_', [char]'\' } { $null = $like.Append('\').Append($c); break }
            default { $null = $like.Append($c) }
        }
    }
    @{ Like = $like.ToString(); Exact = $exact }
}

function Get-PaTemplateList {
    <#
    .SYNOPSIS
    Every template, from the API.
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Template')]
    param()
    Invoke-PaApi -Method GET -Path '/api/v1/admin/templates' | ForEach-Object -Process { $_ } | ConvertTo-PaTemplate
}

function Get-PaAliasList {
    <#
    .SYNOPSIS
    Aliases from the API, a page at a time, written as each page arrives.

    .DESCRIPTION
    Search narrows them on the server (a name, capcode or pattern
    containing it); callers filter exactly.
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Alias')]
    param(
        # Text the name, capcode or pattern contains.
        [string] $Search = ''
    )
    $after = ''
    do {
        $query = [ordered]@{ q = $Search; limit = 500; after = $after }
        $page = Invoke-PaApi -Method GET -Path '/api/v1/admin/aliases' -Query $query
        $rows = @($page.aliases)
        $rows | ConvertTo-PaAlias
        if ($rows.Count) {
            $after = $rows[-1].pattern
        }
    } while ($page.more -and $rows.Count)
}

function Find-PaTemplate {
    <#
    .SYNOPSIS
    The template called Name (case-insensitive), or an error.
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Template')]
    param(
        # The template's name.
        [Parameter(Mandatory)]
        [string] $Name,

        # Templates already fetched.
        [object[]] $From
    )
    if (-not $From) {
        $From = @(Get-PaTemplateList)
    }
    $found = $From | Where-Object -FilterScript { $_.Name -eq $Name } | Select-Object -First 1
    if (-not $found) {
        throw (New-PaErrorRecord -ErrorId 'no_template' -Category ObjectNotFound -Message "No template is called '$Name'. Get-PagerArchiveTemplate lists them." -TargetObject $Name)
    }
    $found
}

function Find-PaAlias {
    <#
    .SYNOPSIS
    The aliases a command acts on: by capcode or pattern, or by name with wildcards.
    #>

    [CmdletBinding()]
    [OutputType('PagerArchive.Alias')]
    param(
        # Capcodes or patterns, exactly (zero-padded or not).
        [string[]] $Pattern,

        # Names, with wildcards.
        [string[]] $Name
    )
    foreach ($p in $Pattern) {
        $p = $p.Trim()
        $key = if ($p -match '^[0-9]{1,7}$') { $p.PadLeft(7, '0') } else { $p }
        $found = Get-PaAliasList -Search $key | Where-Object -FilterScript { $_.Pattern -eq $key }
        if (-not $found) {
            throw (New-PaErrorRecord -ErrorId 'no_alias' -Category ObjectNotFound -Message "No alias has the capcode or pattern '$p'." -TargetObject $p)
        }
        $found
    }
    if ($Name) {
        $all = $null
        foreach ($n in $Name) {
            $wild = [System.Management.Automation.WildcardPattern]::new($n, [System.Management.Automation.WildcardOptions]::IgnoreCase)
            if (-not [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($n)) {
                $found = Get-PaAliasList -Search $n | Where-Object -FilterScript { $wild.IsMatch($_.Name) }
            } else {
                if ($null -eq $all) { $all = @(Get-PaAliasList) }
                $found = $all | Where-Object -FilterScript { $wild.IsMatch($_.Name) }
            }
            if (-not $found -and -not [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($n)) {
                throw (New-PaErrorRecord -ErrorId 'no_alias' -Category ObjectNotFound -Message "No alias is called '$n'." -TargetObject $n)
            }
            $found
        }
    }
}

function New-PaConnectionObject {
    <#
    .SYNOPSIS
    The PagerArchive.Connection object for a connection.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Makes an object in memory; changes nothing.')]
    [CmdletBinding()]
    [OutputType('PagerArchive.Connection')]
    param(
        # The connection.
        [Parameter(Mandatory)]
        [hashtable] $Connection
    )
    $user = $Connection.User
    [pscustomobject]@{
        PSTypeName = 'PagerArchive.Connection'
        Uri        = [uri]$Connection.Uri
        Name       = if ($user -and $user.name) { $user.name } else { '' }
        Email      = if ($user) { $user.email } else { '' }
        Role       = if ($user) { $user.role } else { '' }
        SavedIn    = switch ($Connection.Store) {
            'dpapi' { 'DPAPI file' }
            'vault' { "Vault $($Connection.Vault)" }
            default { 'This session only' }
        }
        ExpiresAt  = $Connection.ExpiresAt
    }
}

function Get-PaImportSummary {
    <#
    .SYNOPSIS
    The import's one-line summary for its single prompt, e.g. "412 aliases: 380 new, 12 different".
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        # The dry run's report.
        [Parameter(Mandatory)]
        [object] $Report
    )
    $count = @{}
    foreach ($row in $Report.rows) {
        $count[$row.result] = 1 + $(if ($count.ContainsKey($row.result)) { $count[$row.result] } else { 0 })
    }
    $parts = foreach ($pair in @(@('added', 'new'), @('updated', 'to update'), @('different', 'different'), @('squashed', 'squashed'), @('unchanged', 'unchanged'), @('invalid', 'invalid (skipped)'))) {
        if ($count.ContainsKey($pair[0])) {
            "$($count[$pair[0]]) $($pair[1])"
        }
    }
    $total = @($Report.rows).Count
    $noun = if ($total -eq 1) { 'alias' } else { 'aliases' }
    $templates = @($Report.templates_created).Count
    $tail = if ($templates -gt 0) { "; $templates new template$(if ($templates -ne 1) { 's' })" } else { '' }
    "${total} ${noun}: $($parts -join ', ')$tail"
}