Private/ConvertTo-StructuredFilterString.ps1
|
function ConvertTo-StructuredFilterString { <# .SYNOPSIS Converts Hashtable Key-Value pairs to a structured filter expression string .DESCRIPTION When given a hashtable as input, converts key/value pairs into a structured filter expression string, as used by newer CyberArk API endpoints (e.g. the -filter parameter of Get-PASReport). Returns a single key hashtable, with the expression string as the value of the "filter" key. Supports the following operators, matching the CyberArk filter grammar: - Comparison: EQ, NE, GT, GE, LT, LE - Set: IN, NOTIN - String: CONTAINS, NOTCONTAINS, STARTSWITH, ENDSWITH - Null: IS NULL, IS NOTNULL By default, a hashtable value is treated as a plain value to compare for equality (EQ). To use a different operator, prefix the value with the operator keyword, e.g: @{status = 'NE Done'} @{age = 'GT 5'} @{description = 'IS NULL'} @{safe = 'IN Safe1,Safe2'} An array value is treated as an implicit IN list. Multiple key/value pairs in the input hashtable are joined with a single logical operator - see the LogicalOperator parameter. Parenthetical/grouped expressions (mixing AND & OR at different levels of precedence) are not supported - a flat hashtable cannot express nested groups. No psPAS function currently requires this; if a future caller needs a mixed-precedence expression, this function will need to be extended, or a different input shape introduced. .PARAMETER Parameters Hashtable containing parameter names and values to include in output .PARAMETER LogicalOperator The logical operator (AND/OR) used to join multiple key/value pairs. Defaults to AND. .PARAMETER QuoteValue Specify this switch to always enclose string operand values in quotes, even if they contain no whitespace. String operand values containing whitespace are always quoted, regardless of this switch. .EXAMPLE @{status = 'Done'} | ConvertTo-StructuredFilterString Output: @{"filter" = "status EQ Done"} .EXAMPLE @{status = 'NE Done'; age = 'GT 5'} | ConvertTo-StructuredFilterString Output: @{"filter" = "status NE Done AND age GT 5"} .EXAMPLE @{status = 'Done'; safe = 'Prod'} | ConvertTo-StructuredFilterString -LogicalOperator OR Output: @{"filter" = "status EQ Done OR safe EQ Prod"} .EXAMPLE @{description = 'IS NULL'} | ConvertTo-StructuredFilterString Output: @{"filter" = "description IS NULL"} .EXAMPLE @{safe = 'Safe1', 'Safe2'} | ConvertTo-StructuredFilterString Output: @{"filter" = "safe IN Safe1,Safe2"} .EXAMPLE @{name = 'John Smith'} | ConvertTo-StructuredFilterString Output: @{"filter" = 'name EQ "John Smith"'} Values containing whitespace are automatically enclosed in quotes. .NOTES Based on documentation for filter parameter of Get Reports API endpoint: https://docs.cyberark.com/pam-self-hosted/15.0/en/content/webservices/get-reports.htm #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'FilterList', Justification = 'False Positive')] [CmdletBinding()] [OutputType('System.Collections.Hashtable')] param( [parameter( Mandatory = $false, ValueFromPipeline = $true )] [hashtable]$Parameters, [parameter( Mandatory = $false, ValueFromPipeline = $false )] [ValidateSet('AND', 'OR')] [string]$LogicalOperator = 'AND', [parameter( Mandatory = $false, ValueFromPipeline = $false )] [switch]$QuoteValue ) begin { #Matches a value which is entirely a null-check operator, with no operand $NullOperatorPattern = '^(?i)(IS NOTNULL|IS NULL)$' #Matches a value prefixed with an operator keyword, capturing the operator and the remaining operand text $ValueOperatorPattern = '^(?i)(EQ|NE|GT|GE|LT|LE|CONTAINS|NOTCONTAINS|STARTSWITH|ENDSWITH|NOTIN|IN)\s+(\S.*)$' } process { if ($Parameters) { $Parameters.Keys | ForEach-Object { $FilterList = [Collections.Generic.List[Object]]@() } { $Property = $PSItem $Value = $Parameters[$PSItem] #Note: deliberately if/elseif, not switch - switch auto-enumerates array values #element-by-element, and $Matches set inside a switch condition script block is not #visible in the case body if ($Value -is [datetime]) { $Operator = 'EQ' $OperandValues = @($Value | ConvertTo-UnixTime) } elseif ($Value -is [array]) { $Operator = 'IN' $OperandValues = $Value } elseif (($Value -is [string]) -and ($Value.Trim() -match $NullOperatorPattern)) { $Operator = $Matches[1].ToUpper() $OperandValues = $null } elseif (($Value -is [string]) -and ($Value.Trim() -match $ValueOperatorPattern)) { $Operator = $Matches[1].ToUpper() if ($Operator -in @('IN', 'NOTIN')) { $OperandValues = $Matches[2] -split '\s*,\s*' } else { $OperandValues = @($Matches[2]) } } else { $Operator = 'EQ' $OperandValues = @($Value) } if ($null -eq $OperandValues) { $null = $FilterList.Add("$Property $Operator") } else { $FormattedValues = $OperandValues | ForEach-Object { if (($PSItem -is [string]) -and ($QuoteValue -or ($PSItem -match '\s'))) { """$PSItem""" } else { $PSItem } } $null = $FilterList.Add("$Property $Operator $($FormattedValues -join ',')") } } { if ($FilterList.count -gt 0) { @{'filter' = $FilterList -join " $LogicalOperator " } } } } } end { } } |