functions/Resolve-SMAXEntityInput.ps1
|
function Resolve-SMAXEntityInput { <# .SYNOPSIS Resolves generic entity input into SMAX-compatible properties. .DESCRIPTION Resolves property names against SMAX metadata, lookup values, and ENUM values. Input can be provided as a JSON object string, PSCustomObject, or dictionary. UserOptions values support complete inline lookup tokens in the format {{lookup:EntityType|Filter}}. .PARAMETER InputObject A JSON object string, PSCustomObject, or dictionary containing entity properties. This parameter accepts pipeline input. .PARAMETER EntityType The SMAX entity type whose metadata is used to resolve the input properties. .PARAMETER Connection The connection used to read metadata and resolve SMAX entity lookups. .EXAMPLE $input = '{"CurrentAssignment":"servicedesk"}' Resolve-SMAXEntityInput -InputObject $input -EntityType Request Description: Resolves the CurrentAssignment ENUM value to its canonical SMAX value. .EXAMPLE @{ 'lookup.AssignedToGroup' = "Name='Operations'" } | Resolve-SMAXEntityInput -EntityType Request -Connection $connection Description: Resolves an entity-link lookup from a hashtable received through the pipeline. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$InputObject, [Parameter(Mandatory = $true)] [PSFramework.TabExpansion.PsfArgumentCompleterAttribute('SMAX.EntityTypes')] [string]$EntityType, [Parameter(Mandatory = $false)] [object]$Connection = (Get-SMAXLastConnection) ) process { if ($InputObject -is [string]) { if ([string]::IsNullOrWhiteSpace($InputObject)) { throw 'InputObject must not be an empty JSON string.' } try { $inputProperties = $InputObject | ConvertFrom-Json -ErrorAction Stop } catch { throw 'InputObject must contain valid JSON.' } } elseif ($InputObject -is [System.Collections.IDictionary] -or $InputObject -is [pscustomobject]) { $inputProperties = $InputObject } else { throw "InputObject must be a JSON string, PSCustomObject, or dictionary. Received '$($InputObject.GetType().FullName)'." } if ($inputProperties -is [System.Collections.IList] -or $inputProperties -isnot [System.Collections.IDictionary] -and $inputProperties -isnot [pscustomobject]) { throw 'InputObject must represent a JSON object, PSCustomObject, or dictionary.' } $description = Get-SMAXEntityDescription -Connection $Connection -EntityType $EntityType -Mode PropertiesAsHashtable $smaxBody = [ordered]@{} $properties = if ($inputProperties -is [System.Collections.IDictionary]) { $inputProperties.GetEnumerator() | ForEach-Object { [pscustomobject]@{ Name = [string]$_.Key Value = $_.Value } } } else { $inputProperties.PSObject.Properties } $canonicalAttributeNames = @{} foreach ($inputProperty in $properties) { $inputKey = [string]$inputProperty.Name $isLookup = $inputKey.StartsWith('lookup.', [System.StringComparison]::OrdinalIgnoreCase) $attributeName = if ($isLookup) { $inputKey.Substring('lookup.'.Length) } else { $inputKey } $attributeDescription = $description.$attributeName if ($null -eq $attributeDescription) { throw "Property '$attributeName' is not available for entity type '$EntityType'." } $canonicalAttributeName = [string]$attributeDescription.name if ($canonicalAttributeNames.ContainsKey($canonicalAttributeName)) { throw "Property '$canonicalAttributeName' is specified more than once." } $canonicalAttributeNames[$canonicalAttributeName] = $true } foreach ($inputProperty in $properties) { $inputKey = [string]$inputProperty.Name $isLookup = $inputKey.StartsWith('lookup.', [System.StringComparison]::OrdinalIgnoreCase) $attributeName = if ($isLookup) { $inputKey.Substring('lookup.'.Length) } else { $inputKey } $attributeDescription = $description.$attributeName if ($null -eq $attributeDescription) { throw "Property '$attributeName' is not available for entity type '$EntityType'." } $canonicalAttributeName = [string]$attributeDescription.name if ($isLookup) { if ($attributeDescription.logical_type -ne 'ENTITY_LINK') { throw "Lookup '$inputKey' refers to '$canonicalAttributeName', which is not an ENTITY_LINK." } $remoteType = [string]$attributeDescription.remoteType if ([string]::IsNullOrWhiteSpace($remoteType)) { throw "No remoteType is available for ENTITY_LINK '$canonicalAttributeName'." } $filter = [string]$inputProperty.Value if ([string]::IsNullOrWhiteSpace($filter)) { throw "Lookup '$inputKey' requires an SMAX filter." } $linkedEntity = @(Get-SMAXEntity -Connection $Connection -Properties Id -EntityType $remoteType -Filter $filter) if ($linkedEntity.Count -eq 0) { throw "Lookup '$inputKey' did not find a '$remoteType' entity for filter '$filter'." } if ($linkedEntity.Count -gt 1) { throw "Lookup '$inputKey' found multiple '$remoteType' entities for filter '$filter'." } $smaxBody[$canonicalAttributeName] = $linkedEntity[0].Id continue } $attributeValue = $inputProperty.Value if ($attributeDescription.logical_type -eq 'ENUM') { $validEnumValues = @($attributeDescription.possibleValues.PSObject.Properties.Name) if ($validEnumValues.Count -eq 0) { throw "No valid values are available in SMAX metadata for ENUM '$canonicalAttributeName'." } if ($attributeValue -isnot [string]) { throw "Value '$attributeValue' for ENUM '$canonicalAttributeName' must be a string." } $matchingEnumValues = @($validEnumValues | Where-Object { $_ -ieq $attributeValue }) if ($matchingEnumValues.Count -ne 1) { $validValuesText = $validEnumValues -join "', '" throw "Value '$attributeValue' is invalid for ENUM '$canonicalAttributeName'. Valid values: '$validValuesText'." } $attributeValue = [string]$matchingEnumValues[0] if ($inputProperty.Value -cne $attributeValue) { Write-PSFMessage -Level Verbose -Message "Resolve-SMAXEntityInput: ENUM '$canonicalAttributeName' was normalized from '$($inputProperty.Value)' to '$attributeValue'." } } if ($canonicalAttributeName -ceq 'UserOptions') { if ($attributeValue -isnot [string] -or [string]::IsNullOrWhiteSpace($attributeValue)) { throw "Property '$canonicalAttributeName' must contain a JSON string." } try { $userOptions = $attributeValue | ConvertFrom-Json -ErrorAction Stop } catch { throw "Property '$canonicalAttributeName' does not contain valid JSON." } if ($userOptions -is [System.Collections.IList] -or $userOptions -isnot [pscustomobject]) { throw "Property '$canonicalAttributeName' must contain a JSON object." } $valuesToCheck = [System.Collections.Stack]::new() $valuesToCheck.Push(@{ Value = $userOptions Parent = $null Key = $null Path = $canonicalAttributeName }) while ($valuesToCheck.Count -gt 0) { $currentValue = $valuesToCheck.Pop() if ($currentValue.Value -is [System.Collections.IDictionary]) { foreach ($key in $currentValue.Value.Keys) { $valuesToCheck.Push(@{ Value = $currentValue.Value[$key] Parent = $currentValue.Value Key = $key Path = "$($currentValue.Path).$key" }) } continue } if ($currentValue.Value -is [pscustomobject]) { foreach ($property in $currentValue.Value.PSObject.Properties) { $valuesToCheck.Push(@{ Value = $property.Value Parent = $currentValue.Value Key = $property.Name Path = "$($currentValue.Path).$($property.Name)" }) } continue } if ($currentValue.Value -is [System.Collections.IList] -and $currentValue.Value -isnot [string]) { for ($index = 0; $index -lt $currentValue.Value.Count; $index++) { $valuesToCheck.Push(@{ Value = $currentValue.Value[$index] Parent = $currentValue.Value Key = $index Path = "$($currentValue.Path)[$index]" }) } continue } if ($currentValue.Value -isnot [string]) { continue } $lookupToken = [regex]::Match($currentValue.Value, '^\{\{lookup:(?<EntityType>[^|{}]+)\|(?<Filter>.+)\}\}$') if (-not $lookupToken.Success) { continue } $lookupEntityType = $lookupToken.Groups['EntityType'].Value.Trim() $lookupFilter = $lookupToken.Groups['Filter'].Value.Trim() if ([string]::IsNullOrWhiteSpace($lookupEntityType) -or [string]::IsNullOrWhiteSpace($lookupFilter)) { throw "Lookup token at '$($currentValue.Path)' requires EntityType and Filter: {{lookup:EntityType|Filter}}." } $linkedEntity = @(Get-SMAXEntity -Connection $Connection -Properties Id -EntityType $lookupEntityType -Filter $lookupFilter) if ($linkedEntity.Count -eq 0) { throw "Lookup token at '$($currentValue.Path)' did not find a '$lookupEntityType' entity for filter '$lookupFilter'." } if ($linkedEntity.Count -gt 1) { throw "Lookup token at '$($currentValue.Path)' found multiple '$lookupEntityType' entities for filter '$lookupFilter'." } if ($currentValue.Parent -is [System.Collections.IDictionary] -or $currentValue.Parent -is [System.Collections.IList]) { $currentValue.Parent[$currentValue.Key] = $linkedEntity[0].Id } else { $currentValue.Parent.PSObject.Properties[$currentValue.Key].Value = $linkedEntity[0].Id } Write-PSFMessage -Level Verbose -Message "Resolve-SMAXEntityInput: UserOptions lookup at '$($currentValue.Path)' was resolved to Id '$($linkedEntity[0].Id)'." } $attributeValue = $userOptions | ConvertTo-Json -Depth 100 -Compress } $smaxBody[$canonicalAttributeName] = $attributeValue } $smaxBody } } |