AzureFunctions.PowerShell.Durable.SDK.psm1

#
# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
#

using namespace System.Net

# Set aliases for cmdlets to export
Set-Alias -Name Wait-ActivityFunction -Value Wait-DurableTask
Set-Alias -Name Invoke-ActivityFunction -Value Invoke-DurableActivity
Set-Alias -Name New-OrchestrationCheckStatusResponse -Value New-DurableOrchestrationCheckStatusResponse
Set-Alias -Name Start-NewOrchestration -Value Start-DurableOrchestration
Set-Alias -Name New-DurableRetryOptions -Value New-DurableRetryPolicy

function GetDurableClientFromModulePrivateData {
    $PrivateData = $PSCmdlet.MyInvocation.MyCommand.Module.PrivateData
    if ($null -eq $PrivateData -or $null -eq $PrivateData['DurableClient']) {
        throw "Could not find `DurableClient` private data. This can occur when you have not set application setting 'ExternalDurablePowerShellSDK' to 'true' or if you're using a DurableClient CmdLet but have no DurableClient binding declared in `function.json`."
    }
    else {
        $PrivateData['DurableClient']
    }
}

function Get-DurableStatus {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceId,

        [Parameter(
            ValueFromPipelineByPropertyName = $true)]
        [object] $DurableClient,

        [switch] $ShowHistory,

        [switch] $ShowHistoryOutput,

        [switch] $ShowInput
    )

    $ErrorActionPreference = 'Stop'

    if ($null -eq $DurableClient) {
        $DurableClient = GetDurableClientFromModulePrivateData
    }

    $requestUrl = "$($DurableClient.rpcBaseUrl)/instances/$InstanceId"

    $query = @()
    if ($ShowHistory.IsPresent) {
        $query += "showHistory=true"
    }
    if ($ShowHistoryOutput.IsPresent) {
        $query += "showHistoryOutput=true"
    }
    if ($ShowInput.IsPresent) {
        $query += "showInput=true"
    }

    if ($query.Count -gt 0) {
        $requestUrl += "?" + [string]::Join("&", $query)
    }

    Invoke-RestMethod -Uri $requestUrl
}

<#
.SYNOPSIS
    Start an orchestration Azure Function.
.DESCRIPTION
    Start an orchestration Azure Function with the given function name and input value.
.EXAMPLE
    PS > Start-DurableOrchestration -DurableClient Starter -FunctionName OrchestratorFunction -InputObject "input value for the orchestration function"
    Return the instance id of the new orchestration.
.PARAMETER FunctionName
    The name of the orchestration Azure Function you want to start.
.PARAMETER InputObject
    The input value that will be passed to the orchestration Azure Function.
.PARAMETER DurableClient
    The orchestration client object.
#>

function Start-DurableOrchestration {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory=$true,
            Position=0,
            ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $FunctionName,

        [Parameter(
            Position=1,
            ValueFromPipelineByPropertyName=$true)]
        [object] $InputObject,

        [Parameter(
            ValueFromPipelineByPropertyName=$true)]
        [object] $DurableClient,

        [Parameter(
            ValueFromPipelineByPropertyName=$true)]
        [string] $InstanceId
    )

    $ErrorActionPreference = 'Stop'

    if ($null -eq $DurableClient) {
        $DurableClient = GetDurableClientFromModulePrivateData
    }

    if (-not $InstanceId) {
        $InstanceId = (New-Guid).Guid
    }

    $Uri =
        if ($DurableClient.rpcBaseUrl) {
            # Fast local RPC path
            "$($DurableClient.rpcBaseUrl)orchestrators/$FunctionName$($InstanceId ? "/$InstanceId" : '')"
        } else {
            # Legacy app frontend path
            $UriTemplate = $DurableClient.creationUrls.createNewInstancePostUri
            $UriTemplate.Replace('{functionName}', $FunctionName).Replace('[/{instanceId}]', "/$InstanceId")
        }

    $Body = $InputObject | ConvertTo-Json -Compress -Depth 100
              
    $null = Invoke-RestMethod -Uri $Uri -Method 'POST' -ContentType 'application/json' -Body $Body
    
    return $instanceId
}

