Private/DataverseQueries.ps1

# OData string escaping utility
function Get-EscapedODataString {
    <#
    .SYNOPSIS
        Escapes a string value for use in OData filter expressions.
    .DESCRIPTION
        OData string literals use single quotes. To include a single quote
        in a string value, it must be doubled (escaped as '').
    .PARAMETER Value
        The string value to escape.
    .OUTPUTS
        The escaped string safe for use in OData $filter expressions.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Value
    )

    # OData escapes single quotes by doubling them
    return $Value -replace "'", "''"
}

# Constants for stage/mode/image type mappings
$script:PluginStageMap = @{
    10 = "PreValidation"
    20 = "PreOperation"
    40 = "PostOperation"
}

$script:PluginModeMap = @{
    0 = "Synchronous"
    1 = "Asynchronous"
}

$script:PluginImageTypeMap = @{
    0 = "PreImage"
    1 = "PostImage"
    2 = "Both"
}

$script:DataverseStageValues = @{
    "PreValidation" = 10
    "PreOperation" = 20
    "PostOperation" = 40
}

$script:DataverseModeValues = @{
    "Synchronous" = 0
    "Asynchronous" = 1
}

$script:DataverseImageTypeValues = @{
    "PreImage" = 0
    "PostImage" = 1
    "Both" = 2
}

$script:ComponentType = @{
    PluginAssembly = 91
    SdkMessageProcessingStep = 92
}

function Get-PluginAssembly {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    $escapedName = Get-EscapedODataString -Value $Name
    $filter = "`$filter=name eq '$escapedName'"
    $select = "`$select=pluginassemblyid,name,version,publickeytoken"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "pluginassemblies?$filter&$select" -Method GET
    return $result.value | Select-Object -First 1
}

function Get-PluginPackage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [Parameter()]
        [string]$UniqueName
    )

    $select = "`$select=pluginpackageid,name,uniquename,version"
    try {
        if ($UniqueName) {
            $escapedUniqueName = Get-EscapedODataString -Value $UniqueName
            $filter = "`$filter=uniquename eq '$escapedUniqueName'"
            $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "pluginpackages?$filter&$select" -Method GET
            $package = $result.value | Select-Object -First 1
            if ($package) { return $package }
        }

        $escapedName = Get-EscapedODataString -Value $Name
        $filter = "`$filter=name eq '$escapedName'"
        $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "pluginpackages?$filter&$select" -Method GET
        return $result.value | Select-Object -First 1
    }
    catch {
        Write-LogDebug "Could not query plugin packages: $($_.Exception.Message)"
        return $null
    }
}

function Get-PluginType {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$AssemblyId,
        [Parameter(Mandatory = $true)]
        [string]$TypeName
    )

    $escapedTypeName = Get-EscapedODataString -Value $TypeName
    $filter = "`$filter=_pluginassemblyid_value eq '$AssemblyId' and typename eq '$escapedTypeName'"
    $select = "`$select=plugintypeid,typename,friendlyname"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "plugintypes?$filter&$select" -Method GET
    return $result.value | Select-Object -First 1
}

function Get-PluginTypesForAssembly {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$AssemblyId
    )

    $filter = "`$filter=_pluginassemblyid_value eq '$AssemblyId'"
    $select = "`$select=plugintypeid,typename,friendlyname"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "plugintypes?$filter&$select" -Method GET
    return $result.value
}

function Get-PluginTypeStepCount {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$PluginTypeId
    )

    $filter = "`$filter=_eventhandler_value eq '$PluginTypeId'"
    $select = "`$select=sdkmessageprocessingstepid"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingsteps?$filter&$select" -Method GET
    return @($result.value).Count
}

function Remove-PluginType {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$PluginTypeId,
        [Parameter()]
        [switch]$WhatIf
    )

    if ($WhatIf) {
        Write-Log "[WhatIf] Would delete plugin type: $PluginTypeId"
        return $true
    }

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "plugintypes($PluginTypeId)" -Method DELETE
}

function Get-SdkMessage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$MessageName
    )

    $escapedMessageName = Get-EscapedODataString -Value $MessageName
    $filter = "`$filter=name eq '$escapedMessageName'"
    $select = "`$select=sdkmessageid,name"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessages?$filter&$select" -Method GET
    return $result.value | Select-Object -First 1
}

