A better QueryXML function for Autotask WebAPI

I was never happy with my previous QueryXML function. It had a hard limit on how many conditions you could use and there was no way to nest conditions. So I have wre-written it from scratch to make it a bit more powerful and flexible.

This time around there is no limit on the complexity of a query. I have added two keywords: begin and end. You may think of them as the beginning and ending of parentheses. Look at this query:

New-AtwsQuery contract accountid equals 123456 `
              and contracttype equals 1 or contracttype equals 7

This will return all contracts from account with id 123456 contracttype 1 (Time & Materials) and all contracts of type 7 (Recurring Service), regardless of account. To put the two conditions in parentheses you can use:

New-AtwsQuery contract accountid equals 123456 `
              begin contracttype equals 1 or contracttype equals 7 end

This function does not return any data from Autotask, it just creates the QueryXML you need to perform a query. You must first connect powershell to Autotask. Then you can post your query like this:

$QueryXML = New-AtwsQuery contract accountid equals 123456 `
            begin contracttype equals 1 or contracttype equals 7 end

$Result = $atws.query($QueryXML)

# Print any returned entities to console:
$Result.EntityResults

Here is the complete function:

function New-ATWSQuery {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [switch]$UDF,
        [Parameter(Mandatory=$true,ValueFromRemainingArguments=$true)]
        [String[]]$QueryText

    )
    # List of allowed operators in QueryXML
    $Operators = @('and', 'or', 'begin')

    # List of all allowed conditions in QueryXML
    $Conditions = @('Equals', 'NotEqual', 'GreaterThan', 'LessThan', 'GreaterThanorEquals', 'LessThanOrEqual', 
                'BeginsWith', 'EndsWith', 'Contains', 'IsNotNull', 'IsNull', 'IsThisDay', 'Like', 'NotLike', 'SoundsLike')

    $NoValueNeeded = @('IsNotNull', 'IsNull', 'IsThisDay')

    # Create an XML document object. Only used to create XML elements.
    $xml = New-Object XML

    # Create base element and add a single Entity definition to it.
    $queryxml = $xml.CreateElement('queryxml')
    $entityxml = $xml.CreateElement('entity')
    $queryxml.AppendChild($entityxml) | Out-Null

    # Entity is the first element of the querytext
    $Entityxml.InnerText = $QueryText[0]

    # Create an XML element for the query tag.
    # It will contain all conditions.
    $Query = $xml.CreateElement('query')
    $queryxml.AppendChild($Query) | Out-Null

    # Set generic pointer $Node to the query tag
    $Node = $Query

    # Create an index pointer that starts on the second element
    # of the querytext array
    For ($i = 1; $i -lt $QueryText.Count; $i++)
    {
        Switch ($QueryText[$i])
        {
            # Check for operators
            {$Operators -contains $_}
                {
                    # Element is an operator. Add a condition tag with
                    # attribute 'operator' set to the value of element
                    $Condition = $xml.CreateElement('condition')
                    If ($_ -eq 'begin')
                    {
                        # Add nested condition
                        $Node.AppendChild($Condition) | Out-Null
                        $Node = $Condition
                        $Condition = $xml.CreateElement('condition')
                    }
                    If ('or','and' -contains $_)
                    {
                        $Condition.SetAttribute('operator', $_)
                    }

                    # Append condition to current $Node
                    $Node.AppendChild($Condition) | Out-Null

                    # Set condition tag as current $Node. Next field tag
                    # should be nested inside the condition tag.
                    $Node = $Condition
                    Break
                }
             # End a nested condition
            'end'
                {
                    $Node = $Node.ParentNode
                    Break
                }
           # Check for a condition
            {$Conditions -contains $_} 
                {
                    # Element is a condition. Add an expression tag with
                    # attribute 'op' set to the value of element
                    $Expression = $xml.CreateElement('expression')
                    $Expression.SetAttribute('op', $_)

                    # Append condition to current $Node
                    $Node.AppendChild($Expression) | Out-Null

                    # Not all conditions need a value. 
                    If ($NoValueNeeded -notcontains $_)
                    {
                        # Increase pointer and add next element as 
                        # Value to expression
                        $i++
                        $Expression.InnerText = $QueryText[$i]
                    }

                    # An expression closes a field tag. The next
                    # element refers to the next level up.
                    $Node = $Node.ParentNode

                    # If the parentnode is a conditiontag we need
                    # to go one more step up
                    If ($Node.Name -eq 'condition')
                    {
                        $Node = $Node.ParentNode
                    }
                    Break
                }
            # Everything that aren't an operator or a condition is treated
            # as a field.
            default
                {
                    # Create a field tag, fill it with element
                    # and add it to current Node
                    $Field = $xml.CreateElement('field')
                    $Field.InnerText = $QueryText[$i]
                    $Node.AppendChild($Field) | Out-Null

                    # If UDF is set we must add an attribute to the field
                    # tag. But only once!
                    If ($UDF)
                    {
                        $Field.SetAttribute('udf', 'true')
                        # Only the first field can be UDF
                        $UDF = $false
                    }

                    # The field tag is now the current Node
                    $Node = $Field
                }
        }
    }

    # Return formatted XML as text
    $queryxml.OuterXml

}