function Stop-DurableOrchestration {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceId,

        [Parameter(
            Mandatory = $true,
            Position = 1,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Reason
    )

    $ErrorActionPreference = 'Stop'

    if ($null -eq $DurableClient) {
        $DurableClient = GetDurableClientFromModulePrivateData
    }

    $requestUrl = "$($DurableClient.rpcBaseUrl)/instances/$InstanceId/terminate?reason=$([System.Web.HttpUtility]::UrlEncode($Reason))"

    Invoke-RestMethod -Uri $requestUrl -Method 'POST'
}

function Suspend-DurableOrchestration {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceId,

        [Parameter(
            Mandatory = $true,
            Position = 1,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Reason
    )

    $ErrorActionPreference = 'Stop'

    if ($null -eq $DurableClient) {
        $DurableClient = GetDurableClientFromModulePrivateData
    }

    $requestUrl = "$($DurableClient.rpcBaseUrl)/instances/$InstanceId/suspend?reason=$([System.Web.HttpUtility]::UrlEncode($Reason))"

    Invoke-RestMethod -Uri $requestUrl -Method 'POST'
}

function Resume-DurableOrchestration {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceId,

        [Parameter(
            Mandatory = $true,
            Position = 1,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Reason
    )

    $ErrorActionPreference = 'Stop'

    if ($null -eq $DurableClient) {
        $DurableClient = GetDurableClientFromModulePrivateData
    }

    $requestUrl = "$($DurableClient.rpcBaseUrl)/instances/$InstanceId/resume?reason=$([System.Web.HttpUtility]::UrlEncode($Reason))"

    Invoke-RestMethod -Uri $requestUrl -Method 'POST'
}

function IsValidUrl([uri]$Url) {
    $Url.IsAbsoluteUri -and ($Url.Scheme -in 'http', 'https')
}

function GetUrlOrigin([uri]$Url) {
    $fixedOriginUrl = New-Object System.UriBuilder
    $fixedOriginUrl.Scheme = $Url.Scheme
    $fixedOriginUrl.Host = $Url.Host
    $fixedOriginUrl.Port = $Url.Port
    $fixedOriginUrl.ToString()
}

function New-DurableOrchestrationCheckStatusResponse {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory=$true,
            ValueFromPipelineByPropertyName=$true)]
        [object] $Request,

        [Parameter(
            Mandatory=$true,
            ValueFromPipelineByPropertyName=$true)]
        [string] $InstanceId,

        [Parameter(
            ValueFromPipelineByPropertyName=$true)]
        [object] $DurableClient
    )

    if ($null -eq $DurableClient) {
        $DurableClient = GetDurableClientFromModulePrivateData
    }

    [uri]$requestUrl = $Request.Url
    $requestHasValidUrl = IsValidUrl $requestUrl
    $requestUrlOrigin = GetUrlOrigin $requestUrl
    
    $httpManagementPayload = [ordered]@{ }
    foreach ($entry in $DurableClient.managementUrls.GetEnumerator()) {
        $value = $entry.Value
    
        if ($requestHasValidUrl -and (IsValidUrl $value)) {
            $dataOrigin = GetUrlOrigin $value
            $value = $value.Replace($dataOrigin, $requestUrlOrigin)
        }
      
        $value = $value.Replace($DurableClient.managementUrls.id, $InstanceId)
        $httpManagementPayload.Add($entry.Name, $value)
    }
    
    [HttpResponseContext]@{
        StatusCode = [HttpStatusCode]::Accepted
        Body = $httpManagementPayload
        Headers = @{
            'Content-Type' = 'application/json'
            'Location' = $httpManagementPayload.statusQueryGetUri
            'Retry-After' = 10
        }
    }
}

<#
.SYNOPSIS
    Send an external event to an orchestration instance.
.DESCRIPTION
    Send an external event with the given event name, and event data to an orchestration instance with the given instance ID.
.EXAMPLE
    PS > Send-DurableExternalEvent -InstanceId "example-instance-id" -EventName "ExampleExternalEvent" -EventData "data for the external event"
    Return the instance id of the new orchestration.
.PARAMETER InstanceId
    The ID of the orchestration instance that will handle the external event.
.PARAMETER EventName
    The name of the external event.
.PARAMETER EventData
    The JSON-serializable data associated with the external event.
.PARAMETER TaskHubName
    The TaskHubName of the orchestration instance that will handle the external event.
.PARAMETER ConnectionName
    The name of the connection string associated with TaskHubName
#>

function Send-DurableExternalEvent {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory=$true,
            Position=0,
            ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceId,

        [Parameter(
            Mandatory=$true,
            Position=1,
            ValueFromPipelineByPropertyName=$true)]
        [string] $EventName,

        [Parameter(
            Position=2,
            ValueFromPipelineByPropertyName=$true)]
        [object] $EventData,

        [Parameter(
            ValueFromPipelineByPropertyName=$true)]
        [string] $TaskHubName,

        [Parameter(
            ValueFromPipelineByPropertyName=$true)]
        [string] $ConnectionName
    )
    
    $DurableClient = GetDurableClientFromModulePrivateData

    $RequestUrl = GetRaiseEventUrl -DurableClient $DurableClient -InstanceId $InstanceId -EventName $EventName -TaskHubName $TaskHubName -ConnectionName $ConnectionName

    $Body = $EventData | ConvertTo-Json -Compress -Depth 100
              
    $null = Invoke-RestMethod -Uri $RequestUrl -Method 'POST' -ContentType 'application/json' -Body $Body
}

function GetRaiseEventUrl(
    $DurableClient,
    [string] $InstanceId,
    [string] $EventName,
    [string] $TaskHubName,
    [string] $ConnectionName) {

    $RequestUrl = $DurableClient.rpcBaseUrl + "/instances/$InstanceId/raiseEvent/$EventName"
    
    $query = @()
    if ($null -eq $TaskHubName) {
        $query += "taskHub=$TaskHubName"
    }
    if ($null -eq $ConnectionName) {
        $query += "connection=$ConnectionName"
    }
    if ($query.Count -gt 0) {
        $RequestUrl += "?" + [string]::Join("&", $query)
    }

    return $RequestUrl
}