PsLogicAppExtractor.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PsLogicAppExtractor.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName PsLogicAppExtractor.Import.DoDotSource -Fallback $false
if ($PsLogicAppExtractor_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName PsLogicAppExtractor.Import.IndividualFiles -Fallback $false
if ($PsLogicAppExtractor_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # End it here, do not load compiled code below
    return
}

#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PsLogicAppExtractor' -Language 'en-US'

class Helper {
    Helper([object] $values) {
        if ($values -is [System.Collections.IDictionary]) {
            foreach ($key in $values.Keys) {
                if ($this.PSObject.Properties.Item($key)) {
                    $this.$key = $values[$key]
                }
            }
        }
        else {
            foreach ($property in $values.PSObject.Properties) {
                if ($this.PSObject.Properties.Item($property.Name)) {
                    $this.($property.Name) = $property.Value
                }
            }
        }
    }
}

class Definition: Helper {
    [object] ${$schema}
    [string] $contentVersion
    [object] $parameters
    [object] $triggers
    [object] $actions
    [object] $outputs

    Definition([object] $values) : base($values) { }
}

class Properties : Helper {
    [string] $state
    [Definition] $definition
    [object] $parameters
    [object] $integrationAccount
    [object] $accessControl
    
    Properties([object] $values) : base($values) { }
}

class LogicApp : Helper {
    [string] $type
    [string] $apiVersion
    [string] $name
    [string] $location
    [object] $tags
    [object] $identity
    [Properties] $properties

    LogicApp([object] $values) : base($values) { }
}

class ArmTemplate: Helper {
    [object] ${$schema}
    [string] $contentVersion
    [object] $parameters
    [object] $variables
    [object] $resources
    [object] $outputs
    
    ArmTemplate([object] $values) : base($values) { }
}



<#
    .SYNOPSIS
        Get the header for a runbook file
         
    .DESCRIPTION
        Gets the header for a runbook file, containing the sane defaults
         
        Allows you to prepare the runbook file as much as possible, based on the parameters that you pass to it
         
    .PARAMETER SubscriptionId
        Id of the subscription that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session either needs to be "connected" to the subscription or at least have permissions to work against the subscription
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER ResourceGroup
        Name of the resource group that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the resource group
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER Name
        Name of the logic app, that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the logic app
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER ApiVersion
        The ApiVersion that you want the LogicApp to be working against
         
        The default value is: "2019-05-01"
         
    .PARAMETER IncludePrefixSuffix
        Instruct the cmdlet to add the different prefix and suffix options, with the default values that comes with the module
         
        This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior
         
    .EXAMPLE
        PS C:\> Get-BuildHeader
         
        Creates the bare minimum header for the runbook file
        Prepares the Properties object with sane defaults, allowing you to edit them after the file has been created
         
    .EXAMPLE
        PS C:\> Get-BuildHeader -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg"
         
        Creates the bare minimum header for the runbook file
        Prepares the Properties object with SubscriptionId and ResourceGroup, and sane defaults for the remaining objects, allowing you to edit them after the file has been created
         
    .EXAMPLE
        PS C:\> Get-BuildHeader -Name "TestLogicApp" -ApiVersion "2019-05-01"
         
        Creates the bare minimum header for the runbook file
        Prepares the Properties object with Name and ApiVersion, and sane defaults for the remaining objects, allowing you to edit them after the file has been created
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-BuildHeader {
    [CmdletBinding()]
    param (
        [string] $SubscriptionId,

        [string] $ResourceGroup,

        [string] $Name,

        [string] $ApiVersion = "2019-05-01",

        [switch] $IncludePrefixSuffix
    )

    $res = New-Object System.Collections.Generic.List[System.Object]

    $res.Add("# Object to store the needed parameters for when running the export")
    $res.Add("Properties {")
    
    if ($SubscriptionId) {
        $res.Add('$SubscriptionId = "{0}"' -f $SubscriptionId)
    }
    else {
        $res.Add('$SubscriptionId = $null')
    }

    if ($ResourceGroup) {
        $res.Add('$ResourceGroup = "{0}"' -f $ResourceGroup)
    }
    else {
        $res.Add('$ResourceGroup = $null')
    }
    
    if ($Name) {
        $res.Add('$Name = "{0}"' -f $Name)
    }
    else {
        $res.Add('$Name = ""')
    }

    if ($ApiVersion) {
        $res.Add('$ApiVersion = "{0}"' -f $ApiVersion)
    }
    else {
        $res.Add('$ApiVersion = ""')
    }
    
    if ($IncludePrefixSuffix) {
        $res.Add('$Tag_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.prefix))
        $res.Add('$Tag_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.suffix))

        $res.Add('$Parm_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.prefix))
        $res.Add('$Parm_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.suffix))

        $res.Add('$Connection_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.prefix))
        $res.Add('$Connection_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.suffix))

        $res.Add('$Trigger_Prefix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.prefix))
        $res.Add('$Trigger_Suffix = "{0}"' -f $(Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.suffix))
    }

    $res.Add('}')
    #Above line completes the Properties declaration

    $res.Add('')
    $res.Add('# Used to import the needed classes into the powershell session, to help with the export of the Logic App')
    $res.Add('."$(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Classes)\PsLogicAppExtractor.class.ps1"')
    $res.Add('')
    $res.Add("# Path variable for all the tasks that is available from the PsLogicAppExtractor module")
    $res.Add('$pathTasks = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Tasks)')

    $res.Add('')
    $res.Add("# Include all the tasks that is available from the PsLogicAppExtractor module")
    $res.Add("Include `"`$pathTasks\All\All.task.ps1`"")
    $res.Add('')

    $res
}


<#
    .SYNOPSIS
        Show the window used for OAuth consent
         
    .DESCRIPTION
        Used to handle the consent flow of ApiConnection objects, where the user needs to fill in an user account / credentials
         
        It requires human interaction to handle the consent flow, but this cmdlet helps make it a smooth process
         
    .PARAMETER Url
        The url of the endpoint where the consent flow for the specific ApiConnection object can be completed
         
    .EXAMPLE
        PS C:\> Show-OAuthConsentWindow -Url "https://logic-apis-westeurope.consent.azure-apim.net/login?data=eyJMb2dpbklkIjo..."
         
        This will invoke the consent flow
        It will prompt the user to enter an user account / credentials
         
        It returns the url, containing the code needed to complete the constent flow against the ApiConnection Object
         
    .NOTES
        This is highly inspired by the previous work of other smart people:
        https://github.com/logicappsio/LogicAppConnectionAuth/blob/master/LogicAppConnectionAuth.ps1
        https://github.com/OfficeDev/microsoft-teams-apps-requestateam/blob/master/Deployment/Scripts/deploy.ps1
         
        Author: Mötz Jensen (@Splaxi)
#>

function Show-OAuthConsentWindow {
    [CmdletBinding()]
    [OutputType('System.String')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    param (
        [Alias('Uri')]
        [string] $Url
    )

    Add-Type -AssemblyName System.Windows.Forms

    #Building the outer body of the form
    $form = New-Object -TypeName System.Windows.Forms.Form -Property @{Width = 600; Height = 800 }
    
    #Building the inner part of the content on the form
    $web = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width = 580; Height = 780; Url = ($url -f ($Scope -join "%20")) }
    
    $docComp = {
        $Global:uri = $web.Url.AbsoluteUri

        #Close on error or on returned code
        if ($Global:Uri -match "error=[^&]*|code=[^&]*") { $form.Close() }
    }

    #Register the event handler
    $web.Add_DocumentCompleted($docComp)
    
    #Construct the form
    $form.Controls.Add($web)

    $form.Add_Shown( { $form.Activate() })

    #Display the form
    $form.ShowDialog() | Out-Null

    # If below is the URL - the user exited the flow before completing the consent
    if ($Global:Uri -like "https://login.microsoftonline.com/common/oauth2/authorize*") {
        ""
    }
    else {
        $Global:Uri
    }
}


<#
    .SYNOPSIS
        Add new parameter to the ARM template
         
    .DESCRIPTION
        Adds or overwrites an ARM template parameter by the name provided, and allows you to specify the default value, type and the metadata decription
         
    .PARAMETER InputObject
        The ARM object that you want to work against
         
        It has to be a object of the type [ArmTemplate] for it to work properly
         
    .PARAMETER Name
        Name of the parameter that you want to work against
         
        If the parameter exists, the value gets overrided otherwise a new parameter is added to the list of parameters
         
    .PARAMETER Type
        The type of the ARM template parameter
         
        It supports all known types
         
    .PARAMETER Value
        The default value, that you want to assign to the ARM template parameter
         
    .PARAMETER Description
        The metadata description that you want to assign to the ARM template parameters
         
    .EXAMPLE
        PS C:\> Add-ArmParameter -InputObject $armObj -Name "logicAppName" -Type "string" -Value "TestLogicApp"
         
        Creates / updates the logicAppName ARM template parameter
        Sets the type of the parameter to: string
        Sets the default value to: TestLogicApp
         
    .EXAMPLE
        PS C:\> Add-ArmParameter -InputObject $armObj -Name "logicAppName" -Type "string" -Value "TestLogicApp" -Description "This is the name we extracted from the orignal LogicApp"
         
        Creates / updates the logicAppName ARM template parameter
        Sets the type of the parameter to: string
        Sets the default value to: TestLogicApp
        Sets the metadata description
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Add-ArmParameter {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('ParameterName')]
        [Parameter(Mandatory = $true)]
        [string] $Name,

        [Parameter(Mandatory = $true)]
        [string] $Type,

        [Parameter(Mandatory = $true)]
        [object] $Value,

        [string] $Description
    )

    if ($Description) {
        $valueObj = $([ordered]@{
                type         = $Type;
                defaultValue = $Value;
                metadata     = [ordered]@{
                    description = $Description
                }
            })

    }
    else {
        $valueObj = $([ordered]@{
                type         = $Type;
                defaultValue = $Value;
            })
    }

    if ($InputObject.parameters.$Name) {
        $InputObject.parameters.$Name = $($valueObj)
    }
    else {
        $InputObject.parameters | Add-Member -MemberType NoteProperty -Name $Name -Value $valueObj
    }
    
    $InputObject
}


<#
    .SYNOPSIS
        Add new variable to the ARM template
         
    .DESCRIPTION
        Adds or overwrites an ARM template variable by the name provided, and allows you to specify the value
         
    .PARAMETER InputObject
        The ARM object that you want to work against
         
        It has to be a object of the type [ArmTemplate] for it to work properly
         
    .PARAMETER Name
        Name of the variable that you want to work against
         
        If the variable exists, the value gets overrided otherwise a new variable is added to the list of variables
         
    .PARAMETER Value
        The value, that you want to assign to the ARM template variable
         
    .EXAMPLE
        PS C:\> Add-ArmVariable -InputObject $armObj -Name "logicAppName" -Value "TestLogicApp"
         
        Creates / updates the logicAppName ARM template variable
        Sets the value to: TestLogicApp
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Add-ArmVariable {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('VariableName')]
        [Parameter(Mandatory = $true)]
        [string] $Name,

        [Parameter(Mandatory = $true)]
        [object] $Value
    )

    if ($InputObject.variables.$Name) {
        $InputObject.variables.$Name = $($Value)
    }
    else {
        $InputObject.variables | Add-Member -MemberType NoteProperty -Name $Name -Value $($Value)
    }

    $InputObject
}


<#
    .SYNOPSIS
        Add new parm (parameter) to the LogicApp object
         
    .DESCRIPTION
        Adds or overwrites a LogicApp parm (parameter) by the name provided, and allows you to specify the default value and the type
         
    .PARAMETER InputObject
        The ARM object that you want to work against
         
        It has to be a object of the type [LogicApp] for it to work properly
         
    .PARAMETER Name
        Name of the parm (parameter) that you want to work against
         
        If the parm (parameter) exists, the value gets overrided otherwise a new parm (parameter) is added to the list of parms (parameters)
         
    .PARAMETER Type
        The type of the LogicApp parm (parameter)
         
        It supports all known types
         
    .PARAMETER Value
        The default value, that you want to assign to the LogicApp parm (parameter)
         
    .EXAMPLE
        PS C:\> Add-LogicAppParm -InputObject $lgObj -Name "TriggerQueue" -Type "string" -Value "Inbound"
         
        Creates / updates the TriggerQueue LogicApp parm (parameter)
        Sets the type of the parameter to: string
        Sets the default value to: Inbound
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Add-LogicAppParm {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('ParmName')]
        [Parameter(Mandatory = $true)]
        [string] $Name,

        [Parameter(Mandatory = $true)]
        [string] $Type,

        [Parameter(Mandatory = $true)]
        [object] $Value
    )

    $valueObj = $([ordered]@{
            type         = $Type;
            defaultValue = $Value;
        })
        
    if ($InputObject.properties.definition.parameters.$Name) {
        $InputObject.properties.definition.parameters.$Name = $($valueObj)
    }
    else {
        $InputObject.properties.definition.parameters | Add-Member -MemberType NoteProperty -Name $Name -Value $valueObj
    }

    $InputObject
}


<#
    .SYNOPSIS
        Format the name with the prefix and suffix
         
    .DESCRIPTION
        Format the name with the prefix and suffix
         
        If the passed prefix and suffix is not $null, then they are used
         
        Otherwise the cmdlet will default back to the configuration for each type, that is persisted in the configuration store
         
    .PARAMETER Type
        The type of name that you want to work against
         
        Allowed values:
        Tag
        Connection
        Parameter
        Parm
         
    .PARAMETER Prefix
        The prefix that you want to append to the name
         
        If empty / $null - then the cmdlet will use the prefix that is stored for the specific type
         
    .PARAMETER Suffix
        The suffix that you want to append to the name
         
        If empty / $null - then the cmdlet will use the suffix that is stored for the specific type
         
    .PARAMETER Value
        The string value that you want to have the prefix and suffix concatenated with
         
    .EXAMPLE
        PS C:\> Format-Name -Type "Tag" -Value "CostCenter"
         
        Formats the value: CostCenter with the default prefix and suffix for the type: Tag
        The default prefix is: tag_
        The default suffix is: $null
         
        The output will be: tag_CostCenter
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Format-Name {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    param (
        [ValidateSet('Tag', 'Connection', 'Parameter', 'Parm', 'Trigger')]
        [Parameter(Mandatory = $true)]
        [string] $Type,

        [string] $Prefix,

        [string] $Suffix,

        [Alias('Name')]
        [Parameter(Mandatory = $true)]
        [string] $Value
    )
    
    switch ($Type) {
        "Tag" {
            if ($Prefix -or $Suffix) {
                "$Prefix$Value$Suffix"
            }
            else {
                $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.prefix
                $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.tag.suffix

                "$Prefix$Value$Suffix"
            }
        }
        "Connection" {
            if ($Prefix -or $Suffix) {
                "$Prefix$Value$Suffix"
            }
            else {
                $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.prefix
                $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.connection.suffix

                "$Prefix$Value$Suffix"
            }
        }
        "Parameter" {  }
        "Parm" {
            if ($Prefix -or $Suffix) {
                "$Prefix$Value$Suffix"
            }
            else {
                $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.prefix
                $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.parm.suffix

                "$Prefix$Value$Suffix"
            }
        }
        "Trigger" {
            if ($Prefix -or $Suffix) {
                "$Prefix$Value$Suffix"
            }
            else {
                $Prefix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.prefix
                $Suffix = Get-PSFConfigValue -FullName PsLogicAppExtractor.prefixsuffix.trigger.suffix

                "$Prefix$Value$Suffix"
            }
        }
        Default { "$Prefix$Value$Suffix" }
    }
}

<#
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.prefix' -Value "tag_" -Initialize -Description "The default prefix for Tag objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.suffix' -Value "" -Initialize -Description "The default suffix for Tag objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string"
 
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.prefix' -Value "tag_" -Initialize -Description "The default prefix for Tag objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.suffix' -Value "" -Initialize -Description "The default suffix for Tag objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string."
 
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.prefix' -Value "parm_" -Initialize -Description "The default prefix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.suffix' -Value "" -Initialize -Description "The default suffix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string."
 
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.prefix' -Value "connection_" -Initialize -Description "The default prefix for connection objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.suffix' -Value "_id" -Initialize -Description "The default suffix for connection objects, used as a fallback value for the Format-Name cmdlet."
 
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.prefix' -Value "trigger_" -Initialize -Description "The default prefix for trigger objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.suffix' -Value "" -Initialize -Description "The default suffix for trigger objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string."
 
#>



<#
    .SYNOPSIS
        Get action from the object, filtered by the type of the action
         
    .DESCRIPTION
        Get actions and all nested actions, filtered by type
         
    .PARAMETER InputObject
        The object that you want to work against
         
        Will by analyzed to see if it has nested actions, and will be recursively traversed to fetch all actions
         
    .PARAMETER Type
        The action type that will be outputted
         
    .EXAMPLE
        PS C:\> Get-ActionsByType -InputObject $obj -Type "Http"
         
        Will traverse the $obj and filter actions to only output the ones of the type Http
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-ActionsByType {
    param (
        [PsCustomObject] $InputObject,

        [string] $Type
    )
    
    if ($InputObject.Type -eq $Type -or $InputObject.Value.Type -eq $Type) {
        $InputObject
    }

    if ($InputObject.Value.actions) {
        foreach ($item in $InputObject.Value.actions.PsObject.Properties) {
            Get-ActionsByType -InputObject $item -Type $Type
        }
    }
    elseif ($InputObject.actions) {
        foreach ($item in $InputObject.actions.PsObject.Properties) {
            Get-ActionsByType -InputObject $item -Type $Type
        }
    }
}


<#
    .SYNOPSIS
        Get the value from an ARM template parameter
         
    .DESCRIPTION
        Gets the current default value from the specified ARM template parameter
         
    .PARAMETER InputObject
        The ARM object that you want to work against
         
        It has to be a object of the type [ArmTemplate] for it to work properly
         
    .PARAMETER Name
        Name of the parameter that you want to work against
         
    .EXAMPLE
        PS C:\> Get-ArmParameterValue -InputObject $armObj -Name "logicAppName"
         
        Gets the default value from the ARM template parameter: logicAppName
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-ArmParameterValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('ParameterName')]
        [Parameter(Mandatory = $true)]
        [string] $Name
    )
    
    if ($InputObject.parameters.$Name) {
        $InputObject.parameters.$Name.defaultValue
    }
}


