Public/New-ServiceNowQuery.ps1

<#
.SYNOPSIS
    Build query string for api call

.DESCRIPTION
    Build query string for api call, there are basic and advanced methods; see the different parameter sets.

    Basic allows you to look for exact matches as well as fields that are like a value; these are all and'd together.
    You can also sort your results, ascending or descending, by 1 field.

    Advanced allows you to perform the (almost) complete set of operations that ServiceNow has.
    The comparison operators have been made to mimic powershell itself so the code should be easy to understand.
    You can use a very large set of comparison operators (see the script variable ServiceNowOperator),
    and, or, and grouping joins, as well as multiple sorting parameters.

.PARAMETER Filter
    Array or multidimensional array of fields and values to filter on.
    Each array should be of the format @(field, comparison operator, value) separated by a join, either 'and', 'or', or 'group'.
    For a complete list of comparison operators, see $script:ServiceNowOperator and use Name in your filter.
    See the examples.
    Also, see https://docs.servicenow.com/bundle/quebec-platform-user-interface/page/use/common-ui-elements/reference/r_OpAvailableFiltersQueries.html
    for how to represent dates.

.PARAMETER Sort
    Array or multidimensional array of fields to sort on.
    Each array should be of the format @(field, asc/desc).

.EXAMPLE
    New-ServiceNowQuery -MatchExact @{field_name=value}
    Get query string where field name exactly matches the value

.EXAMPLE
    New-ServiceNowQuery -MatchContains @{field_name=value}
    Get query string where field name contains the value

.EXAMPLE
    New-ServiceNowQuery -Filter @('state', '-eq', '1'), 'or', @('short_description','-like', 'powershell')
    Get query string where state equals New or short description contains the word powershell

.EXAMPLE
    $filter = @('state', '-eq', '1'),
                'and',
              @('short_description','-like', 'powershell'),
              'group',
              @('state', '-eq', '2')
    PS > New-ServiceNowQuery -Filter $filter
    Get query string where state equals New and short description contains the word powershell or state equals In Progress.
    The first 2 filters are combined and then or'd against the last.

.EXAMPLE
    New-ServiceNowQuery -Filter @('state', '-eq', '1') -Sort @('opened_at', 'desc'), @('state')
    Get query string where state equals New and first sort by the field opened_at descending and then sort by the field state ascending

.EXAMPLE
    New-ServiceNowQuery -Filter @('opened_at', '-ge', 'javascript:gs.daysAgoEnd(30)')
    Get query string where the record was opened in the last 30 days

.INPUTS
    None

.OUTPUTS
    String
#>

