Private/EAI/Build-EAIRecordsQueryPayload.ps1

$Script:EAIRecordsQueryDefaultSize = 10
$Script:EAIRecordsQueryMaximumSize = 200

function Resolve-EAIRecordsQuerySize {
    [CmdletBinding()]
    param (
        $Size
    )

    if ($null -eq $Size) {
        return $Script:EAIRecordsQueryDefaultSize
    }

    if ($Size -lt 1) {
        throw "Size must be at least 1. The API default is $($Script:EAIRecordsQueryDefaultSize)."
    }

    if ($Size -gt $Script:EAIRecordsQueryMaximumSize) {
        throw "Size cannot exceed $($Script:EAIRecordsQueryMaximumSize)."
    }

    return [int]$Size
}

function Get-EAIRecordsQueryInsightTypeExpression {
    return @{
        OR = @(
            @{
                EQUALS = @(
                    @{ field = 'meta.insightType'; type = 'string' }
                    'correlation'
                )
            }
            @{
                EQUALS = @(
                    @{ field = 'meta.insightType'; type = 'string' }
                    'alertStorm'
                )
            }
            @{
                EMPTY = @(
                    @{ field = 'meta.insightType'; type = 'string' }
                )
            }
        )
    }
}

function Get-EAIRecordsQueryWithinExpression {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$TimestampField
    )

    return @{
        OR = @(
            @{
                AND = @(
                    @{
                        WITHIN = @(
                            @{ field = $TimestampField; type = 'long' }
                            @{ duration = 24; unit = 'hour' }
                        )
                    }
                )
            }
        )
    }
}

function Get-EAIRecordsQueryDefaultFilter {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType
    )

    $timestampField = switch ($RecordType) {
        'events' { 'meta.eventTimestamp' }
        default  { 'meta.firstEventTimestamp' }
    }

    return [PSCustomObject]@{
        schemaName    = 'filterCondition'
        schemaVersion = 4
        expression    = @{
            AND = @(
                (Get-EAIRecordsQueryWithinExpression -TimestampField $timestampField)
                (Get-EAIRecordsQueryInsightTypeExpression)
            )
        }
    }
}

function Resolve-EAIRecordsQueryFilter {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType,

        $Filter
    )

    if ($null -eq $Filter) {
        return Get-EAIRecordsQueryDefaultFilter -RecordType $RecordType
    }

    return ConvertTo-EAIQueryFilterObject -Filter $Filter
}

function ConvertTo-EAIQueryFilterObject {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Filter
    )

    if ($null -eq $Filter) {
        throw 'Filter is required.'
    }

    return ConvertTo-EAISdtFilterObject -Filter $Filter
}

function Write-EAIUnknownQueryFieldWarning {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String[]]$Field,

        [Parameter(Mandatory)]
        [String]$ParameterName,

        [ValidateSet('Filter', 'Order', 'Return')]
        [String]$Usage
    )

    foreach ($fieldName in @($Field)) {
        if ([string]::IsNullOrWhiteSpace($fieldName)) {
            continue
        }

        if (-not (Test-EAIQueryFieldCatalogContains -Field $fieldName)) {
            Write-Warning "'$fieldName' is not a recognized query field for -$ParameterName. Run Get-EAIQueryField -Usage $Usage to list common field names used in Edwin queries."
        }
    }
}

function Get-EAIRecordsQueryDefaultOrderField {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType
    )

    switch ($RecordType) {
        'events' { return 'meta.eventTimestamp' }
        default  { return 'meta.firstEventTimestamp' }
    }
}

function New-EAIQueryOrderClause {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Field,

        [Parameter(Mandatory)]
        [ValidateSet('asc', 'desc', 'ASC', 'DESC')]
        [String]$Type,

        [ValidateSet('MIN', 'MAX', 'SUM', 'AVG', 'MEDIAN')]
        [String]$Mode,

        [Nullable[int]]$Size
    )

    $clause = [ordered]@{
        field = $Field
        type  = $Type.ToLower()
    }

    if ($PSBoundParameters.ContainsKey('Mode')) {
        $clause.mode = $Mode
    }

    if ($PSBoundParameters.ContainsKey('Size')) {
        $clause.size = $Size
    }

    return [PSCustomObject]$clause
}

function Get-EAIQueryDefaultOrder {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType
    )

    return @(
        (New-EAIQueryOrderClause -Field (Get-EAIRecordsQueryDefaultOrderField -RecordType $RecordType) -Type 'desc')
    )
}