function Get-SdkMessageFilter {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$MessageId,
        [Parameter(Mandatory = $true)]
        [string]$EntityLogicalName,
        [Parameter()]
        [string]$SecondaryEntityLogicalName
    )

    $escapedEntity = Get-EscapedODataString -Value $EntityLogicalName
    $filter = "_sdkmessageid_value eq '$MessageId' and primaryobjecttypecode eq '$escapedEntity'"
    if ($SecondaryEntityLogicalName) {
        $escapedSecondary = Get-EscapedODataString -Value $SecondaryEntityLogicalName
        $filter += " and secondaryobjecttypecode eq '$escapedSecondary'"
    }

    $select = "`$select=sdkmessagefilterid,primaryobjecttypecode,secondaryobjecttypecode"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessagefilters?`$filter=$filter&$select" -Method GET
    return $result.value | Select-Object -First 1
}

function Get-ProcessingStep {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$StepName
    )

    $escapedStepName = Get-EscapedODataString -Value $StepName
    $filter = "`$filter=name eq '$escapedStepName'"
    $select = "`$select=sdkmessageprocessingstepid,name,stage,mode,rank,filteringattributes,configuration"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingsteps?$filter&$select" -Method GET
    return $result.value | Select-Object -First 1
}

function Get-ProcessingStepsForAssembly {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$AssemblyId
    )

    $typesFilter = "`$filter=_pluginassemblyid_value eq '$AssemblyId'"
    $types = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "plugintypes?$typesFilter" -Method GET

    $steps = @()
    foreach ($type in $types.value) {
        $stepsFilter = "`$filter=_plugintypeid_value eq '$($type.plugintypeid)'"
        $select = "`$select=sdkmessageprocessingstepid,name,stage,mode,rank,filteringattributes,configuration"
        $typeSteps = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingsteps?$stepsFilter&$select" -Method GET
        $steps += $typeSteps.value
    }

    return $steps
}

function Get-StepImages {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$StepId
    )

    $filter = "`$filter=_sdkmessageprocessingstepid_value eq '$StepId'"
    $select = "`$select=sdkmessageprocessingstepimageid,name,entityalias,imagetype,attributes"
    $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingstepimages?$filter&$select" -Method GET
    return $result.value
}

function Get-Solution {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$UniqueName
    )

    $escapedUniqueName = Get-EscapedODataString -Value $UniqueName
    $filter = "`$filter=uniquename eq '$escapedUniqueName'"
    $select = "`$select=solutionid,uniquename,friendlyname,version"
    $expand = "`$expand=publisherid(`$select=customizationprefix,uniquename)"
    try {
        $result = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "solutions?$filter&$select&$expand" -Method GET
        $solution = $result.value | Select-Object -First 1

        if ($solution -and $solution.publisherid) {
            $solution | Add-Member -MemberType NoteProperty -Name "publisherprefix" -Value $solution.publisherid.customizationprefix -Force
        }

        return $solution
    }
    catch {
        Write-LogDebug "Could not query solution: $($_.Exception.Message)"
        return $null
    }
}

function Add-SolutionComponent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$SolutionUniqueName,
        [Parameter(Mandatory = $true)]
        [string]$ComponentId,
        [Parameter(Mandatory = $true)]
        [ValidateSet(91, 92)]
        [int]$ComponentType,
        [Parameter()]
        [switch]$WhatIf
    )

    $componentTypeName = switch ($ComponentType) {
        91 { "Plugin Assembly" }
        92 { "SDK Message Processing Step" }
    }

    if ($WhatIf) {
        Write-Log "[WhatIf] Would add $componentTypeName ($ComponentId) to solution '$SolutionUniqueName'"
        return $true
    }

    $addRequired = $false

    $body = @{
        ComponentId = $ComponentId
        ComponentType = $ComponentType
        SolutionUniqueName = $SolutionUniqueName
        AddRequiredComponents = $addRequired
    }

    try {
        $null = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "AddSolutionComponent" -Method POST -Body $body
        Write-LogDebug "Added $componentTypeName to solution '$SolutionUniqueName'"
        return $true
    }
    catch {
        $errorMsg = $_.Exception.Message
        if ($errorMsg -match "already exists" -or $errorMsg -match "duplicate") {
            Write-LogDebug "$componentTypeName already in solution"
            return $true
        }
        Write-LogWarning "Failed to add $componentTypeName to solution: $errorMsg"
        return $false
    }
}