<#
    .SYNOPSIS
        Get the output file
         
    .DESCRIPTION
        Get the full path of the "latest" file from the workpath of the runbook / extraction process
         
    .PARAMETER Path
        Path to the workpath where the runbook has been persisting files
         
    .EXAMPLE
        PS C:\> Get-ExtractOutput -Path "C:\temp\work_directory"
         
        Returns the full path of the latest written file from the "C:\temp\work_directory" path
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-ExtractOutput {
    [CmdletBinding()]
    param (

        [Alias('WorkPath')]
        [Parameter(Mandatory = $true)]
        [string] $Path
    )
    
    $files = Get-ChildItem -Path $Path -Recurse -File
    $files | Sort-Object -Property LastWriteTime | Select-Object -Last 1 -ExpandProperty FullName
}


<#
    .SYNOPSIS
        Get parameters from ARM template
         
    .DESCRIPTION
        Get parameters from the ARM template
         
        You can include / exclude parameters, so your parameter file only contains the parameters you want to handle at deployment
         
        The default value is promoted as the initial value of the parameter
         
    .PARAMETER Path
        Path to the ARM template that you want to work against
         
    .PARAMETER Exclude
        Instruct the cmdlet to exclude the given set of parameter names
         
        Supports array / list
         
    .PARAMETER Include
        Instruct the cmdlet to include the given set of parameter names
         
        Supports array / list
         
    .PARAMETER AsFile
        Instruct the cmdlet to save a valid ARM template parameter file next to the ARM template file
         
    .PARAMETER BlankValues
        Instructs the cmdlet to blank the values in the parameter file
         
    .PARAMETER CopyMetadata
        Instructs the cmdlet to copy over the metadata property from the original parameter in the ARM template, if present
         
    .EXAMPLE
        PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json"
         
        Gets all parameters from the "TestLogicApp.json" ARM template
        The output is written to the console
         
    .EXAMPLE
        PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -Exclude "logicAppLocation","trigger_Frequency"
         
        Gets all parameters from the "TestLogicApp.json" ARM template
        Will exclude the parameters "logicAppLocation" & "trigger_Frequency" if present
        The output is written to the console
         
    .EXAMPLE
        PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -Include "trigger_Interval","trigger_Frequency"
         
        Gets all parameters from the "TestLogicApp.json" ARM template
        Will only copy over the parameters "trigger_Interval" & "trigger_Frequency" if present
        The output is written to the console
         
    .EXAMPLE
        PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -AsFile
         
        Gets all parameters from the "TestLogicApp.json" ARM template
        The output is written the "C:\temp\work_directory\TestLogicApp.parameters.json" file
         
    .EXAMPLE
        PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -BlankValues
         
        Gets all parameters from the "TestLogicApp.json" ARM template
        Blank all values for each parameter
        The output is written to the console
         
    .EXAMPLE
        PS C:\> Get-PsLaArmParameter -Path "C:\temp\work_directory\TestLogicApp.json" -CopyMetadata
         
        Gets all parameters from the "TestLogicApp.json" ARM template
        Copies over the metadata property from the original parameter, if present
        The output is written to the console
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-PsLaArmParameter {
    [CmdletBinding()]
    param (
        [string] $Path,

        [string[]] $Exclude,

        [string[]] $Include,

        [switch] $AsFile,

        [switch] $BlankValues,

        [switch] $CopyMetadata
    )

    $armObj = [ArmTemplate]$(Get-TaskWorkObject -Path $Path)

    $res = [ordered]@{}
    $res.'$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"
    $res.contentVersion = "1.0.0.0"
    $res.parameters = [ordered]@{}
      
    foreach ($item in $armObj.parameters.PsObject.Properties) {
        if ($item.Name -in $Exclude) { continue }
        
        if ($Include.Count -gt 0) {
            if (-not ($item.Name -in $Include)) { continue }
        }
        
        $valueObj = [ordered]@{}

        if ($BlankValues) {
            switch ($item.Value.Type) {
                "int" {
                    $valueObj.value = 0
                }
                "bool" {
                    $valueObj.value = $false
                }
                "object" {
                    $valueObj.value = $null
                }
                "array" {
                    $valueObj.value = @()
                }
                Default {
                    $valueObj.value = ""
                }
            }
        }
        else {
            $valueObj.value = $item.Value.DefaultValue
        }

        if ($CopyMetadata -and $item.Value.metadata) {
            $valueObj.metadata = $item.Value.metadata
        }

        $res.parameters."$($item.Name)" = $valueObj
    }

    if ($AsFile) {
        $pathLocal = $Path.Replace(".json", ".parameters.json")

        $encoding = New-Object System.Text.UTF8Encoding($true)
        [System.IO.File]::WriteAllLines($pathLocal, $($([PSCustomObject] $res) | ConvertTo-Json -Depth 10), $encoding)

        Get-Item -Path $pathLocal | Select-Object -ExpandProperty FullName
    }
    else {
        $([PSCustomObject] $res) | ConvertTo-Json -Depth 10
    }
}


