Public/New-PSDVTableWebHook.ps1

function New-PSDVTableWebHook {
    <#
    .SYNOPSIS
    Creates a new webhook registration for a Dataverse table.
 
    .DESCRIPTION
    New-PSDVTableWebHook registers a webhook endpoint to be triggered when specified operations occur on a Dataverse table.
    The function creates a service endpoint, retrieves the necessary SDK message and filter IDs, and then creates the
    webhook step registration. This enables real-time notifications to external systems when data changes occur.
 
    .PARAMETER Table
    The logical name of the Dataverse table to monitor for webhook triggers.
 
    .PARAMETER WebHookName
    The display name for the webhook registration.
 
    .PARAMETER TriggerUri
    The HTTP endpoint URL that will receive webhook notifications when the trigger events occur.
 
    .PARAMETER Operation
    The SDK message operation to monitor. Valid values are Create, Update, Delete, or Retrieve. Default is Create.
 
    .PARAMETER AuthSecret
    The optional authentication secret for webhook security. This will be passed in the x-dv-webhook-secret header.
 
    .PARAMETER Stage
    The execution stage for the webhook. Valid values are PreValidation (10), PreOperation (20), MainOperation (30), or PostOperation (40). Default is PostOperation.
 
    .PARAMETER Rank
    The execution order rank within the stage. Lower numbers execute first. Default is 1.
 
    .PARAMETER Mode
    The execution mode. Valid values are Synchronous (0) or Asynchronous (1). Default is Asynchronous.
 
    .PARAMETER SupportedDeployment
    The deployment scope. Valid values are ServerOnly (0), ClientOnly (1), or Both (2). Default is ServerOnly.
 
    .PARAMETER FilteringAttributes
    An optional array of column logical names to filter on. When specified, the webhook will only trigger when one or more of these columns are modified. This is only applicable for Update operations. If not specified, the webhook triggers for all column changes.
 
    .PARAMETER PreImage
    When specified, registers a pre-image on the webhook step. A pre-image captures a snapshot of the entity record's values before the operation is executed. This is useful for comparing old and new values during Update or Delete operations.
 
    .PARAMETER PreImageAttributes
    An optional array of column logical names to include in the pre-image. When specified, only these columns will be captured in the pre-image snapshot. If not specified, all columns are included. Only used when -PreImage is specified.
 
    .PARAMETER PostImage
    When specified, registers a post-image on the webhook step. A post-image captures a snapshot of the entity record's values after the operation is executed. This is useful for accessing the final state of the record after Create or Update operations.
 
    .PARAMETER PostImageAttributes
    An optional array of column logical names to include in the post-image. When specified, only these columns will be captured in the post-image snapshot. If not specified, all columns are included. Only used when -PostImage is specified.
 
    .EXAMPLE
    New-PSDVTableWebHook -Table "account" -WebHookName "Account Changes Monitor" -TriggerUri "https://myapp.azurewebsites.net/api/DataverseTrigger" -Operation "Create"
 
    Creates a webhook to monitor account creation events.
 
    .EXAMPLE
    New-PSDVTableWebHook -Table "spork_sporkuserrequest" -WebHookName "SPORK Users New Record WebHook" -TriggerUri "https://sporkapps.azurewebsites.net/api/DataverseTrigger" -Operation "Create" -AuthSecret "DontTell"
 
    Creates a webhook with authentication secret for a custom table.
 
    .EXAMPLE
    New-PSDVTableWebHook -Table "contact" -WebHookName "Contact Update Monitor" -TriggerUri "https://myapp.com/webhook" -Operation "Update" -Stage "PreOperation" -Mode "Synchronous"
 
    Creates a synchronous webhook that fires before contact updates are processed.
 
    .EXAMPLE
    New-PSDVTableWebHook -Table "account" -WebHookName "Account Name Monitor" -TriggerUri "https://myapp.com/webhook" -Operation "Update" -FilteringAttributes @("name", "accountcategorycode")
 
    Creates a webhook that only triggers when the account name or category code fields are updated.
 
    .EXAMPLE
    New-PSDVTableWebHook -Table "contact" -WebHookName "Contact Update With PreImage" -TriggerUri "https://myapp.com/webhook" -Operation "Update" -PreImage -PreImageAttributes @("firstname", "lastname", "emailaddress1")
 
    Creates a webhook that captures a pre-image of the firstname, lastname, and emailaddress1 fields before contact updates.
 
    .EXAMPLE
    New-PSDVTableWebHook -Table "account" -WebHookName "Account Create With PostImage" -TriggerUri "https://myapp.com/webhook" -Operation "Create" -PostImage -PostImageAttributes @("name", "accountnumber")
 
    Creates a webhook that captures a post-image of the name and accountnumber fields after account creation.
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [String]
        $Table,

        [Parameter(Mandatory)]
        [String]
        $WebHookName,

        [Parameter(Mandatory)]
        [String]
        $TriggerUri,

        [Parameter()]
        [ValidateSet('Create', 'Update', 'Delete', 'Retrieve')]
        [String]
        $Operation = 'Create',

        [Parameter()]
        [String]
        $AuthSecret,

        [Parameter()]
        [ValidateSet('PreValidation', 'PreOperation', 'MainOperation', 'PostOperation')]
        [String]
        $Stage = 'PostOperation',

        [Parameter()]
        [ValidateRange(1, 2147483647)]
        [Int32]
        $Rank = 1,

        [Parameter()]
        [ValidateSet('Synchronous', 'Asynchronous')]
        [String]
        $Mode = 'Asynchronous',

        [Parameter()]
        [ValidateSet('ServerOnly', 'ClientOnly', 'Both')]
        [String]
        $SupportedDeployment = 'ServerOnly',

        [Parameter()]
        [String[]]
        $FilteringAttributes,

        [Parameter()]
        [Switch]
        $PreImage,

        [Parameter()]
        [String[]]
        $PreImageAttributes,

        [Parameter()]
        [Switch]
        $PostImage,

        [Parameter()]
        [String[]]
        $PostImageAttributes
    )

    if ($null -eq $Global:DATAVERSEACCESSTOKEN) {
        throw 'No existing connection to Dataverse Environment, run Connect-PSDVOrg before executing other PSDV cmdlets'
    }

    # Validate filtering attributes are only used with Update operations
    if ($FilteringAttributes -and $Operation -ne 'Update') {
        throw "FilteringAttributes parameter can only be used with Update operations. Current operation: $Operation"
    }

    # Validate PreImageAttributes are only used with PreImage
    if ($PreImageAttributes -and -not $PreImage) {
        throw "PreImageAttributes parameter can only be used when -PreImage is specified"
    }

    # Validate PreImage is only used with Update or Delete operations
    if ($PreImage -and $Operation -notin @('Update', 'Delete')) {
        throw "PreImage parameter can only be used with Update or Delete operations. Current operation: $Operation"
    }

    # Validate PostImageAttributes are only used with PostImage
    if ($PostImageAttributes -and -not $PostImage) {
        throw "PostImageAttributes parameter can only be used when -PostImage is specified"
    }

    # Validate PostImage is only used with Create or Update operations
    if ($PostImage -and $Operation -notin @('Create', 'Update')) {
        throw "PostImage parameter can only be used with Create or Update operations. Current operation: $Operation"
    }

    # Convert stage names to numeric values
    $stageMap = @{
        'PreValidation' = 10
        'PreOperation' = 20
        'MainOperation' = 30
        'PostOperation' = 40
    }

    # Convert mode names to numeric values
    $modeMap = @{
        'Synchronous' = 0
        'Asynchronous' = 1
    }

    # Convert deployment names to numeric values
    $deploymentMap = @{
        'ServerOnly' = 0
        'ClientOnly' = 1
        'Both' = 2
    }

    # Check for existing webhook with same table, operation, and URL
    Write-Verbose "Checking for existing webhook with same table ($Table), operation ($Operation), and URL ($TriggerUri)"
    try {
        $tableLiteral = ConvertTo-PSDVODataStringLiteral -Value $Table
        $operationLiteral = ConvertTo-PSDVODataStringLiteral -Value $Operation
        $triggerUriLiteral = ConvertTo-PSDVODataStringLiteral -Value $TriggerUri
        $existingWebhookQuery = "eventhandler_serviceendpoint ne null and eventhandler_serviceendpoint/serviceendpointid ne null and sdkmessagefilterid/primaryobjecttypecode eq $tableLiteral and sdkmessageid/name eq $operationLiteral and eventhandler_serviceendpoint/url eq $triggerUriLiteral"
        $existingWebhook = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/sdkmessageprocessingsteps" -Select "sdkmessageprocessingstepid,name" -Expand "eventhandler_serviceendpoint(`$select=serviceendpointid,name,url)" -Filter $existingWebhookQuery
        
        if ($existingWebhook -and $existingWebhook.Count -gt 0) {
            $existingWebhookName = if ($existingWebhook[0].eventhandler_serviceendpoint.name) { $existingWebhook[0].eventhandler_serviceendpoint.name } else { "Unknown" }
            throw "A webhook already exists for table '$Table', operation '$Operation', and URL '$TriggerUri'. Existing webhook: $existingWebhookName"
        }
        
        Write-Verbose "No duplicate webhook found. Proceeding with webhook creation."
    }
    catch {
        if ($_.Exception.Message -like "*webhook already exists*") {
            throw
        }
        Write-Verbose "Error checking for duplicates (proceeding anyway): $($_.Exception.Message)"
    }

    try {
        # Step 1: Create service endpoint
        Write-Verbose "Creating service endpoint for webhook: $WebHookName"
        
        $serviceEndPointSetup = @{
            "name" = $WebHookName
            "url" = $TriggerUri
            "contract" = 8  # WebHook contract type
            "authtype" = 5  # HttpHeader authentication
        }

        if ($PSBoundParameters.ContainsKey('AuthSecret')) {
            $escapedAuthSecret = ConvertTo-PSDVXmlAttributeValue -Value $AuthSecret
            $serviceEndPointSetup["authvalue"] = "<settings><setting name=""x-dv-webhook-secret"" value=""$escapedAuthSecret""/></settings>"
        } else {
            $serviceEndPointSetup["authvalue"] = "<settings></settings>"
        }

        if ($PSCmdlet.ShouldProcess($WebHookName, "Create service endpoint")) {
            $serviceEndpointHeaders = @{
                'Prefer' = 'odata.include-annotations="*",return=representation'
            }
            $serviceEndpoint = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/serviceendpoints" -Method Post -Body $serviceEndPointSetup -Headers $serviceEndpointHeaders
            Write-Verbose "Service endpoint created with ID: $($serviceEndpoint.serviceendpointid)"
        } else {
            Write-Verbose "Would create service endpoint for: $WebHookName"
            return
        }

        # Step 2: Get SDK message ID
        Write-Verbose "Retrieving SDK message ID for operation: $Operation"
        $operationLiteral = ConvertTo-PSDVODataStringLiteral -Value $Operation
        $sdkMessage = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/sdkmessages" -Select "sdkmessageid,name" -Filter "name eq $operationLiteral"
        
        if (-not $sdkMessage -or $sdkMessage.Count -eq 0) {
            throw "SDK message '$Operation' not found"
        }
        
        $sdkMessageId = $sdkMessage.sdkmessageid
        Write-Verbose "SDK message ID: $sdkMessageId"

        # Step 3: Get SDK message filter ID
        Write-Verbose "Retrieving SDK message filter for table '$Table' and operation '$Operation'"
        $tableLiteral = ConvertTo-PSDVODataStringLiteral -Value $Table
        $operationLiteral = ConvertTo-PSDVODataStringLiteral -Value $Operation
        $sdkMessageFilter = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/sdkmessagefilters" -Select "sdkmessagefilterid,primaryobjecttypecode" -Filter "primaryobjecttypecode eq $tableLiteral and sdkmessageid/name eq $operationLiteral"
        
        if (-not $sdkMessageFilter -or $sdkMessageFilter.Count -eq 0) {
            throw "SDK message filter for table '$Table' and operation '$Operation' not found"
        }
        
        $sdkMessageFilterId = $sdkMessageFilter.sdkmessagefilterid
        Write-Verbose "SDK message filter ID: $sdkMessageFilterId"

        # Step 4: Create webhook step
        Write-Verbose "Creating webhook step registration"
        $webhookStepName = "$($WebHookName.ToLower().Replace(' ', '.')).$Table.$($Operation.ToLower())"
        
        $webhookStep = @{
            "name" = $webhookStepName
            "description" = "$WebHookName - $Table - $Operation"
            "stage" = $stageMap[$Stage]
            "rank" = $Rank
            "mode" = $modeMap[$Mode]
            "supporteddeployment" = $deploymentMap[$SupportedDeployment]
            "eventhandler_serviceendpoint@odata.bind" = "/serviceendpoints($($serviceEndpoint.serviceendpointid))"
            "sdkmessageid@odata.bind" = "/sdkmessages($sdkMessageId)"
            "sdkmessagefilterid@odata.bind" = "/sdkmessagefilters($sdkMessageFilterId)"
        }

        # Add filtering attributes if specified
        if ($FilteringAttributes -and $FilteringAttributes.Count -gt 0) {
            $webhookStep["filteringattributes"] = ($FilteringAttributes -join ",")
            Write-Verbose "Adding filtering attributes: $($FilteringAttributes -join ', ')"
        }

        if ($PSCmdlet.ShouldProcess($webhookStepName, "Create webhook step")) {
            $webhookStepHeaders = @{
                'Prefer' = 'odata.include-annotations="*",return=representation'
            }
            $webhookStepResult = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/sdkmessageprocessingsteps" -Method Post -Body $webhookStep -Headers $webhookStepHeaders
            Write-Verbose "Webhook step created with ID: $($webhookStepResult.sdkmessageprocessingstepid)"

            # Step 5: Register pre-image if requested
            $preImageId = $null
            if ($PreImage) {
                Write-Verbose "Registering pre-image on webhook step"
                $preImageBody = @{
                    "sdkmessageprocessingstepid@odata.bind" = "/sdkmessageprocessingsteps($($webhookStepResult.sdkmessageprocessingstepid))"
                    "imagetype" = 0  # PreImage
                    "name" = "PreImage"
                    "entityalias" = "PreImage"
                    "messagepropertyname" = "Target"
                }

                if ($PreImageAttributes -and $PreImageAttributes.Count -gt 0) {
                    $preImageBody["attributes"] = ($PreImageAttributes -join ",")
                    Write-Verbose "Pre-image attributes: $($PreImageAttributes -join ', ')"
                }

                $preImageHeaders = @{
                    'Prefer' = 'odata.include-annotations="*",return=representation'
                }
                $preImageResult = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/sdkmessageprocessingstepimages" -Method Post -Body $preImageBody -Headers $preImageHeaders
                $preImageId = $preImageResult.sdkmessageprocessingstepimageid
                Write-Verbose "Pre-image registered with ID: $preImageId"
            }

            # Step 6: Register post-image if requested
            $postImageId = $null
            if ($PostImage) {
                Write-Verbose "Registering post-image on webhook step"
                $postImageBody = @{
                    "sdkmessageprocessingstepid@odata.bind" = "/sdkmessageprocessingsteps($($webhookStepResult.sdkmessageprocessingstepid))"
                    "imagetype" = 1  # PostImage
                    "name" = "PostImage"
                    "entityalias" = "PostImage"
                    "messagepropertyname" = "Target"
                }

                if ($PostImageAttributes -and $PostImageAttributes.Count -gt 0) {
                    $postImageBody["attributes"] = ($PostImageAttributes -join ",")
                    Write-Verbose "Post-image attributes: $($PostImageAttributes -join ', ')"
                }

                $postImageHeaders = @{
                    'Prefer' = 'odata.include-annotations="*",return=representation'
                }
                $postImageResult = Invoke-PSDVWebRequest -WebUri "api/data/v9.2/sdkmessageprocessingstepimages" -Method Post -Body $postImageBody -Headers $postImageHeaders
                $postImageId = $postImageResult.sdkmessageprocessingstepimageid
                Write-Verbose "Post-image registered with ID: $postImageId"
            }
            
            # Return webhook registration details
            return [PSCustomObject]@{
                WebHookName = $WebHookName
                ServiceEndpointId = $serviceEndpoint.serviceendpointid
                WebHookStepId = $webhookStepResult.sdkmessageprocessingstepid
                Table = $Table
                Operation = $Operation
                TriggerUri = $TriggerUri
                Stage = $Stage
                Mode = $Mode
                Rank = $Rank
                SupportedDeployment = $SupportedDeployment
                FilteringAttributes = if ($FilteringAttributes) { $FilteringAttributes } else { $null }
                PreImageId = $preImageId
                PreImageAttributes = if ($PreImageAttributes) { $PreImageAttributes } else { $null }
                PostImageId = $postImageId
                PostImageAttributes = if ($PostImageAttributes) { $PostImageAttributes } else { $null }
            }
        }
    }
    catch {
        throw "Error creating webhook '$WebHookName': $($_.Exception.Message)"
    }
}