function New-ServiceNowQuery {

    [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state is actually changing')]

    [CmdletBinding()]
    [OutputType([System.String])]

    param(
        [parameter()]
        [object[]] $Filter,

        [parameter()]
        [object[]] $Sort

    )

    Write-Verbose ('{0} - {1}' -f $MyInvocation.MyCommand, $PSCmdlet.ParameterSetName)

    if ( $Filter ) {

        if ( $Filter[0].GetType().Name -eq 'Object[]' ) {
            # already the format we want
            $filterList = $Filter
        }
        else {
            # change to array object as opposed to traditional array
            $filterList = , $Filter
        }

        $query = for ($i = 0; $i -lt $filterList.Count; $i++) {
            $thisFilter = $filterList[$i]

            # allow passing of string instead of array
            # useful for joins
            if ($thisFilter.GetType().Name -eq 'String') {
                $thisFilter = @(, $thisFilter)
            }

            switch ($thisFilter.Count) {
                0 {
                    # nothing to see here
                    Continue
                }

                1 {
                    # should be a join

                    switch ($thisFilter[0]) {
                        { $_ -in 'and', '-and' } {
                            '^'
                        }

                        { $_ -in 'or', '-or' } {
                            '^OR'
                        }

                        { $_ -in 'group', '-group' } {
                            '^NQ'
                        }

                        Default {
                            throw "Unsupported join operator '$($thisFilter[0])'. 'and', 'or', and 'group' are supported."
                        }
                    }

                    # make sure we don't end on a join
                    if ( $i -eq $filterList.Count - 1) {
                        throw '$Filter cannot end with a join'
                    }

                    break
                }

                { $_ -ne 1 } {
                    # perform data validation on all filters other than a join operator
                    $thisOperator = $script:ServiceNowOperator | Where-Object { $_.Name -eq $thisFilter[1] }
                    if ( -not $thisOperator ) {
                        throw ('Operator ''{0}'' is not valid' -f $thisFilter[1])
                    }
                    if ( $thisOperator.NumValues -ne $thisFilter.Count - 2 ) {
                        throw ('Operator ''{0}'' requires 1 field name and {1} value(s)' -f $thisFilter[1], $thisOperator.NumValues)
                    }
                }

                2 {
                    # should be a non-value operator, eg. ='' / ISEMPTY
                    '{0}{1}' -f $thisFilter[0], $thisOperator.QueryOperator
                    break
                }

                3 {
                    # should be format - field operator value

                    if ( $thisFilter[2] -is [DateTime] ) {
                        $dateGen = "'{0}','{1}'" -f $thisFilter[2].ToString('yyyy-MM-dd'), $thisFilter[2].ToString('HH:mm:ss')
                        '{0}{1}javascript:gs.dateGenerate({2})' -f $thisFilter[0], $thisOperator.QueryOperator, $dateGen
                    }
                    else {
                        '{0}{1}{2}' -f $thisFilter[0], $thisOperator.QueryOperator, $thisFilter[2]
                    }

                    break
                }

                4 {
                    # should be format - field operator value1 value2, where applicable, eg. between

                    if ( $thisFilter[2] -is [DateTime] ) {
                        $dateGen1 = "'{0}','{1}'" -f $thisFilter[2].ToString('yyyy-MM-dd'), $thisFilter[2].ToString('HH:mm:ss')
                        $dateGen2 = "'{0}','{1}'" -f $thisFilter[3].ToString('yyyy-MM-dd'), $thisFilter[3].ToString('HH:mm:ss')
                        '{0}{1}javascript:gs.dateGenerate({2})@javascript:gs.dateGenerate({3})' -f $thisFilter[0], $thisOperator.QueryOperator, $dateGen1, $dateGen2
                    }
                    else {
                        '{0}{1}{2}@{3}' -f $thisFilter[0], $thisOperator.QueryOperator, $thisFilter[2], $thisFilter[3]
                    }

                    break
                }

                Default {
                    throw ('Too many filter items for {0}, see the help' -f $thisFilter[0])
                }
            }
        }
    }

    # force query to an array in case we only got one item and its a string
    # otherwise below add to query won't work as expected
    $query = @($query)

    if ($query) {
        $query += '^'
    }

    $orderList = $Sort

    if ( $Sort ) {
        # see if we're working with 1 array or multidimensional array
        # we want multidimensional so convert if not
        if ($Sort[0].GetType().Name -eq 'String') {
            $orderList = @(, $Sort)
        }
    }

    $query += for ($i = 0; $i -lt $orderList.Count; $i++) {
        $thisOrder = $orderList[$i]
        if ( $orderList.Count -gt 1 -and $i -gt 0 ) {
            '^'
        }

        switch ($thisOrder.Count) {
            0 {
                # nothing to see here
                Continue
            }

            1 {
                # should be field, default to ascending
                'ORDERBY'
                $thisOrder[0]
            }

            2 {
                switch ($thisOrder[1]) {
                    'asc' {
                        'ORDERBY'
                    }

                    'desc' {
                        'ORDERBYDESC'
                    }

                    Default {
                        throw "Invalid order direction '$_'. Provide either 'asc' or 'desc'."
                    }
                }
                $thisOrder[0]
            }

            Default {
                throw ('Too many items for {0}, see the help' -f $thisOrder[0])
            }
        }
    }

    ($query -join '').Trim('^')
}