<#
    .SYNOPSIS
        Get ManagedApi connection objects
         
    .DESCRIPTION
        Get the ApiConnection objects from a resource group
         
        Helps to identity ApiConnection objects that are failed or missing an consent / authentication
 
        Uses the current connected Az.Account session to pull the details from the azure portal
         
    .PARAMETER SubscriptionId
        Id of the subscription that you want to work against, your current Az.Account powershell session either needs to be "connected" to the subscription or at least have permissions to work against the subscription
         
    .PARAMETER ResourceGroup
        Name of the resource group that you want to work against, your current powershell session needs to have permissions to work against the resource group
         
    .PARAMETER FilterError
        Filter the list of ApiConnections to a specific error status
         
        Valid list of options:
        'Unauthorized'
        'Unauthenticated'
         
    .PARAMETER IncludeStatus
        Filter the list of ApiConnections to a specific (overall) status
         
        Valid list of options:
        Connected
        Error
         
    .PARAMETER Detailed
        Instruct the cmdlet to output with the detailed format directly
         
    .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg"
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
         
        Output example:
         
        Name DisplayName OverallStatus Id StatusDetails
        ---- ----------- ------------- -- -------------
        azureblob TestFtpDownload Connected /subscriptions/467c… {"status": "Connect…
        azureeventgrid TestEventGrid Error /subscriptions/467c… {"status": "Error",…
        azurequeues Test Connected /subscriptions/467c… {"status": "Connect…
        office365 MyPersonalCon.. Connected /subscriptions/467c… {"status": "Error",…
        office365-1 MyPersonalCon.. Connected /subscriptions/467c… {"status": "Connect…
         
    .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg" -Detailed
 
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        It will display detailed information about the ApiConnection object
        Output example:
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureblob
        name : azureblob
        DisplayName : TestFtpDownload
        AuthenticatedUser :
        ParameterValues : @{accountName=storageaccount1}
        OverallStatus : Connected
        StatusDetails : {
                            "status": "Connected"
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureeventgrid
        name : azureeventgrid
        DisplayName : TestEventGrid
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues : @{token:TenantId=f312ba7d-b63a-4306-9e97-a623c3f42024; token:grantType=code}
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthorized",
                                    "message": "Failed to refresh access token for service: aadcertificate. Correlation
                                Id=g-3bdeeea8-ae1f-4ac3-82dc-7fee7f16a1e2, UTC TimeStamp=4/8/2021 11:40:36 PM, Error: Failed to
                                acquire token from AAD: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS700082: The refresh
                                token has expired due to inactivity.The token was issued on 2020-09-01T12:13:41.5336734Z and was
                                inactive for 90.00:00:00.\\r\\nTrace ID: b6f03183-79e9-4f81-a640-efcf65c30400\\r\\nCorrelation ID:
                                52b391c3-9c1d-42c7-99f3-a219b7675aee\\r\\nTimestamp: 2021-04-08
                                23:40:36Z\",\"error_codes\":[700082],\"timestamp\":\"2021-04-08 23:40:36Z\",\"trace_id\":\"b6f03183-79e
                                9-4f81-a640-efcf65c30400\",\"correlation_id\":\"52b391c3-9c1d-42c7-99f3-a219b7675aee\",\"error_uri\":\"
                                https://login.windows.net/error?code=700082\"}"
                                }
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/office365
        name : office365
        DisplayName : MyPersonalConnection
        AuthenticatedUser :
        ParameterValues :
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthenticated",
                                    "message": "This connection is not authenticated."
                                }
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            ft.Web/connections/office365-1
        name : office365-1
        DisplayName : MyPersonalConnection2
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues :
        OverallStatus : Connected
        StatusDetails : {
                                "status": "Connected"
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg" -IncludeStatus Error -Detailed
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error
         
        Output example:
         
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureeventgrid
        name : azureeventgrid
        DisplayName : TestEventGrid
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues : @{token:TenantId=f312ba7d-b63a-4306-9e97-a623c3f42024; token:grantType=code}
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthorized",
                                    "message": "Failed to refresh access token for service: aadcertificate. Correlation
                                Id=g-3bdeeea8-ae1f-4ac3-82dc-7fee7f16a1e2, UTC TimeStamp=4/8/2021 11:40:36 PM, Error: Failed to
                                acquire token from AAD: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS700082: The refresh
                                token has expired due to inactivity.The token was issued on 2020-09-01T12:13:41.5336734Z and was
                                inactive for 90.00:00:00.\\r\\nTrace ID: b6f03183-79e9-4f81-a640-efcf65c30400\\r\\nCorrelation ID:
                                52b391c3-9c1d-42c7-99f3-a219b7675aee\\r\\nTimestamp: 2021-04-08
                                23:40:36Z\",\"error_codes\":[700082],\"timestamp\":\"2021-04-08 23:40:36Z\",\"trace_id\":\"b6f03183-79e
                                9-4f81-a640-efcf65c30400\",\"correlation_id\":\"52b391c3-9c1d-42c7-99f3-a219b7675aee\",\"error_uri\":\"
                                https://login.windows.net/error?code=700082\"}"
                                }
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/office365
        name : office365
        DisplayName : MyPersonalConnection
        AuthenticatedUser :
        ParameterValues :
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthenticated",
                                    "message": "This connection is not authenticated."
                                }
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg" -FilterError Unauthenticated -Detailed
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
 
        This is useful in combination with the Invoke-PsLaConsent.AzAccount cmdlet
         
        Output example:
         
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/office365
        name : office365
        DisplayName : MyPersonalConnection
        AuthenticatedUser :
        ParameterValues :
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthenticated",
                                    "message": "This connection is not authenticated."
                                }
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg" -FilterError Unauthorized -Detailed
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
 
        Output example:
         
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureeventgrid
        name : azureeventgrid
        DisplayName : TestEventGrid
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues : @{token:TenantId=f312ba7d-b63a-4306-9e97-a623c3f42024; token:grantType=code}
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthorized",
                                    "message": "Failed to refresh access token for service: aadcertificate. Correlation
                                Id=g-3bdeeea8-ae1f-4ac3-82dc-7fee7f16a1e2, UTC TimeStamp=4/8/2021 11:40:36 PM, Error: Failed to
                                acquire token from AAD: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS700082: The refresh
                                token has expired due to inactivity.The token was issued on 2020-09-01T12:13:41.5336734Z and was
                                inactive for 90.00:00:00.\\r\\nTrace ID: b6f03183-79e9-4f81-a640-efcf65c30400\\r\\nCorrelation ID:
                                52b391c3-9c1d-42c7-99f3-a219b7675aee\\r\\nTimestamp: 2021-04-08
                                23:40:36Z\",\"error_codes\":[700082],\"timestamp\":\"2021-04-08 23:40:36Z\",\"trace_id\":\"b6f03183-79e
                                9-4f81-a640-efcf65c30400\",\"correlation_id\":\"52b391c3-9c1d-42c7-99f3-a219b7675aee\",\"error_uri\":\"
                                https://login.windows.net/error?code=700082\"}"
                                }
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg" -FilterError Unauthenticated | Invoke-PsLaConsent.AzAccount
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
        Will pipe the objects to Invoke-PsLaConsent.AzAccount, which will prompt you to enter a valid user account / credentials
         
        Note: Read more about Invoke-PsLaConsent.AzAccount before running this command, to ensure you understand what it does
 
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-PsLaManagedApiConnection.AzAccount {
    [CmdletBinding(DefaultParameterSetName = "ResourceGroup")]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $SubscriptionId,

        [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")]
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $ResourceGroup,

        [ValidateSet('Unauthorized', 'Unauthenticated')]
        [string] $FilterError,

        [ValidateSet('Connected', 'Error')]
        [string] $IncludeStatus,

        [switch] $Detailed
    )
    
    $parms = @{}
    $parms.ResourceGroupName = $ResourceGroup
    $parms.ApiVersion = "2018-07-01-preview"
    $parms.ResourceProviderName = "Microsoft.Web"
    $parms.ResourceType = "connections"

    if ($SubscriptionId) {
        $parms.SubscriptionId = $SubscriptionId
    }

    $res = Invoke-AzRestMethod -Method Get @parms

    $cons = $res.Content | ConvertFrom-Json -Depth 20 | Select-Object -ExpandProperty Value

    $temp = $cons | Select-PSFObject -TypeName PsLaExtractor.ManagedConnection -Property id, Name,
    @{Label = "DisplayName"; Expression = { $_.properties.DisplayName } },
    @{Label = "OverallStatus"; Expression = { $_.properties.overallStatus } },
    @{Label = "AuthenticatedUser"; Expression = { $_.properties.authenticatedUser } },
    @{Label = "ParameterValues"; Expression = { $_.properties.parameterValues } },
    @{Label = "StatusDetails"; Expression = {
            if ($Detailed) {
                $_.properties.Statuses | ConvertTo-Json -Depth 4
            }
            else {
                $($_.properties.Statuses | ConvertTo-Json -Depth 4).Replace("`r`n", "").Replace(" ", "")
            }
        }
    }

    if ($FilterError -or $IncludeStatus) {
        $filtered = @(foreach ($item in $temp) {
                $details = $item.StatusDetails | ConvertFrom-Json -Depth 10

                if ($FilterError -and $details.error.code -ne $FilterError) {
                    continue
                }

                if ($IncludeStatus -and $item.OverallStatus -ne $IncludeStatus) {
                    continue
                }

                $item
            }
        )

        $temp = $filtered
    }

    if ($Detailed) {
        $temp | Select-PSFObject -Property * -TypeName PsLaExtractor.ManagedConnection.Detailed
    }
    else {
        $temp
    }
}


<#
    .SYNOPSIS
        Get ManagedApi connection objects
         
    .DESCRIPTION
        Get the ApiConnection objects from a resource group
         
        Helps to identity ApiConnection objects that are failed or missing an consent / authentication
 
        Uses the current connected az cli session to pull the details from the azure portal
         
    .PARAMETER SubscriptionId
        Id of the subscription that you want to work against, your current az cli session either needs to be "connected" to the subscription or at least have permissions to work against the subscription
         
    .PARAMETER ResourceGroup
        Name of the resource group that you want to work against, your current az cli session needs to have permissions to work against the resource group
         
    .PARAMETER FilterError
        Filter the list of ApiConnections to a specific error status
         
        Valid list of options:
        'Unauthorized'
        'Unauthenticated'
         
    .PARAMETER IncludeStatus
        Filter the list of ApiConnections to a specific (overall) status
         
        Valid list of options:
        Connected
        Error
         
    .PARAMETER Detailed
        Instruct the cmdlet to output with the detailed format directly
         
    .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg"
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
         
        Output example:
         
        Name DisplayName OverallStatus Id StatusDetails
        ---- ----------- ------------- -- -------------
        azureblob TestFtpDownload Connected /subscriptions/467c… {"status": "Connect…
        azureeventgrid TestEventGrid Error /subscriptions/467c… {"status": "Error",…
        azurequeues Test Connected /subscriptions/467c… {"status": "Connect…
        office365 MyPersonalCon.. Connected /subscriptions/467c… {"status": "Error",…
        office365-1 MyPersonalCon.. Connected /subscriptions/467c… {"status": "Connect…
         
    .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg" -Detailed
 
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        It will display detailed information about the ApiConnection object
        Output example:
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureblob
        name : azureblob
        DisplayName : TestFtpDownload
        AuthenticatedUser :
        ParameterValues : @{accountName=storageaccount1}
        OverallStatus : Connected
        StatusDetails : {
                            "status": "Connected"
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureeventgrid
        name : azureeventgrid
        DisplayName : TestEventGrid
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues : @{token:TenantId=f312ba7d-b63a-4306-9e97-a623c3f42024; token:grantType=code}
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthorized",
                                    "message": "Failed to refresh access token for service: aadcertificate. Correlation
                                Id=g-3bdeeea8-ae1f-4ac3-82dc-7fee7f16a1e2, UTC TimeStamp=4/8/2021 11:40:36 PM, Error: Failed to
                                acquire token from AAD: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS700082: The refresh
                                token has expired due to inactivity.The token was issued on 2020-09-01T12:13:41.5336734Z and was
                                inactive for 90.00:00:00.\\r\\nTrace ID: b6f03183-79e9-4f81-a640-efcf65c30400\\r\\nCorrelation ID:
                                52b391c3-9c1d-42c7-99f3-a219b7675aee\\r\\nTimestamp: 2021-04-08
                                23:40:36Z\",\"error_codes\":[700082],\"timestamp\":\"2021-04-08 23:40:36Z\",\"trace_id\":\"b6f03183-79e
                                9-4f81-a640-efcf65c30400\",\"correlation_id\":\"52b391c3-9c1d-42c7-99f3-a219b7675aee\",\"error_uri\":\"
                                https://login.windows.net/error?code=700082\"}"
                                }
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/office365
        name : office365
        DisplayName : MyPersonalConnection
        AuthenticatedUser :
        ParameterValues :
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthenticated",
                                    "message": "This connection is not authenticated."
                                }
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            ft.Web/connections/office365-1
        name : office365-1
        DisplayName : MyPersonalConnection2
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues :
        OverallStatus : Connected
        StatusDetails : {
                                "status": "Connected"
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg" -IncludeStatus Error -Detailed
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error
         
        Output example:
         
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureeventgrid
        name : azureeventgrid
        DisplayName : TestEventGrid
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues : @{token:TenantId=f312ba7d-b63a-4306-9e97-a623c3f42024; token:grantType=code}
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthorized",
                                    "message": "Failed to refresh access token for service: aadcertificate. Correlation
                                Id=g-3bdeeea8-ae1f-4ac3-82dc-7fee7f16a1e2, UTC TimeStamp=4/8/2021 11:40:36 PM, Error: Failed to
                                acquire token from AAD: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS700082: The refresh
                                token has expired due to inactivity.The token was issued on 2020-09-01T12:13:41.5336734Z and was
                                inactive for 90.00:00:00.\\r\\nTrace ID: b6f03183-79e9-4f81-a640-efcf65c30400\\r\\nCorrelation ID:
                                52b391c3-9c1d-42c7-99f3-a219b7675aee\\r\\nTimestamp: 2021-04-08
                                23:40:36Z\",\"error_codes\":[700082],\"timestamp\":\"2021-04-08 23:40:36Z\",\"trace_id\":\"b6f03183-79e
                                9-4f81-a640-efcf65c30400\",\"correlation_id\":\"52b391c3-9c1d-42c7-99f3-a219b7675aee\",\"error_uri\":\"
                                https://login.windows.net/error?code=700082\"}"
                                }
                            }
 
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/office365
        name : office365
        DisplayName : MyPersonalConnection
        AuthenticatedUser :
        ParameterValues :
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthenticated",
                                    "message": "This connection is not authenticated."
                                }
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg" -FilterError Unauthenticated -Detailed
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
 
        This is useful in combination with the Invoke-PsLaConsent.AzCli cmdlet
         
        Output example:
         
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/office365
        name : office365
        DisplayName : MyPersonalConnection
        AuthenticatedUser :
        ParameterValues :
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthenticated",
                                    "message": "This connection is not authenticated."
                                }
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg" -FilterError Unauthorized -Detailed
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
 
        Output example:
         
        id : /subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/resourceGroups/TestRg/providers/Microsoft.W
                            eb/connections/azureeventgrid
        name : azureeventgrid
        DisplayName : TestEventGrid
        AuthenticatedUser : @{name=sarah@contoso.com}
        ParameterValues : @{token:TenantId=f312ba7d-b63a-4306-9e97-a623c3f42024; token:grantType=code}
        OverallStatus : Error
        StatusDetails : {
                                "status": "Error",
                                "target": "token",
                                "error": {
                                    "code": "Unauthorized",
                                    "message": "Failed to refresh access token for service: aadcertificate. Correlation
                                Id=g-3bdeeea8-ae1f-4ac3-82dc-7fee7f16a1e2, UTC TimeStamp=4/8/2021 11:40:36 PM, Error: Failed to
                                acquire token from AAD: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS700082: The refresh
                                token has expired due to inactivity.The token was issued on 2020-09-01T12:13:41.5336734Z and was
                                inactive for 90.00:00:00.\\r\\nTrace ID: b6f03183-79e9-4f81-a640-efcf65c30400\\r\\nCorrelation ID:
                                52b391c3-9c1d-42c7-99f3-a219b7675aee\\r\\nTimestamp: 2021-04-08
                                23:40:36Z\",\"error_codes\":[700082],\"timestamp\":\"2021-04-08 23:40:36Z\",\"trace_id\":\"b6f03183-79e
                                9-4f81-a640-efcf65c30400\",\"correlation_id\":\"52b391c3-9c1d-42c7-99f3-a219b7675aee\",\"error_uri\":\"
                                https://login.windows.net/error?code=700082\"}"
                                }
                            }
 
        .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg" -FilterError Unauthenticated | Invoke-PsLaConsent.AzCli
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
        Will pipe the objects to Invoke-PsLaConsent.AzCli, which will prompt you to enter a valid user account / credentials
         
        Note: Read more about Invoke-PsLaConsent.AzCli before running this command, to ensure you understand what it does
 
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-PsLaManagedApiConnection.AzCli {
    [CmdletBinding(DefaultParameterSetName = "ResourceGroup")]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $SubscriptionId,

        [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")]
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $ResourceGroup,

        [ValidateSet('Unauthorized', 'Unauthenticated')]
        [string] $FilterError,

        [ValidateSet('Connected', 'Error')]
        [string] $IncludeStatus,

        [switch] $Detailed
    )
    
    $uri = "/subscriptions/{subscriptionId}/resourceGroups/$ResourceGroup/providers/Microsoft.Web/connections?api-version=2018-07-01-preview"

    if ($SubscriptionId) {
        $uri = $uri.Replace("{subscriptionId}", $SubscriptionId)
    }

    $res = az rest --url "$uri" | ConvertFrom-Json -Depth 10
    
    if ($null -eq $res) {
        #TODO! We need to throw an error
        Throw
    }

    $cons = $res | Select-Object -ExpandProperty Value

    $temp = $cons | Select-PSFObject -TypeName PsLaExtractor.ManagedConnection -Property id, Name,
    @{Label = "DisplayName"; Expression = { $_.properties.DisplayName } },
    @{Label = "OverallStatus"; Expression = { $_.properties.overallStatus } },
    @{Label = "AuthenticatedUser"; Expression = { $_.properties.authenticatedUser } },
    @{Label = "ParameterValues"; Expression = { $_.properties.parameterValues } },
    @{Label = "StatusDetails"; Expression = {
            if ($Detailed) {
                $_.properties.Statuses | ConvertTo-Json -Depth 4
            }
            else {
                $($_.properties.Statuses | ConvertTo-Json -Depth 4).Replace("`r`n", "").Replace(" ", "")
            }
        }
    }

    if ($FilterError -or $IncludeStatus) {
        $filtered = @(foreach ($item in $temp) {
                $details = $item.StatusDetails | ConvertFrom-Json -Depth 10

                if ($FilterError -and $details.error.code -ne $FilterError) {
                    continue
                }

                if ($IncludeStatus -and $item.OverallStatus -ne $IncludeStatus) {
                    continue
                }

                $item
            }
        )

        $temp = $filtered
    }

    if ($Detailed) {
        $temp | Select-PSFObject -Property * -TypeName PsLaExtractor.ManagedConnection.Detailed
    }
    else {
        $temp
    }
}


<#
    .SYNOPSIS
        Get tasks that are part of the module
         
    .DESCRIPTION
        List all avaiable tasks that are part of the module, to be used for exporting, sanitizing and converting a LogicApp into a deployable ARM template
         
    .PARAMETER Category
        Used to filter the number of tasks down to only being part of the category that you are looking for
         
    .PARAMETER Detailed
        Instruct the cmdlet to output the details about the tasks in a more detailed fashion, makes it easier to read the descriptions for each task
         
    .EXAMPLE
        PS C:\> Get-PsLaTask
         
        List all available tasks
         
        Output example:
         
        Category Name Description
        -------- ---- -----------
        Arm Set-Arm.Connections.ManagedApis.AsParameter Loops all $connections childs…
        Arm Set-Arm.Connections.ManagedApis.AsVariable Loops all $connections childs…
        Arm Set-Arm.Connections.ManagedApis.IdFormatted Loops all $connections childs…
        Arm Set-Arm.IntegrationAccount.IdFormatted.Simple.AsParameter.AsVariable Creates an Arm variable: integrationAccount…
         
    .EXAMPLE
        PS C:\> Get-PsLaTask -Category Converter
         
        List all available tasks, which are in the Converter category
         
        Output example:
         
        Category Name Description
        -------- ---- -----------
        Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json
        Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp json,…
         
    .EXAMPLE
        PS C:\> Get-PsLaTask -Detailed
         
        List all available tasks, and outputs it in the detailed view
         
        Output example:
         
        Category : Arm
        Name : Set-Arm.Connections.ManagedApis.AsParameter
        Description : Loops all $connections childs
        -Creates an Arm parameter, with prefix & suffix
        --Sets the default value to the original name, extracted from connectionId property
        -Sets the connectionId to: [resourceId('Microsoft.Web/connections', parameters('XYZ'))]
        -Sets the connectionName to: [parameters('XYZ')]
         
        Category : Arm
        Name : Set-Arm.Connections.ManagedApis.AsVariable
        Description : Loops all $connections childs
        -Creates an Arm variable, with prefix & suffix
        --Sets the value to the original name, extracted from connectionId property
        -Sets the connectionId to: [resourceId('Microsoft.Web/connections', variables('XYZ'))]
        -Sets the connectionName to: [variables('XYZ')]
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-PsLaTask {
    [CmdletBinding()]
    param (
        [ValidateSet('Arm', 'Converter', 'Exporter', 'Raw')]
        [string] $Category,

        [switch] $Detailed
    )

    $res = Get-PsLaTaskByPath -Path "$($MyInvocation.MyCommand.Module.ModuleBase)\internal\tasks" | Where-Object Category -like "*$Category*" | Select-Object -Property * -ExcludeProperty Path

    if ($Detailed) {
        $res | Format-List
    }
    else {
        $res
    }
}


<#
    .SYNOPSIS
        Get tasks that are references from a file
         
    .DESCRIPTION
        Get tasks that are references from a runbook file, to make it easier to understand what a given runbook file is doing
         
    .PARAMETER File
        Path to the runbook file, that you want to analyze
         
    .PARAMETER Category
        Used to filter the number of tasks down to only being part of the category that you are looking for
         
    .PARAMETER Detailed
        Instruct the cmdlet to output the details about the tasks in a more detailed fashion, makes it easier to read the descriptions for each task
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1"
         
        List all tasks that are referenced from the file
        The file needs to be a valid PSake runbook file
         
        Output example:
         
        Category Name Description
        -------- ---- -----------
        Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json
        Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp json,…
        Exporter Export-LogicApp.AzCli Exports the raw version of the Logic App from the Azure Portal
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -Category Converter
         
        List all tasks that are referenced from the file, which are in the Converter category
        The file needs to be a valid PSake runbook file
         
        Output example:
         
        Category Name Description
        -------- ---- -----------
        Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json
        Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp json,…
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -Detailed
         
        List all tasks that are referenced from the file, and outputs it in the detailed view
        The file needs to be a valid PSake runbook file
         
        Output example:
         
        Category : Converter
        Name : ConvertTo-Arm
        Description : Converts the LogicApp json structure into a valid ARM template json
         
        Category : Converter
        Name : ConvertTo-Raw
        Description : Converts the raw LogicApp json structure into the a valid LogicApp json,
        this will remove different properties that are not needed
         
        Category : Exporter
        Name : Export-LogicApp.AzCli
        Description : Exports the raw version of the Logic App from the Azure Portal
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-PsLaTaskByFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    param (
        [Alias('Runbook')]
        [Parameter(Mandatory = $true)]
        [string] $File,

        [ValidateSet('Arm', 'Converter', 'Exporter', 'LogicApp')]
        [string] $Category,

        [switch] $Detailed
    )
    
    # We are playing around with the internal / global psake object
    $psake.context = New-Object System.Collections.Stack

    $res = @(Get-PSakeScriptTasks -Runbook $File | Where-Object Name -ne "Default" | Select-Object -Property @{Label = "Category"; Expression = { $_.Alias.Split(".")[0] } }, Name, Description)

    $temp = $res | Where-Object Category -like "*$Category*" | Sort-Object Category, Name
        
    if ($Detailed) {
        $temp | Format-List
    }
    else {
        $temp
    }
}


<#
    .SYNOPSIS
        Get tasks based on files from a directory
         
    .DESCRIPTION
        Get tasks from individual files, that are located in a directory
         
    .PARAMETER Path
        Path to the directory where there are valid PSake tasks saved as ps1 files
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskByPath -Path c:\temp\tasks
         
        List all available tasks, based on the files in the directory
        All files has to be valid PSake files saved as ps1 files
         
        Output example:
         
        Category : Arm
        Name : Set-Arm.Connections.ManagedApis.AsParameter
        Description : Loops all $connections childs
        -Creates an Arm parameter, with prefix & suffix
        --Sets the default value to the original name, extracted from connectionId property
        -Sets the connectionId to: [resourceId('Microsoft.Web/connections', parameters('XYZ'))]
        -Sets the connectionName to: [parameters('XYZ')]
        Path : c:\temp\tasks\Set-Arm.Connections.ManagedApis.AsParameter.task.ps1
        File : Set-Arm.Connections.ManagedApis.AsParameter.task.ps1
         
        Category : Arm
        Name : Set-Arm.Connections.ManagedApis.AsVariable
        Description : Loops all $connections childs
        -Creates an Arm variable, with prefix & suffix
        --Sets the value to the original name, extracted from connectionId property
        -Sets the connectionId to: [resourceId('Microsoft.Web/connections', variables('XYZ'))]
        -Sets the connectionName to: [variables('XYZ')]
        Path : c:\temp\tasks\Set-Arm.Connections.ManagedApis.AsVariable.task.ps1
        File : Set-Arm.Connections.ManagedApis.AsVariable.task.ps1
         
    .NOTES
        General notes
#>

function Get-PsLaTaskByPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $Path
    )
    
    $files = Get-ChildItem -Path "$Path\*.ps1"

    $res = New-Object System.Collections.Generic.List[System.Object]

    # We are playing around with the internal / global psake object
    $psake.context = New-Object System.Collections.Stack
    $psake.context.push(
        @{
            "tasks"   = @{}
            "aliases" = @{}
        }
    )

    foreach ($item in $files) {
        # We are playing around with the internal / global psake object
        $psake.context = New-Object System.Collections.Stack
        $psake.context.push(
            @{
                "tasks"   = @{}
                "aliases" = @{}
            }
        )
        
        . $item.FullName

        foreach ($task in $psake.context.tasks) {
            foreach ($value in $task.Values) {
                $res.Add([PsCustomObject][ordered]@{
                        Category    = $value.Alias.Split(".")[0]
                        Name        = $value.Name
                        Description = $value.Description
                        Path        = $item.FullName
                        File        = $item.Name
                    })
            }
        }
    }

    # We are playing around with the internal / global psake object
    $psake.context = New-Object System.Collections.Stack

    $res.ToArray() | Where-Object Name -ne "Default" | Sort-Object Category, Name
}