function ConvertTo-EAIQueryOrderList {
    [CmdletBinding()]
    param (
        $Order
    )

    if ($null -eq $Order) {
        throw 'Order cannot be null when specified explicitly. Omit -Order to use the default sort.'
    }

    $normalized = foreach ($item in @($Order)) {
        if ($item -is [hashtable]) {
            if (-not $item.ContainsKey('field') -or -not $item.ContainsKey('type')) {
                throw 'Each order clause requires field and type.'
            }

            $clause = [ordered]@{
                field = [string]$item.field
                type  = [string]$item.type.ToLower()
            }

            if ($item.ContainsKey('mode')) {
                $clause.mode = [string]$item.mode
            }

            if ($item.ContainsKey('size')) {
                $clause.size = [int]$item.size
            }

            [PSCustomObject]$clause
        }
        elseif ($item -is [PSCustomObject]) {
            if ($null -eq $item.field -or $null -eq $item.type) {
                throw 'Each order clause requires field and type.'
            }

            $clause = [ordered]@{
                field = [string]$item.field
                type  = [string]$item.type.ToLower()
            }

            if ($null -ne $item.mode) {
                $clause.mode = [string]$item.mode
            }

            if ($null -ne $item.size) {
                $clause.size = [int]$item.size
            }

            [PSCustomObject]$clause
        }
        else {
            throw 'Each order entry must be a hashtable or PSCustomObject.'
        }
    }

    return @($normalized)
}

function Resolve-EAIRecordsQueryOrder {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType,

        $Order,

        [String]$OrderField,

        [ValidateSet('asc', 'desc', 'ASC', 'DESC')]
        [String]$OrderDirection,

        [Switch]$OrderSpecified,

        [Switch]$OrderFieldSpecified,

        [Switch]$OrderDirectionSpecified
    )

    if ($OrderSpecified -and ($OrderFieldSpecified -or $OrderDirectionSpecified)) {
        throw 'Cannot specify -Order together with -OrderField or -OrderDirection.'
    }

    if ($OrderSpecified) {
        return ConvertTo-EAIQueryOrderList -Order $Order
    }

    if ($OrderFieldSpecified -or $OrderDirectionSpecified) {
        $field = if ($OrderFieldSpecified) {
            $OrderField
        }
        else {
            Get-EAIRecordsQueryDefaultOrderField -RecordType $RecordType
        }

        if ($OrderFieldSpecified) {
            Write-EAIUnknownQueryFieldWarning -Field @($field) -ParameterName 'OrderField' -Usage Order
        }

        $direction = if ($OrderDirectionSpecified) {
            $OrderDirection
        }
        else {
            'desc'
        }

        return @(New-EAIQueryOrderClause -Field $field -Type $direction)
    }

    return Get-EAIQueryDefaultOrder -RecordType $RecordType
}

function Build-EAIRecordsQueryPayload {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType,

        [String[]]$Field,

        $Filter,

        [String]$Timezone,

        $Order,

        [String]$OrderField,

        [ValidateSet('asc', 'desc', 'ASC', 'DESC')]
        [String]$OrderDirection,

        $Size,

        $ElasticQuery
    )

    $fieldsValue = $null
    if ($PSBoundParameters.ContainsKey('Field') -and $null -ne $Field -and $Field.Count -gt 0) {
        $fieldsValue = [string[]]@($Field)
        Write-EAIUnknownQueryFieldWarning -Field $fieldsValue -ParameterName 'Field' -Usage Return
    }

    $effectiveTimezone = if ([string]::IsNullOrWhiteSpace($Timezone)) {
        [TimeZoneInfo]::Local.Id
    }
    else {
        $Timezone
    }

    $orderParams = @{
        RecordType              = $RecordType
        OrderSpecified          = $PSBoundParameters.ContainsKey('Order')
        OrderFieldSpecified     = $PSBoundParameters.ContainsKey('OrderField')
        OrderDirectionSpecified = $PSBoundParameters.ContainsKey('OrderDirection')
    }

    if ($PSBoundParameters.ContainsKey('Order')) {
        $orderParams.Order = $Order
    }

    if ($PSBoundParameters.ContainsKey('OrderField')) {
        $orderParams.OrderField = $OrderField
    }

    if ($PSBoundParameters.ContainsKey('OrderDirection')) {
        $orderParams.OrderDirection = $OrderDirection
    }

    $orderList = Resolve-EAIRecordsQueryOrder @orderParams

    $payload = [ordered]@{
        env        = @{
            timezone = $effectiveTimezone
        }
        recordType = $RecordType
        fields     = $fieldsValue
        filter     = Resolve-EAIRecordsQueryFilter -RecordType $RecordType -Filter $Filter
        order      = [object[]]@($orderList)
    }

    if ($PSBoundParameters.ContainsKey('Size')) {
        $payload.size = Resolve-EAIRecordsQuerySize -Size $Size
    }

    if ($PSBoundParameters.ContainsKey('ElasticQuery')) {
        if ($null -eq $ElasticQuery) {
            throw 'ElasticQuery cannot be null when specified.'
        }

        $payload.elasticQuery = $ElasticQuery
    }

    return [PSCustomObject]$payload
}