<#
    .SYNOPSIS
        Get tasks that are references from a file, with the execution order
         
    .DESCRIPTION
        Get tasks that are references from a runbook file, to make it easier to understand what a given runbook file is doing
         
        Includes the execution order of the tasks, to visualize the tasks sequence
         
    .PARAMETER File
        Path to the runbook file, that you want to analyze
         
    .PARAMETER Detailed
        Instruct the cmdlet to output the details about the tasks in a more detailed fashion, makes it easier to read the descriptions for each task
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskOrderByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1"
         
        List all tasks that are referenced from the file
        The file needs to be a valid PSake runbook file
         
        Output example:
         
        ExecutionOrder Category Name Description
        -------------- -------- ---- -----------
        1 Exporter Export-LogicApp.AzCli Exports the raw version of the Logic App from the Azure Portal
        2 Converter ConvertTo-Raw Converts the raw LogicApp json structure into the a valid LogicApp j…
        3 Converter ConvertTo-Arm Converts the LogicApp json structure into a valid ARM template json
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskOrderByFile -File "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -Detailed
         
        List all tasks that are referenced from the file, and outputs it in the detailed view
        The file needs to be a valid PSake runbook file
         
        Output example:
         
        ExecutionOrder : 1
        Category : Exporter
        Name : Export-LogicApp.AzCli
        Description : Exports the raw version of the Logic App from the Azure Portal
         
        ExecutionOrder : 2
        Category : Converter
        Name : ConvertTo-Raw
        Description : Converts the raw LogicApp json structure into the a valid LogicApp json,
        this will remove different properties that are not needed
         
        ExecutionOrder : 3
        Category : Converter
        Name : ConvertTo-Arm
        Description : Converts the LogicApp json structure into a valid ARM template json
         
    .NOTES
        General notes
#>

function Get-PsLaTaskOrderByFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
    [CmdletBinding()]
    param (
        [Alias('Runbook')]
        [Parameter(Mandatory = $true)]
        [string] $File,

        [switch] $Detailed
    )
    
    # We are playing around with the internal / global psake object
    $psake.context = New-Object System.Collections.Stack

    $default = Get-PSakeScriptTasks -Runbook $File | Where-Object Name -eq "Default" | Select-Object -First 1

    $tasks = @(Get-PSakeScriptTasks -Runbook $File | Where-Object Name -ne "Default" | Select-Object -Property @{Label = "Category"; Expression = { $_.Alias.Split(".")[0] } }, Name, Description)

    $res = @(for ($i = 0; $i -lt $default.DependsOn.Count; $i++) {
            $tasks | Where-Object Name -eq $($default.DependsOn[$i]) | Select-Object -Property @{Label = "ExecutionOrder"; Expression = { $i + 1 } }, *
        })

    if ($Detailed) {
        $res | Format-List
    }
    else {
        $res
    }
}


<#
    .SYNOPSIS
        Short description
         
    .DESCRIPTION
        Long description
         
    .PARAMETER Category
        Instruct the cmdlet which template type you want to have outputted
         
    .PARAMETER OutputPath
        Path to were the Task template file will be persisted
         
        The path has to be a directory
         
        The file will be named: _set-XYA.Template.ps1
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskTemplate -Category "Arm"
         
        Outputs the task template of the type Arm to the console
         
    .EXAMPLE
        PS C:\> Get-PsLaTaskTemplate -Category "Arm" -OutputPath "C:\temp\work_directory"
         
        Outputs the task template of the type Arm
        Persists the file into the "C:\temp\work_directory" directory
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-PsLaTaskTemplate {
    [CmdletBinding()]
    param (
        [ValidateSet('Arm', 'Converter', 'Raw')]
        [string] $Category,

        [string] $OutputPath
    )

    if ($OutputPath) {
        Copy-Item -Path "$($script:ModuleRoot)\internal\tasks\_Set-$Category.Template.tmp" -Destination "$OutputPath\_Set-$Category.Template.ps1" -PassThru -Force | Select-Object -ExpandProperty FullName
    }
    else {
        Get-Content -Path "$($script:ModuleRoot)\internal\tasks\_Set-$Category.Template.tmp" -Raw
    }
}


<#
    .SYNOPSIS
        Get the object that the task has to work against
         
    .DESCRIPTION
        Gets the object from the "previous" task, based on the persisted path and loads it into memory using ConvertFrom-Json
         
    .PARAMETER Path
        Path to the file that you want the task to work against
         
    .EXAMPLE
        PS C:\> Get-TaskWorkObject
         
        Returns the object that is stored at the location passed in the Path parameter
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-TaskWorkObject {
    [CmdletBinding()]
    param (
        [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext)

    )

    Get-Content -Path $Path -Raw | ConvertFrom-Json
}


<#
    .SYNOPSIS
        Get the object that the task has to work against, as a raw string
         
    .DESCRIPTION
        Gets the object from the "previous" task, based on the persisted path and loads it into memory using Get-Content
         
    .PARAMETER Path
        Path to the file that you want the task to work against
         
    .EXAMPLE
        PS C:\> Get-TaskWorkObject
         
        Returns the object that is stored at the location passed in the Path parameter
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Get-TaskWorkRaw {
    [CmdletBinding()]
    param (
        [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext)
    )

    Get-Content -Path $Path -Raw
}