function Format-EAIQueryRecordPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType,

        [Parameter(Mandatory)]
        [String]$RecordId
    )

    $encodedRecordId = [System.Uri]::EscapeDataString($RecordId)
    return "/ui/query/record/$RecordType/$encodedRecordId"
}

function ConvertTo-EAIQueryRequestJson {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $InputObject
    )

    return ($InputObject | ConvertTo-Json -Depth 30)
}

function Get-EAIQueryRecordTypeName {
    [CmdletBinding()]
    param (
        [String]$RecordType,

        [Switch]$Projected
    )

    if ($Projected) {
        switch ($RecordType) {
            'events' { return 'Edwin.Event.Projected' }
            'alerts' { return 'Edwin.Alert.Projected' }
            'insights' { return 'Edwin.Insight.Projected' }
            default { return 'Edwin.Query.Record' }
        }
    }

    switch ($RecordType) {
        'events' { return 'Edwin.Event' }
        'alerts' { return 'Edwin.Alert' }
        'insights' { return 'Edwin.Insight' }
        default { return 'Edwin.Query.Record' }
    }
}

function Add-EAIQueryRecordTypeInfo {
    [CmdletBinding()]
    param (
        $InputObject,

        [String]$RecordType,

        [Switch]$Projected
    )

    if ($null -eq $InputObject) {
        return $null
    }

    $typeName = Get-EAIQueryRecordTypeName -RecordType $RecordType -Projected:$Projected
    return Add-ObjectTypeInfo -InputObject $InputObject -TypeName $typeName
}

function Test-EAIRecordsQueryUsesFieldProjection {
    [CmdletBinding()]
    param (
        [Switch]$FieldSpecified,

        $Field
    )

    return $FieldSpecified -and $null -ne $Field -and @($Field).Count -gt 0
}

function Write-EAIQueryRecordsOutput {
    [CmdletBinding()]
    param (
        $Response,

        [String]$RecordType,

        [Switch]$AsResponse,

        [Switch]$Projected
    )

    if ($AsResponse) {
        return ConvertTo-EAIQueryResponse -InputObject $Response
    }

    if ($null -eq $Response) {
        Write-Output @() -NoEnumerate
        return
    }

    $effectiveRecordType = if (-not [string]::IsNullOrWhiteSpace($RecordType)) {
        $RecordType
    }
    elseif ($null -ne $Response.meta -and $null -ne $Response.meta.recordType) {
        [string]$Response.meta.recordType
    }

    $records = if ($null -eq $Response.results) {
        @()
    }
    else {
        @($Response.results)
    }

    if ($records.Count -eq 0) {
        Write-Output @() -NoEnumerate
        return
    }

    return Add-EAIQueryRecordTypeInfo -InputObject $records -RecordType $effectiveRecordType -Projected:$Projected
}

function Write-EAIQueryRecordOutput {
    [CmdletBinding()]
    param (
        $Response,

        [Parameter(Mandatory)]
        [ValidateSet('events', 'alerts', 'insights')]
        [String]$RecordType,

        [Switch]$AsResponse,

        [Switch]$Projected
    )

    if ($AsResponse) {
        return ConvertTo-EAIQueryResponse -InputObject $Response
    }

    if ($null -eq $Response -or $null -eq $Response.results -or @($Response.results).Count -eq 0) {
        return $null
    }

    return Add-EAIQueryRecordTypeInfo -InputObject $Response.results[0] -RecordType $RecordType -Projected:$Projected
}

function ConvertTo-EAIQueryResponse {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $InputObject
    )

    if ($null -eq $InputObject) {
        return $null
    }

    return Add-ObjectTypeInfo -InputObject $InputObject -TypeName 'Edwin.Query.Response'
}