<#
    .SYNOPSIS
        Start the consent flow for an ApiConnection object
         
    .DESCRIPTION
        Some of the ApiConnection objects needs an user account to consent / authenticate, before it works
         
        This cmdlet helps starting, running and completing the consent flow an ApiConnection object
         
        Uses the current connected Az.Account session to pull the details from the azure portal
         
    .PARAMETER Id
        The (resource) id of the ApiConnection object that you want to work against, your current Az.Account powershell session either needs to be "connected" to the subscription/resource group or at least have permissions to work against the subscription/resource group, where the ApiConnection object is located
         
    .EXAMPLE
        PS C:\> Invoke-PsLaConsent.AzAccount -Id "/subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/providers/Microsoft.Web/locations/westeurope/managedApis/servicebus"
         
        This will start the consent flow for the ApiConnection object
        It will prompt the user to fill in an account / credential
        It will confirm the consent directly to the ApiConnection object
         
    .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzAccount -ResourceGroup "TestRg" -FilterError Unauthenticated | Invoke-PsLaConsent.AzAccount
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
        Will pipe the objects to Invoke-PsLaConsent.AzAccount
        This will start the consent flow for the ApiConnection object
        It will prompt the user to fill in an account / credential
        It will confirm the consent directly to the ApiConnection object
         
    .NOTES
        This is highly inspired by the previous work of other smart people:
        https://github.com/logicappsio/LogicAppConnectionAuth/blob/master/LogicAppConnectionAuth.ps1
        https://github.com/OfficeDev/microsoft-teams-apps-requestateam/blob/master/Deployment/Scripts/deploy.ps1
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Invoke-PsLaConsent.AzAccount {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("ResourceId")]
        [string] $Id
    )
    
    begin {
        $listParms = @{
            "parameters" = , @{
                "parameterName" = "token";
                "redirectUrl"   = "http://localhost"
            }
        }
    }

    process {
        if (-not ($Id -match "/Microsoft.Web/connections/(.*)")) {
            $messageString = "The resource id supplied didn't match the expected structure. Please make sure the resource id is a <c='em'>Microsoft.Web/connections</c>."
            Write-PSFMessage -Level Host -Message $messageString
            Stop-PSFFunction -Message "Stopping because the resource id did match." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        Write-PSFMessage -Level Host -Message "You will be prompted to consent the ApiConnection object: <c='em'>$($Matches[1])</c>. You might need to supplied credentials that <c='em'>are different</c> from your personal account / credentials."

        # Get the links needed for consent
        $consentResponse = Invoke-AzResourceAction -Action "listConsentLinks" -ResourceId $Id -Parameters $listParms -Force

        # Show sign-in prompt window and grab the code after authentication
        $resUrl = Show-OAuthConsentWindow -URL $consentResponse.Value.Link

        if ([System.String]::IsNullOrEmpty($resUrl)) {
            $messageString = "It seems that either the consent failed or you exited the consent flow before it completed. Please make sure to complete the consent flow all the way through for this to work."
            Write-PSFMessage -Level Host -Message $messageString
            Stop-PSFFunction -Message "Stopping because result from the consent flow was empty." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        $regex = '(code=)(.*)$'
        $code = ($resUrl | Select-string -pattern $regex).Matches[0].Groups[2].Value

        if ($code) {
            $confirmParms = @{ }
            $confirmParms.Add("code", $code)
        
            # NOTE: errors ignored as this appears to error due to a null response
            Invoke-AzResourceAction -Action "confirmConsentCode" -ResourceId $Id -Parameters $confirmParms -Force -ErrorAction Ignore
        }
    }
}


<#
    .SYNOPSIS
        Start the consent flow for an ApiConnection object
         
    .DESCRIPTION
        Some of the ApiConnection objects needs an user account to consent / authenticate, before it works
         
        This cmdlet helps starting, running and completing the consent flow an ApiConnection object
         
        Uses the current connected az cli session to pull the details from the azure portal
         
    .PARAMETER Id
        The (resource) id of the ApiConnection object that you want to work against, your current az cli session either needs to be "connected" to the subscription/resource group or at least have permissions to work against the subscription/resource group, where the ApiConnection object is located
         
    .EXAMPLE
        PS C:\> Invoke-PsLaConsent.AzCli -Id "/subscriptions/b466443d-6eac-4513-a7f0-3579502929f00/providers/Microsoft.Web/locations/westeurope/managedApis/servicebus"
         
        This will start the consent flow for the ApiConnection object
        It will prompt the user to fill in an account / credential
        It will confirm the consent directly to the ApiConnection object
         
    .EXAMPLE
        PS C:\> Get-PsLaManagedApiConnection.AzCli -ResourceGroup "TestRg" -FilterError Unauthenticated | Invoke-PsLaConsent.AzCli
         
        This will fetch all ApiConnection objects from the "TestRg" Resource Group
        Filters the list to show only the ones with error of the type Unauthenticated
        Will pipe the objects to Invoke-PsLaConsent.AzCli
        This will start the consent flow for the ApiConnection object
        It will prompt the user to fill in an account / credential
        It will confirm the consent directly to the ApiConnection object
         
    .NOTES
        This is highly inspired by the previous work of other smart people:
        https://github.com/logicappsio/LogicAppConnectionAuth/blob/master/LogicAppConnectionAuth.ps1
        https://github.com/OfficeDev/microsoft-teams-apps-requestateam/blob/master/Deployment/Scripts/deploy.ps1
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Invoke-PsLaConsent.AzCli {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("ResourceId")]
        [string] $Id
    )
    
    begin {
        #! Hack to make the json a single line / oneline
        # https://github.com/Azure/azure-cli/issues/13056
        $payloadList = $([PSCustomObject]@{
                "parameters" = , @{
                    "parameterName" = "token";
                    "redirectUrl"   = "http://localhost"
                }
            } | ConvertTo-Json -Depth 10).Replace("`r`n", "").Replace('"', '\"')
    }

    process {
        if (-not ($Id -match "/Microsoft.Web/connections/(.*)")) {
            $messageString = "The resource id supplied didn't match the expected structure. Please make sure the resource id is a <c='em'>Microsoft.Web/connections</c>."
            Write-PSFMessage -Level Host -Message $messageString
            Stop-PSFFunction -Message "Stopping because the resource id did match." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        Write-PSFMessage -Level Host -Message "You will be prompted to consent the ApiConnection object: <c='em'>$($Matches[1])</c>. You <c='em'>might</c> need to supplied credentials that <c='em'>are different</c> from your personal account / credentials."
        
        $uriList = "$Id/listConsentLinks?api-version=2018-07-01-preview"

        # Get the links needed for consent
        $consentResponse = az rest --method POST --url "$uriList" --body $payloadList --query "value | [0]"  | ConvertFrom-Json -Depth 10

        # Show sign-in prompt window and grab the code after authentication
        $resUrl = Show-OAuthConsentWindow -URL $consentResponse.Link

        if ([System.String]::IsNullOrEmpty($resUrl)) {
            $messageString = "It seems that either the consent failed or you exited the consent flow before it completed. Please make sure to <c='em'>complete</c> the consent flow all the way through for this to work."
            Write-PSFMessage -Level Host -Message $messageString
            Stop-PSFFunction -Message "Stopping because result from the consent flow was empty." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        $regex = '(code=)(.*)$'
        $code = ($resUrl | Select-string -pattern $regex).Matches[0].Groups[2].Value

        if ($code) {
            $payloadConfirm = $([PSCustomObject]@{code = $code } | ConvertTo-Json -Depth 10).Replace("`r`n", "").Replace('"', '\"')
        
            $uriConfirm = "$Id/confirmConsentCode?api-version=2018-07-01-preview"

            az rest --method POST --url "$uriConfirm" --body $payloadConfirm | ConvertFrom-Json -Depth 10

            if ($LastExitCode -ne 0) {
                $messageString = "There was an error while posting the <c='em'>confirmConsentCode</c> on the ApiConnection object. Try again and make sure that you used a <c='em'>user account / credential</c>."
                Write-PSFMessage -Level Host -Message $messageString
                Stop-PSFFunction -Message "Stopping because the posting of the confirmConsentCode failed." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
                return
            }
        }
    }
}


<#
    .SYNOPSIS
        Execute the extractor process of the LogicApp
         
    .DESCRIPTION
        Execute all the tasks that have been defined in the runbook file, and get an ARM template as output
         
        Depending on the initial extractor task that you are using, your powershell / az cli session needs to be signed in
         
        Your runbook file can contain the tasks available from the module, but also your own custom tasks, that you want to have executed as part of the process
         
    .PARAMETER Runbook
        Path to the PSake valid runbook file that you want to have executed while exporting, sanitizing and converting a LogicApp into a deployable ARM template
         
    .PARAMETER SubscriptionId
        Id of the subscription that you want to work against, your current powershell / az cli session either needs to be "connected" to the subscription or at least have permissions to work against the subscription
         
    .PARAMETER ResourceGroup
        Name of the resource group that you want to work against, your current powershell / az cli session needs to have permissions to work against the resource group
         
    .PARAMETER Name
        Name of the logic app, that you want to work against
         
    .PARAMETER Task
        List of task that you want to have executed, based on the runbook file that you pass
         
        This allows you to only run a small subset of all the tasks that you have defined inside your runbook
         
        Helpful when troubleshooting and trying to identify the best execution order of all the tasks
         
    .PARAMETER WorkPath
        Path to were the tasks will persist their outputs
         
        Each task will save a file into a unique folder, containing the formatted output from its operation
         
        You could risk that secrets or credentials are being stored on your disk, if they in some way are stored as clear text inside the logic app
         
        The default valus is the current users TempPath, where it creates a "\PsLogicAppExtractor\GUID\" directory for each invoke
         
    .PARAMETER OutputPath
        Path to were the ARM template file will be persisted
         
        The path has to be a directory
         
        The file will be named as the Logic App is named
         
    .PARAMETER KeepFiles
        Instruct the cmdlet to keep all the files, across all tasks
         
        This enables troubleshooting and comparison of input vs output, per task, as each task has an input file and the result of the work persisted in the same directory
         
    .EXAMPLE
        PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -ResourceGroup "TestRg" -Name TestLogicApp
         
        Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template
        The file needs to be a valid PSake runbook file
         
    .EXAMPLE
        PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" -Name "TestLogicApp"
         
        Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template
        The file needs to be a valid PSake runbook file
         
    .EXAMPLE
        PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1"
         
        Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template
        The file needs to be a valid PSake runbook file
        The runbook file needs to have populated the Properties object, with the minimum: ResourceGroup and SubscriptionId
         
    .EXAMPLE
        PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -ResourceGroup "TestRg" -Name TestLogicApp -WorkPath "C:\temp\work_directory"
         
        Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template
        The file needs to be a valid PSake runbook file
        Will output all tasks files into the "C:\temp\work_directory" location
         
    .EXAMPLE
        PS C:\> Invoke-PsLaExtractor -Runbook "C:\temp\LogicApp.ExportOnly.psakefile.ps1" -ResourceGroup "TestRg" -Name TestLogicApp -KeepFiles
         
        Invokes the different tasks inside the runbook file, to export the TestLogicApp as an ARM template
        The file needs to be a valid PSake runbook file
        All files that the different tasks has created, are keept, for the user to analyze them
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Invoke-PsLaExtractor {
    [CmdletBinding(DefaultParameterSetName = "NameOnly")]
    param (
        [Alias('File')]
        [Parameter(Mandatory = $true, ParameterSetName = "NameOnly")]
        [Parameter(Mandatory = $true, ParameterSetName = "PreppedFile")]
        [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")]
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $Runbook,

        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $SubscriptionId,

        [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")]
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $ResourceGroup,

        [Parameter(Mandatory = $true, ParameterSetName = "NameOnly")]
        [Parameter(Mandatory = $true, ParameterSetName = "ResourceGroup")]
        [Parameter(Mandatory = $true, ParameterSetName = "Subscription")]
        [string] $Name,

        [string[]] $Task,

        [string] $WorkPath = "$([System.IO.Path]::GetTempPath())PsLogicAppExtractor\$([System.Guid]::NewGuid().Guid)",

        [string] $OutputPath,

        [switch] $KeepFiles
    )

    if (-not ($WorkPath -like "*$([System.IO.Path]::GetTempPath())*")) {
        if ($WorkPath -NotMatch '(?im)[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?') {
            $WorkPath = "$WorkPath\$([System.Guid]::NewGuid().Guid)"
        }
    }

    #The task counter needs to be reset prior running
    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskCounter -Value 0

    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskInputNext -Value ""
    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskOutputFile -Value ""
    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskPath -Value ""

    #Make sure the work path is created and available
    New-Item -Path $WorkPath -ItemType Directory -Force -ErrorAction Ignore > $null

    $parms = @{}
    $parms.buildFile = $Runbook
    $parms.nologo = $true

    if ($Task) {
        $parms.taskList = $Task
    }
    
    $props = @{}
    if ($SubscriptionId) { $props.SubscriptionId = $SubscriptionId }
    if ($ResourceGroup) { $props.ResourceGroup = $ResourceGroup }
    if ($Name) {
        $props.Name = $Name
    }

    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.WorkPath -Value $WorkPath

    $res = Invoke-psake @parms -properties $props -ErrorVariable errorsFound
    
    if ($errorsFound) {
        throw $res
    }

    $resPath = Get-ExtractOutput -Path $WorkPath

    if ($OutputPath) {
        $resPath = Copy-Item -Path $resPath -Destination "$OutputPath" -PassThru -Force | Select-Object -ExpandProperty FullName
    }

    $resPath

    if (-not $KeepFiles) {
        Get-ChildItem -Path $WorkPath -File -Recurse | Where-Object { $_.FullName -ne $resPath } | ForEach-Object { Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue -Confirm:$false -Recurse }
        Get-ChildItem -Path $WorkPath -Directory | Where-Object { $_.FullName -ne $(Split-Path -Path $resPath -Parent) } | ForEach-Object { Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue -Confirm:$false -Recurse }
    }
}


<#
    .SYNOPSIS
        Create valid runbook file based on the task files in the directory
         
    .DESCRIPTION
        Helps you build a valid runbook file, based on all the individual task files (ps1) that are located in the directory
         
        This makes it easy to get a starting point for a new runbook file, based on the tasks you have persisted as individual files
         
        Especially helpful for custom tasks that might be stored in a central repository
         
        The task files has to valid PSake tasks saved as ps1 files
         
    .PARAMETER Path
        Path to the directory where there are valid PSake tasks saved as ps1 files
         
        The default value is set for the internal directory where all the generic tasks that are part of the module is located
         
    .PARAMETER SubscriptionId
        Id of the subscription that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session either needs to be "connected" to the subscription or at least have permissions to work against the subscription
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER ResourceGroup
        Name of the resource group that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the resource group
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER Name
        Name of the logic app, that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the logic app
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER OutputPath
        Path to were the runbook file will be persisted
         
        The path has to be a directory
         
        The runbook file will be named: PsLaExtractor.default.psakefile.ps1
         
    .PARAMETER IncludePrefixSuffix
        Instruct the cmdlet to add the different prefix and suffix options, with the default values that comes with the module
         
        This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByPath
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByPath -Path c:\temp\tasks
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Reads all ps1 files located in c:\temp\tasks
         
        Great to use when you have lots of custom tasks in a directory / repository, and want a good runbook file as starting point
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByPath -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg"
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes
        Prepares the Properties object with SubscriptionId and ResourceGroup
         
        Useful if you have multiple logic apps in the same resource group and you want them extracted using the same runbook file
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByPath -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" -Name "TestLogicApp"
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes
        Prepares the Properties object with SubscriptionId and ResourceGroup and Name
         
        Useful if you want to have a ready to run runbook file, that makes it simple to run the command again and again
        Great for iterative work, where you make lots of small changes in the logic app and want to see how the changes affect your ARM template
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByPath -OutputPath c:\temp\PsLaRunbooks
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes
        Outputs the build to c:\temp\PsLaRunbooks
         
        The runbook file is default named: PsLaExtractor.default.psakefile.ps1
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByPath -IncludePrefixSuffix
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Reads all the internal / generic tasks that are part of the module and implements a valid default path for the includes
        The Properties object inside the runbook file, will be pre-populated with the default prefix and suffix values from the module
         
        This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function New-PsLaRunbookByPath {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Tasks),

        [string] $SubscriptionId,

        [string] $ResourceGroup,

        [string] $Name,

        [string] $OutputPath,

        [switch] $IncludePrefixSuffix
    )

    $files = Get-ChildItem -Path "$Path\*.ps1"

    $res = New-Object System.Collections.Generic.List[System.Object]

    $res.AddRange($(Get-BuildHeader -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Name $Name -IncludePrefixSuffix:$IncludePrefixSuffix))
    
    if ($Path -ne $(Get-PSFConfigValue -FullName PsLogicAppExtractor.ModulePath.Tasks)) {
        $res.Add("# Path to where the custom task files are located")
        $res.Add("`$pathTasksCustom = `"$Path`"")
    }
    
    $res.Add("# Array to hold all tasks for the default task")
    $res.Add("`$listTasks = @()")

    $res.Add("")

    $includes = New-Object System.Collections.Generic.List[System.Object]
    $list = New-Object System.Collections.Generic.List[System.Object]

    $psake.context = New-Object System.Collections.Stack
    $psake.context.push(
        @{
            "tasks"   = @{}
            "aliases" = @{}
        }
    )

    foreach ($item in $files) {

        $psake.context = New-Object System.Collections.Stack
        $psake.context.push(
            @{
                "tasks"   = @{}
                "aliases" = @{}
            }
        )
        
        . $item.FullName

        foreach ($task in $psake.context.tasks) {
            foreach ($value in $task.Values) {
                $list.Add("`$listTasks += `"$($value.Name)`"")
            }
        }
    }

    $psake.context = New-Object System.Collections.Stack

    #TODO! Make sure that we actually need this - other places it was enough with the $psake.context = New-Object System.Collections.Stack
    # $psake.context.push(
    # @{
    # "buildSetupScriptBlock" = {}
    # "buildTearDownScriptBlock" = {}
    # "taskSetupScriptBlock" = {}
    # "taskTearDownScriptBlock" = {}
    # "executedTasks" = new-object System.Collections.Stack
    # "callStack" = new-object System.Collections.Stack
    # "originalEnvPath" = $env:PATH
    # "originalDirectory" = get-location
    # "originalErrorActionPreference" = $global:ErrorActionPreference
    # "tasks" = @{}
    # "aliases" = @{}
    # "properties" = new-object System.Collections.Stack
    # "includes" = new-object System.Collections.Queue
    # }
    # )
    
    $res.Add("# All tasks that needs to be include based on their path")
    $res.AddRange($includes)
    $res.Add("")
    $res.Add("# Building the list of tasks for the default task")
    $res.AddRange($list)
    $res.Add("")
    $res.Add("# Default tasks, the via the dependencies will run all tasks")
    $res.Add("Task -Name `"default`" -Depends `$listTasks")
 
    if ($OutputPath) {
        New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Ignore > $null

        $path = Join-Path -Path $OutputPath -ChildPath "PsLaExtractor.default.psakefile.ps1"

        $encoding = New-Object System.Text.UTF8Encoding($true)
        [System.IO.File]::WriteAllLines($path, $($res.ToArray() -join "`r`n"), $encoding)

        Get-Item -Path $path | Select-Object -ExpandProperty FullName
    }
    else {
        $res.ToArray() -join "`r`n"
    }
}


<#
    .SYNOPSIS
        Create valid runbook file based on the task passed as inputs
         
    .DESCRIPTION
        Helps you build a valid runbook file, based on all the individual tasks that are passed as inputs
         
        This makes it easy to get a starting point for a new runbook file, based on the tasks you have build in your array and then passes into the cmdlet
         
        Tasks are expected to be the ones that are part of the module
         
    .PARAMETER Task
        Names of the tasks that you want to be part of your runbook file
         
        Supports array of task names
         
        Names of the different tasks are expected to be the ones that are part of the module
         
    .PARAMETER SubscriptionId
        Id of the subscription that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session either needs to be "connected" to the subscription or at least have permissions to work against the subscription
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER ResourceGroup
        Name of the resource group that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the resource group
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER Name
        Name of the logic app, that you want to work against
         
        At runtime / execution of Invoke-PsLaExtractor - your current powershell / az cli session needs to have permissions to work against the logic app
         
        Useful when you know upfront what you want to work against, as you don't need to pass the parameter into the Invoke-PsLaExtractor
         
    .PARAMETER OutputPath
        Path to were the runbook file will be persisted
         
        The path has to be a directory
         
        The runbook file will be named: PsLaExtractor.default.psakefile.ps1
         
    .PARAMETER IncludePrefixSuffix
        Instruct the cmdlet to add the different prefix and suffix options, with the default values that comes with the module
         
        This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm"
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Will write the include and taskList in the mentioned order of the tasks
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg"
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Will write the include and taskList in the mentioned order of the tasks
        Prepares the Properties object with SubscriptionId and ResourceGroup
         
        Useful if you have multiple logic apps in the same resource group and you want them extracted using the same runbook file
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -SubscriptionId "f5608f3d-ab28-49d8-9b4e-1b1f812d12e0" -ResourceGroup "TestRg" -Name "TestLogicApp"
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Will write the include and taskList in the mentioned order of the tasks
        Prepares the Properties object with SubscriptionId and ResourceGroup and Name
         
        Useful if you want to have a ready to run runbook file, that makes it simple to run the command again and again
        Great for iterative work, where you make lots of small changes in the logic app and want to see how the changes affect your ARM template
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -OutputPath c:\temp\PsLaRunbooks
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Will write the include and taskList in the mentioned order of the tasks
        Outputs the build to c:\temp\PsLaRunbooks
         
        The runbook file is default named: PsLaExtractor.default.psakefile.ps1
         
    .EXAMPLE
        PS C:\> New-PsLaRunbookByTask -Task "Export-LogicApp.AzCli","ConvertTo-Raw","ConvertTo-Arm" -IncludePrefixSuffix
         
        Creates a valid runbook file, based on the bare minimum and with sane default values
        Will write the include and taskList in the mentioned order of the tasks
        The Properties object inside the runbook file, will be pre-populated with the default prefix and suffix values from the module
         
        This make it easier to make the runbook file work across different environments, without having to worry about prepping different prefix and suffix value prior
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function New-PsLaRunbookByTask {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]] $Task,
        
        [string] $SubscriptionId,

        [string] $ResourceGroup,

        [string] $Name,

        [string] $OutputPath,

        [switch] $IncludePrefixSuffix
    )
    
    $res = New-Object System.Collections.Generic.List[System.Object]

    $res.AddRange($(Get-BuildHeader -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -Name $Name -IncludePrefixSuffix:$IncludePrefixSuffix))

    $res.Add("# Array to hold all tasks for the default task")
    $res.Add('$listTasks = @()')

    $res.Add('')

    $list = New-Object System.Collections.Generic.List[System.Object]
    
    foreach ($item in $Task) {
        $list.Add("`$listTasks += `"$item`"")
    }

    $res.AddRange($list)
    $res.Add("")
    $res.Add("# Default tasks, the via the dependencies will run all tasks")
    $res.Add('Task -Name "default" -Depends $listTasks')
    
    if ($OutputPath) {
        New-Item -Path $OutputPath -ItemType Directory -Force -ErrorAction Ignore > $null

        $path = Join-Path -Path $OutputPath -ChildPath "PsLaExtractor.default.psakefile.ps1"

        $encoding = New-Object System.Text.UTF8Encoding($true)
        [System.IO.File]::WriteAllLines($path, $($res.ToArray() -join "`r`n"), $encoding)

        Get-Item -Path $path | Select-Object -ExpandProperty FullName
    }
    else {
        $res.ToArray() -join "`r`n"
    }
}


<#
    .SYNOPSIS
        Output the tasks result to a file
         
    .DESCRIPTION
        Persists the tasks output into a file, either the raw content or the object
         
        Sets the $Script:FilePath = $Path, to ensure the next tasks can pick up the file and continue its work
         
    .PARAMETER Path
        Path to where the tasks wants the ouput to be persisted
         
    .PARAMETER Content
        Raw string that should be written to the desired path
         
    .PARAMETER InputObject
        The object that should be written to the desired path
         
        Will be converted to a json string, usign the ConvertTo-Json
         
        Important note: If you need the InputObject to be written with a specific structure, the object has to be of the expected type before being passed into the cmdlet
        A simple cast can ensure this to work as intended
         
    .EXAMPLE
        PS C:\> Out-TaskFile -Path "C:\temp\work_directory\1_Export-LogicApp.AzCli" -InputObject $([ArmTemplate]$armObj)
         
        Outputs the armObj variable to the path: "C:\temp\work_directory\1_Export-LogicApp.AzCli"
        The armObj is casted to the [ArmTemplate] type, to ensure it is persisted as the expected json structure
         
    .EXAMPLE
        PS C:\> Out-TaskFile -Path "C:\temp\work_directory\1_Export-LogicApp.AzCli" -Content '{"Test":"Test"}'
         
        Outputs the content string: '{"Test":"Test"}' to the path: "C:\temp\work_directory\1_Export-LogicApp.AzCli"
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Out-TaskFile {
    [CmdletBinding(DefaultParameterSetName = "InputObject")]
    param (
        [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskOutputFile),

        [Parameter(Mandatory = $true, ParameterSetName = "Content")]
        [string] $Content,

        [Parameter(Mandatory = $true, ParameterSetName = "InputObject")]
        [object] $InputObject
    )
    
    if ($InputObject) {
        $Content = $InputObject | ConvertTo-Json -Depth 100
    }

    $encoding = New-Object System.Text.UTF8Encoding($true)
    [System.IO.File]::WriteAllLines($Path, $Content, $encoding)
    
    if ($(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext)) {
        $taskPath = Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskPath
        Copy-Item -Path $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskInputNext) -Destination "$taskPath\Input.json"
    }

    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskInputNext -Value $Path
}


<#
    .SYNOPSIS
        Output the tasks result to a file, as an ARM template
         
    .DESCRIPTION
        Persists the tasks output into a file, as an ARM template
         
    .PARAMETER InputObject
        The object that should be written to the desired path
         
        Will be converted to a json string, usign the ConvertTo-Json
         
    .EXAMPLE
        PS C:\> Out-TaskFileArm -InputObject $armObj
         
        Outputs the armObj variable
        The armObj is casted to the [ArmTemplate] type, to ensure it is persisted as the expected json structure
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Out-TaskFileArm {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject
    )

    Out-TaskFile -InputObject $([ArmTemplate]$InputObject)
}


<#
    .SYNOPSIS
        Output the tasks result to a file, as a LogicApp json structure
         
    .DESCRIPTION
        Persists the tasks output into a file, as a LogicApp json structure
         
    .PARAMETER InputObject
        The object that should be written to the desired path
         
        Will be converted to a json string, usign the ConvertTo-Json
         
    .EXAMPLE
        PS C:\> Out-TaskFileLogicApp -InputObject $lgObj
         
        Outputs the armObj variable
        The armObj is casted to the [ArmTemplate] type, to ensure it is persisted as the expected json structure
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Out-TaskFileLogicApp {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject
    )

    Out-TaskFile -InputObject $([LogicApp]$InputObject)
}


<#
    .SYNOPSIS
        Remove parameter from the ARM template
         
    .DESCRIPTION
        Removes an ARM template parameter by the name provided
         
    .PARAMETER InputObject
        The ARM object that you want to work against
         
        It has to be a object of the type [ArmTemplate] for it to work properly
         
    .PARAMETER Name
        Name of the parameter that you want to work against
         
        If the parameter exists, it will be removed from the InputObject
         
    .EXAMPLE
        PS C:\> Remove-ArmParameter -InputObject $armObj -Name "logicAppName"
         
        Removes the logicAppName ARM template parameter
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Remove-ArmParameter {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('ParameterName')]
        [Parameter(Mandatory = $true)]
        [string] $Name
    )

    if ($InputObject.parameters.$Name) {
        $InputObject.parameters.PsObject.Properties.Remove($Name)
    }

    $InputObject
}


<#
    .SYNOPSIS
        Remove variable from the ARM template
         
    .DESCRIPTION
        Removes an ARM template variable by the name provided
         
    .PARAMETER InputObject
        The ARM object that you want to work against
         
        It has to be a object of the type [ArmTemplate] for it to work properly
         
    .PARAMETER Name
        Name of the variable that you want to work against
         
        If the variable exists, it will be removed from the InputObject
         
    .EXAMPLE
        PS C:\> Remove-ArmVariable -InputObject $armObj -Name "logicAppName"
         
        Removes the logicAppName ARM template variable
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Remove-ArmVariable {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('VariableName')]
        [Parameter(Mandatory = $true)]
        [string] $Name
    )

    if ($InputObject.variables.$Name) {
        $InputObject.variables.PsObject.Properties.Remove($Name)
    }

    $InputObject
}


<#
    .SYNOPSIS
        Remove parm (parameter) from the LogicApp
         
    .DESCRIPTION
        Removes an LogicApp parm (parameter) by the name provided
         
    .PARAMETER InputObject
        The LogicApp object that you want to work against
         
        It has to be a object of the type [LogicApp] for it to work properly
         
    .PARAMETER Name
        Name of the parm (parameter) that you want to work against
         
        If the parm (parameter) exists, it will be removed from the InputObject
         
    .EXAMPLE
        PS C:\> Remove-LogicAppParm -InputObject $armObj -Name "TriggerQueue"
         
        Removes the TriggerQueue LogicApp parm (parameter)
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Remove-LogicAppParm {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [object] $InputObject,

        [Alias('ParmName')]
        [Parameter(Mandatory = $true)]
        [string] $Name
    )
    
    if ($InputObject.properties.definition.parameters.$Name) {
        $InputObject.properties.definition.parameters.PsObject.Properties.Remove($Name)
    }

    $InputObject
}


<#
    .SYNOPSIS
        Set the current working directory
         
    .DESCRIPTION
        Sets the current tasks working directory, based on the current PsLaWorkPath and the execution number that task is in the overall execution
         
        Outputs the path that has been constructed
         
    .PARAMETER Path
        Path to the current working directory
         
        The value passed in should always be the $PsLaWorkPath, to ensure that things are working
         
    .PARAMETER FileName
        The of the file that you want to be configured for the task
         
        Is normally equal to the name of the Logic App
         
    .EXAMPLE
        PS C:\> Set-TaskWorkDirectory
         
        Creates a new sub directory under the $PsLaWorkPath location
        The sub directory is named "$taskCounter`_$TaskName"
         
        The output will be: "$Path\$taskCounter`_$TaskName"
         
    .NOTES
         
        Author: Mötz Jensen (@Splaxi)
         
#>

function Set-TaskWorkDirectory {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param (
        [string] $Path = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.WorkPath),
        
        [string] $FileName = "$Name.json"
    )
    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskCounter -Value $($(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskCounter) + 1)
    $taskCounter = $(Get-PSFConfigValue -FullName PsLogicAppExtractor.Execution.TaskCounter)
    
    $taskName = $($psake.context.Peek().CurrentTaskName)
    $newPath = "$Path\$taskCounter`_$TaskName"
    New-Item -Path $newPath -ItemType Directory -Force -ErrorAction Ignore > $null

    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskPath -Value $newPath
    Set-PSFConfig -FullName PsLogicAppExtractor.Execution.TaskOutputFile -Value "$newPath\$fileName"
}

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'ModulePath.Base' -Value $script:ModuleRoot -Initialize -Description "The base path for the module, used for the internal functions to be able to provide the full path for internal objects."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'ModulePath.Tasks' -Value "$($script:ModuleRoot)\internal\tasks" -Initialize -Description "The full path for all generic tasks that are part of the module."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'ModulePath.Classes' -Value "$($script:ModuleRoot)\internal\Classes" -Initialize -Description "The full path for all the generic classes that are part of the module."

Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.prefix' -Value "tag_" -Initialize -Description "The default prefix for Tag objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.tag.suffix' -Value "" -Initialize -Description "The default suffix for Tag objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string"

Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.prefix' -Value "parm_" -Initialize -Description "The default prefix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.parm.suffix' -Value "" -Initialize -Description "The default suffix for parm (parameter) objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string."

Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.prefix' -Value "connection_" -Initialize -Description "The default prefix for connection objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.connection.suffix' -Value "_id" -Initialize -Description "The default suffix for connection objects, used as a fallback value for the Format-Name cmdlet."

Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.prefix' -Value "trigger_" -Initialize -Description "The default prefix for trigger objects, used as a fallback value for the Format-Name cmdlet."
Set-PSFConfig -Module 'PsLogicAppExtractor' -Name 'prefixsuffix.trigger.suffix' -Value "" -Initialize -Description "The default suffix for trigger objects, used as a fallback value for the Format-Name cmdlet. Be default an empty string."


<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'PsLogicAppExtractor.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "PsLogicAppExtractor.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PsLogicAppExtractor.alcohol
#>


New-PSFLicense -Product 'PsLogicAppExtractor' -Manufacturer 'Motz' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-04-04") -Text @"
Copyright (c) 2022 Motz
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code