Public/Actions.ps1

# Actions
Function New-TMAction {
    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$Action,
        [Parameter(Mandatory = $false)][PSObject]$Project,
        [Parameter(Mandatory = $false)][Switch]$Update,
        [Parameter(Mandatory = $false)][Switch]$Passthru
    )
    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    #Honor SSL Settings
    $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

    ## Check for existing Action
    $ActionCheck = Get-TMAction -Name $Action.name -TMSession $TMSession
    if ($ActionCheck -and -not $Update) {
        if ($PassThru) {
            return $ActionCheck
        } else {
            return
        }
    }


    ## No Action exists. Create it
    $Instance = $TMSession.TMServer.Replace('/tdstm', '')
    $instance = $instance.Replace('https://', '')
    $instance = $instance.Replace('http://', '')

    ## Construct the intial part of the URL
    $uri = 'https://'
    $uri += $instance
    $uri += '/tdstm/ws/apiAction'

    ## Cleans the Action Name of characters we don't want
    $Action.name = $Action.name -replace '\\', '' -replace '\/', '' -replace '\:', '' -replace '>', '' -replace '<', '' -replace '\(', '' -replace '\)', '' -replace '\*', ''
    $Action.name = $Action.name -replace ('/\:><()*'.ToCharArray() -join '|\'), ''

    ## If The Existing action should be updated
    if ($ActionCheck -and $Update) {

        ## When the Action is an update, use important details from the current object.
        $Action.id = $ActionCheck.id
        $Action.dateCreated = $ActionCheck.dateCreated
        $Action.lastUpdated = $ActionCheck.lastUpdated
        $Action.version = $ActionCheck.version
        $Action.project = $ActionCheck.project

        ## Set the HTTP call details
        $uri += '/' + $Action.id
        $HttpMethod = 'Put'
    } else {

        ## Process as a new object.
        $HttpMethod = 'Post'
        $Action.PSObject.Properties.Remove('id')
    }

    ## Lookup Provider and Credential IDs, Field Specs
    if ([String]::IsNullOrWhiteSpace($Action.provider.name)) {
        Write-Error -Message "Provider name is blank for Action: $($Action.name)"
        return
    }
    $ProviderID = (Get-TMProvider -Name $Action.provider.name -TMSession $TMSession).id
    if (!$ProviderID) {
        # Create the provider if it doesn't exist
        $NowFormatted = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ' -AsUTC).ToString()
        $Provider = [PSCustomObject]@{
            id          = $null
            name        = $Action.provider.name
            description = ''
            comment     = ''
            dateCreated = $NowFormatted
            lastUpdated = $NowFormatted
        }
        $ProviderID = (New-TMProvider -Provider $Provider -PassThru -TMSession $TMSession).id
    }
    $Action.provider.id = $ProviderID
    $FieldSettings = Get-TMFieldSpecs -TMSession $TMSession

    ## Evaluate any Parameters on the Action
    if (-not $Action.methodParams) {
        ## Assign an empty array instead of null
        $Action.methodParams = @()
    }

    ## Get the Action.methodParams into an array of $Parameters
    if ($Action.methodParams -is [String]) {
        $Action.methodParams = $Action.methodParams | ConvertFrom-Json
    }

    ## Create a Local Pameters object by running any methodParams through the constructor
    $Parameters = $Action.methodParams | ForEach-Object { [TMActionParameter]::new($_) }

    ## Evaluate/Process each of the parameters
    for ($j = 0; $j -lt $Parameters.Count; $j++) {

        ## Update Context Linked fields
        if ($Parameters[$j].context -in @('ASSET', 'DEVICE', 'APPLICATION', 'STORAGE', 'DATABASE')) {

            ## Use the 'fieldLabel' property to determine the 'fieldName'
            if ($Parameters[$j].PSobject.Properties.name -match 'fieldLabel') {

                $DomainClass = $Parameters[$j].context
                if ($DomainClass -eq 'ASSET') {
                    $DomainClass = 'APPLICATION'
                }

                ## Check if this is a CustomN field
                if ($Parameters[$j].fieldName -eq 'customN') {

                    ## Update the Project's assigned Field by associating the label to the current field list.
                    $Parameters[$j].fieldName = ($FieldSettings.$DomainClass.fields | Where-Object { $_.label -eq $Parameters[$j].fieldLabel }).field
                }

                ## Remove the 'fieldLabel property as it's invalid column definition
                $Parameters[$j].PSObject.Properties.Remove('fieldLabel')
            }
        }

        ## TM-23136 - Convert 6.3+ Method Param changes, adapting to older/newer combinations
        if ($TMSession.TMVersion -ge '6.3.0') {

            ## To be tested, need to import a pre-6.3 with prompting to validate
            if ($Parameters[$j].paramName -like 'get_*') {

                ## Split the name parts to get the Prompt Type
                $ParamNameParts = $Parameters[$j].paramName -split '_'
                $ParamPromptType = $ParamNameParts[1] -replace '-nosave', ''
                $CanCache = $ParamNameParts[1] -match '-nosave'

                ## Update the Parameter name and remove the 'prompt' parameter
                $Parameters[$j].paramName = $Parameters[$j].paramName -replace "get_$($ParamPromptType.toLower())_", ''
                $Parameters[$j] | Add-Member -Force -NotePropertyName 'prompt' -NotePropertyValue @{
                    type     = $ParamPromptType
                    canCache = $CanCache
                    minSize  = 0
                    maxSize  = 1024
                }
            }

            ## Make sure 'Blank' prompts are reset to null
            if($Parameters[$j].prompt) {
                if($Parameters[$j].prompt.type.toLower() -eq 'blank'){
                    $Parameters[$j].prompt = $null
                }
            }
            else {
                $Parameters[$j] | Add-Member -Force -NotePropertyName 'prompt' -NotePropertyValue $null
            }
        } else {

            ## Convert Prompt-bearing action definitions that have a type configured into a get_something_name
            if (
                ($Parameters[$j].PSObject.Properties.Name -contains 'prompt') -and
                -not ([string]::IsNullOrWhiteSpace($Parameters[$j].prompt.type)) -and
                ($Parameters[$j].prompt.type.ToLower() -ne 'blank')
            ) {

                ## Update the name of the parameter
                $ParamNameParts = @('get_')
                $ParamNameParts += $Parameters[$j].prompt.type.toLower()

                if (-Not $Parameters[$j].prompt.canCache) {
                    $ParamNameParts += '-nosave'
                }

                $ParamNameParts += @('_')
                $ParamNameParts += $Parameters[$j].paramName
                $Parameters[$j].paramName = ($ParamNameParts -join '')

                ## Remove the prompt field
                $Parameters[$j].PSObject.Properties.Remove('prompt')
            }
        }
    }

    ## Update the methodParams to an array of whatever resulted of a possible conversion
    switch ($Parameters.count) {
        0 {
            $Action.methodParams = [array]@()
        }
        1 {
            $Action.methodParams = [array](@($Parameters[0]))
        }
        Default {
            $Action.methodParams = [array]$Parameters
        }
    }

    ## Set the Action Type to a single string, which TM 6.1, 6.2 and 6.3...
    ## All agree to accep this value whether the data being posted is ...
    ## New or an update... If you change it, there better be a comment at least this...
    ## long.
    $Action.actionType = "POWER_SHELL"

    ## TM 6.3+ requires an Object for Parameters and Reaction Scripts
    if ($TMSession.TMVersion -ge '6.3') {
        if ($Action.methodParams -is [String]) {
            $Action.methodParams = $Action.methodParams | ConvertFrom-Json
        }
        if ($Action.reactionScripts -is [String]) {
            $Action.reactionScripts = $Action.reactionScripts | ConvertFrom-Json
        }
    } else {

        ## Ensure the MethodParams and ReactionScripts are JSON-Strings
        if (-not ($Action.methodParams -is [String])) {

            switch ($Action.MethodParams.GetType().BaseType.toString()) {
                'TMActionParameter' {
                    $Action.methodParams = "[$($Action.methodParams | ConvertTo-Json -Depth 100 -Compress)]"
                }
                'System.Array' {
                    $Action.methodParams = , $Action.methodParams | ConvertTo-Json -Depth 100 -Compress
                    break
                }
                Default {
                    Write-Host "Type: $_"
                    $Action.methodParams = $Action.methodParams | ConvertTo-Json -Depth 100 -Compress
                }
            }
        }
        if (-not ($Action.reactionScripts -is [String])) {
            $Action.reactionScripts = $Action.reactionScripts | ConvertTo-Json -Depth 100 -Compress
        }

        # ## schema v4 added TM 6.3 Prompt parameters that are only for 6.3
        # if ($Action.PSObject.Properties.Name -contains 'prompt') {
        # $Action.PSObject.Properties.Remove('prompt')
        # }
    }

    <#
 
        NOTING an important checkpoint here. Above this point in the code...
            * Some [object] was provided. Might have been a file path, a basic object, or event a formed TMAction
              * These come in MANY varieties which are generally the subject of the TME 'formatting defintion'
                  which are all version numbered. The code used in the process and where things happen are very delicate.
                Trust the way it's organized, because
            * The object is converted into an $Action object, which carries whatever form it was delivered in
            * The Since there is no SCHEMA validation system, the important properties are inspected and updated
                * Based on the existing format (and the schema-version it has (might be old...))
                * and based on the Server the Action is going to be installed in, it then updates
                * All of those important properties, into the TMServerVersion specific format...
            * There is a Many-Many relationship that was used as a testing framework on this ticket.
 
            * Tested Scenarios
                * Create Action named "... From61" with Parameter definitions, with prompt settings
                * Export the Actions from 6.1
                * Reimported to 6.1 successfully
                * Import the 6.1 into TM6.2
 
 
 
            * Once all of these conversions have completed, the resulting "$Action" variable contains
              something that's not quite a class-defined thing, without having to build in support for the
              legacy formats...
 
              It might possibly have started as 'new', been converted to old, and updated again, but that's simply
              the adaptable way given we have 3 versions of support built into this process...
 
              Congrats, now post the data.
    #>


    $PostBodyJSON = $Action | ConvertTo-Json -Depth 10 -Compress
    Write-Verbose "Action JSON Data: $PostBodyJSON"
    Set-TMHeaderAccept 'JSON' -TMSession $TMSession
    Set-TMHeaderContentType 'JSON' -TMSession $TMSession
    try {
        $response = Invoke-WebRequest -Method $HttpMethod -Uri $uri -WebSession $TMSession.TMWebSession -Body $PostBodyJSON @TMCertSettings
        if ($response.StatusCode -eq 200) {
            $responseContent = $response.Content | ConvertFrom-Json
            if ($responseContent.status -ne 'success') {
                throw ($responseContent.errors -join ', ')
            } else {
                if ($PassThru) {
                    return $responseContent.data
                }
            }
        } elseif ($Response.StatusCode -eq 204) {
            return
        }
    } catch {
        Write-Host 'Unable to create Action.'
        return $_
    }
}

Function Get-TMAction {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Mandatory = $false)]
        [PSObject]$TMSession = 'Default',

        [Parameter(Mandatory = $false, ParameterSetName = 'ByName')]
        [String[]]$Name,

        [Parameter(Mandatory = $false, ParameterSetName = 'ById')]
        [String[]]$Id,

        [Parameter(Mandatory = $false)]
        [String[]]$ProviderName,

        [Parameter(Mandatory = $false)]
        [Switch]$ResetIDs,

        [Parameter(Mandatory = $false)]
        [String]$SaveCodePath,

        [Parameter(Mandatory = $false)]
        [bool]$Api = $true,

        [Parameter(Mandatory = $false)]
        [Switch]$Passthru
    )

    ## Get Session Configuration
    $TMSession = Get-TMSession $TMSession

    #Honor SSL Settings
    $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL }

    $Instance = $TMSession.TMServer.Replace('/tdstm', '').Replace('https://', '').Replace('http://', '')
    if ($Api) {

        # Format the uri
        $uri = "https://$instance/tdstm/api/apiAction?project=$($TMSession.UserContext.project.id)"

        try {
            $response = Invoke-RestMethod -Method Get -Uri $uri -WebSession $TMSession.TMRestSession @TMCertSettings
            $Result = $response | Where-Object { $_.actionType.id -EQ 'POWER_SHELL' }
        } catch {
            throw $_
        }
    } else {

        # Format the uri
        $uri = "https://$instance/tdstm/ws/apiAction"

        try {
            $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings
        } catch {
            throw $_
        }

        if ($Response.StatusCode -in @(200, 204)) {
            $Result = ($Response.Content | ConvertFrom-Json).data
            $Result = $Result | Where-Object actionType -EQ 'POWER_SHELL'
        } else {
            throw 'Unable to collect Actions.'
        }
    }

    ## Return the details -- Filter based on passed parameters
    if ($ProviderName) {
        $Result = $Result | Where-Object { $_.provider.name -in $ProviderName }
    } elseif ($Name) {
        $Result = $Result | Where-Object { $_.name -in $Name }
    } elseif ($Id) {
        $Result = $Result | Where-Object { $_.id -in $Id }
    }

    ## 6.1 and forward, the endpoints do not supply the source code with the object
    if ($TMSession.TMVersion -ge '6.0.0') {

        if ($Api) {

            ## Update each of the objects that were returned with their full version
            $Result = $Result | ForEach-Object {
                # Format the uri
                $uri = "https://$instance/tdstm/api/apiAction/$($_.id)?project=$($TMSession.UserContext.project.id)"
                Invoke-RestMethod -Method Get -Uri $uri -WebSession $TMSession.TMRestSession @TMCertSettings
            }
        } else {
            ## Update each of the objects that were returned with their full version
            $Result = $Result | ForEach-Object {
                # Format the uri
                $uri = "https://$instance/tdstm/ws/apiAction/$($_.id)?project=$($TMSession.UserContext.project.id)"
                Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings

                try {
                    $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings
                } catch {
                    throw $_
                }

                if ($Response.StatusCode -in @(200, 204)) {
                    ($Response.Content | ConvertFrom-Json).data
                } else {
                    throw 'Unable to collect Actions.'
                }
            }
        }
    }

    # Get Field / Label maps to translate the parameters
    $FieldToLabelMap = Get-TMFieldToLabelMap -TMSession $TMSession
    foreach ($Action in $Result | Where-Object methodParams) {

        ## Convert JSON data into an object
        $Parameters = $Action.methodParams
        if ($Parameters -is [String]) {
            $Parameters = $Action.methodParams | ConvertFrom-Json
        }

        foreach ($Parameter in $Parameters | Where-Object Context -In 'DEVICE', 'APPLICATION', 'STORAGE', 'DATABASE' | Where-Object fieldName) {

            ## The custom column field identifer is lost when the IDs get reset. Add a fieldLabel node to put the custom field label. This will get replaced/updated by the Import
            $FieldLabel = $FieldToLabelMap[$Parameter.context][$Parameter.fieldName]
            $Parameter | Add-Member -NotePropertyName 'fieldLabel' -NotePropertyValue $FieldLabel -Force

            ## Clear the Custom field number associated to the Parameter
            if ($ResetIDs -and ($Parameter.fieldName -like 'custom*')) {
                $Parameter.fieldName = 'customN'
            }

        }

        $Action.methodParams = $Parameters

        ## Apply Package Format version 2 details
        if ($TMSession.TMVersion -lt '6.1.0') {
            Add-Member -InputObject $Action -NotePropertyName 'actionType' -NotePropertyValue ($Action.actionType.id ?? $Action.actionType) -Force
        }
    }

    if ($ResetIDs) {
        ## Clear pertinent data in each Action
        foreach ($Action in $Result) {
            $Action.id = $null
            $Action.project.id = $null
            if ($Action.credential) { $Action.credential.id = $null }
            $Action.provider.id = $null
        }
    }

    ## Save the Code Files to a folder
    if ($SaveCodePath) {

        ## Save Each of the Script Source Data
        foreach ($Action in $Result) {

            ## Get a FileName safe version of the Provider Name
            $SafeProviderName = Get-FilenameSafeString $Action.provider.name
            $SafeActionName = Get-FilenameSafeString $Action.name

            ## Create the Provider Action Folder path
            $ProviderPath = Join-Path $SaveCodePath $SafeProviderName

            if (-not (Test-Path -Path $ProviderPath -PathType Container) ) {
                Test-FolderPath $ProviderPath
            }

            ## Create a File name for the Action
            $ProviderScriptConfigPath = Join-Path $ProviderPath ($SafeActionName + '.json')
            $ProviderScriptPath = Join-Path $ProviderPath ($SafeActionName + '.ps1')

            ## Split the script out of the Item, and save both files.
            $ScriptBlock = $Action.script
            Set-Content -Path $ProviderScriptPath -Force -Value $ScriptBlock

            ## Remove the script from the JSON file
            $Action.PSObject.Properties.Remove('script')

            ## Disabling the lass structure computation on the write, because there ARE deliberately-
            ## offline only properties that are required for using them as an input
            ## Convert the Parameters to a TMActionParameters Class Object
            if ($Action.methodParams) {
                $Parameters = $Action.methodParams | ForEach-Object { [TMActionParameter]::new($_) }
                $Action.methodParams = $Parameters
            }

            ## Convert the ReactionScript object to an Object
            if ($Action.reactionScripts -is [String]) {
                $Action.reactionScripts = $Action.reactionScripts | ConvertFrom-Json
            }

            Set-Content -Path $ProviderScriptConfigPath -Force -Value ($Action | ConvertTo-Json -Depth 100)
        }
    }

    if ($Passthru -or !$SaveCodePath) {
        ## Return an array even if it holds a single value - consistency
        return , $Result
    }
}

Function Read-TMActionScriptFile {
    param(
        [Parameter(Mandatory = $false)]
        [PSObject]$TMSession = 'Default',

        [Parameter(Mandatory = $true)]
        [String]$Path
    )

    ## First order of business is to determine if this Script file has a counterpart Json file
    $ActionConfigJsonPath = $Path -Replace '.ps1', '.json'
    $ActionConfigFile = Get-Item -Path $ActionConfigJsonPath -ErrorAction 'SilentlyContinue'

    ## Check if there is a config JSON file, if so, get the Action from the 2 files
    if ($ActionConfigFile) {

        ## Get the Action Object that doesn't have the source code
        $TMAction = Get-Content -Path $ActionConfigFile | ConvertFrom-Json

        $FieldToLabelMap = Get-TMLabelToFieldMap -TMSession $TMSession

        ## If this Action has parameters, handle sorting and updating them
        if ($TMAction.methodParams) {

            ## Convert the methodParams to a String, as it should be
            switch ($TMAction.methodParams.GetType().Name) {
                ## Array of objects, each is a PSCustomObject of a TMActionParameter type
                'Object[]' {

                    ## Remove Offline properties that do not belong on the object.
                    foreach ($TMActionParameter in $TMAction.methodParams) {

                        ## Recast the object as a Parameter with it's current configuration
                        $TMActionParameter = [TMActionParameter]::new($TMActionParameter)

                        ## Add the correct TM Field Label
                        if ($TMActionParameter.fieldName -eq 'customN') {
                            $TMActionParameter.fieldName = $FieldToLabelMap.($TMActionParameter.context.toUpper()).($TMActionParameter.fieldLabel)
                        }
                    }
                    break
                }

                ## There might only be one Param (and was written without being forced to a list)
                'PSCustomObject' {

                    Write-Debug 'Updating TMAction.methodParams to a Class Object'
                    $TMAction.methodParams = [TMActionParameter]::new($TMAction.methodParams)

                    ## Add the correct TM Field Label
                    if ($TMAction.methodParams.fieldName -eq 'customN') {
                        Write-Host "Updating a customN field reference: $($TMAction.methodParams.fieldName) to " -NoNewline
                        $TMAction.methodParams.fieldName = $FieldToLabelMap.($TMAction.methodParams.context.toUpper()).($TMAction.methodParams.fieldLabel)
                        Write-Host $($TMAction.methodParams.fieldName) -ForegroundColor Yellow
                    }

                    if ($TMAction.methodParams.paramName -like 'get_*') {
                        Write-Debug 'methodparams.paramName matches a legacy get_* string. updating the Parameter settings with the info.'

                        # $PromptNameParts = $TMAction.methodParams.paramName -split '_'
                        # $CanPrompt = $PromptNameParts[1].toLower() -notlike '*-nosave'
                        # $PromptType = $PromptNameParts[1].toUpper() -replace '-NOSAVE', ''
                        # $ParameterName = $PromptNameParts[2]

                        # $TMAction.methodParams = [TMActionParameter]::new(@{
                        # paramName = $ParameterName

                        # })
                    }
                    # $TMAction.methodParams.PSObject.Properties.Remove('order')
                    # $TMAction.methodParams.PSObject.Properties.Remove('fieldLabel')
                    # $TMAction.methodParams = @(($TMAction.methodParams)) | ConvertTo-Json -Depth 5 -Compress

                    ## Ensure that the Method Params is an array

                    break
                }
                ## For completeness, some Extensions have the literal JSON string data. If so,
                'String' {
                    ## Do nothing
                    Write-Host ($TMAction.methodParams | ConvertTo-Json -depth 5) -ForegroundColor RED
                    throw 'TransitionManager.Public.Actions.String wants to understand a STRING value for methodParams. Check it further'
                    break
                }

                Default {
                    ## If there was no value, or something else entirely, default to an empty set of parameters
                    $TMAction.methodParams = @()
                }
            }
        }



        ## Read the Script Content and add it to the Action Object
        $ScriptContent = (Get-Content -Path $Path -Raw) ?? ''
        Add-Member -InputObject $TMAction -NotePropertyName script -NotePropertyValue ($ScriptContent.ToString() ?? '') -Force
    }

    ## Perform a read of the PS file to capture the ReferenceDesign formatted settings
    else {
        ## Name the Input File
        $Content = Get-Content -Path $Path -Raw

        ## Ignore Empty Files
        if (-Not $Content) {
            return
        }

        $ContentLines = Get-Content -Path $Path

        ## Create Automation Token Variables Parse the Script File
        New-Variable astTokens -Force
        New-Variable astErr -Force
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$astTokens, [ref]$astErr)

        ##
        ## Assess the Script Parts to get delineating line numbers
        ##

        ## Locate the Delimiting line
        $ConfigBlockEndLine = $astTokens | `
                Where-Object { $_.Text -like '## End of TM Configuration, Begin Script*' } |`
                Select-Object -First 1 | `
                Select-Object -ExpandProperty Extent | `
                Select-Object -ExpandProperty StartLineNumber

        ## Test to see if the Script is formatted output with Metadata
        if (-not $ConfigBlockEndLine) {

            ## There is no metadata, create the basic object with just the source code
            $ActionConfig = [pscustomobject]@{
                ActionName   = (Get-Item -Path $Path).BaseName
                Description  = ''
                ProviderName = (Get-Item -Path $Path).Directory.BaseName
            }

            ## Create the objects the below constructor expect
            $ConfigBlockEndLine = -1
            $Params = $null
            $TMActionParams = [System.Collections.ArrayList] @()
        } else {

            ##
            ## Read the Script Header to gather the configurations
            ##

            ## Get all of the lines in the header comment
            $TMConfigHeader = 0..$ConfigBlockEndLine | ForEach-Object {

                if ($astTokens[$_].kind -eq 'comment') { $astTokens[$_] }
            } | Select-Object -First 1 | Select-Object -ExpandProperty Text

            ## Create a Properties object that will store the values listed in the header of the script file
            $ActionConfig = [PSCustomObject]@{
            }

            ## Process each line of the Header string
            $TMConfigHeader -split "`n" | ForEach-Object {

                ## Process each line of the comment
                if ($_ -like '*=*') {
                    $k, $v = $_ -split '='
                    $k = $k.Trim() -replace "'", '' -replace '"', ''
                    $v = $v.Trim() -replace "'", '' -replace '"', ''
                    $ActionConfig | Add-Member -NotePropertyName $k -NotePropertyValue $v
                }
            }

            ##
            ## Read the Script Block
            ##

            ## Create a Text StrinBuilder to collect the Script into
            $ActionConfigStringBuilder = New-Object System.Text.StringBuilder

            ## For each line in the Code Block, add it to the Action Script Code StringBuilder
            0..$ConfigBlockEndLine | ForEach-Object {
                $ActionConfigStringBuilder.AppendLine($ContentLines[$_]) | Out-Null
            }
            $ActionConfigScriptString = $ActionConfigStringBuilder.ToString()
            $ActionConfigScriptBlock = [scriptblock]::Create($ActionConfigScriptString)

            ## Invoke the Script Block to create the $Params Object in this scope
            ## this line populates the $Params object from the Action Script
            Invoke-Command -ScriptBlock $ActionConfigScriptBlock -NoNewScope

            ## Collect the Parameters
            $TMActionParams = [System.Collections.ArrayList] @()
            ## Action Parameter Class Definition
            # {
            # "desc": "",
            # "type": "string",
            # "value": "",
            # "context": "DEVICE",
            # "encoded": false,
            # "readonly": false,
            # "required": false,
            # "fieldName": null,
            # "paramName": "IPAddress",
            # "fieldLabel": "IP Address"
            # }

            ## Process the Parameters into Action Params
            foreach ($ParamLabel in $Params.Keys) {

                ## Create a new Params Object to load to the Action
                $NewParamConfig = [PSCustomObject]@{
                    type        = 'string'
                    value       = ''
                    description = ''
                    context     = ''
                    fieldLabel  = ''
                    fieldName   = ''
                    required    = $false
                    encoded     = $false
                    readonly    = $false
                }

                ## Read the existing Params configuration, assembling each additional Paramater option
                $ScriptParamConfig = $Params.$ParamLabel
                $ScriptParamConfig.Keys | ForEach-Object {
                    switch ($_.toLower()) {
                        'value' {
                            $NewParamConfig.value = $ScriptParamConfig[$_]
                            break
                        }
                        'type' {
                            $NewParamConfig.type = $ScriptParamConfig[$_]
                            break
                        }
                        'description' {
                            $NewParamConfig.description = $ScriptParamConfig[$_]
                            break
                        }
                        'desc' {
                            $NewParamConfig.description = $ScriptParamConfig[$_]
                            break
                        }
                        'context' {
                            $NewParamConfig.context = $ScriptParamConfig[$_]
                            break
                        }
                        'fieldlabel' {
                            $NewParamConfig.fieldLabel = $ScriptParamConfig[$_]
                            break
                        }
                        'fieldName' {
                            $NewParamConfig.fieldName = $ScriptParamConfig[$_]
                            break
                        }
                        'required' {
                            $NewParamConfig.required = (ConvertTo-Boolean $ScriptParamConfig[$_])
                            break
                        }
                    }
                }

                ## Add the Parameter Name from the Configuration Object
                Add-Member -InputObject $NewParamConfig -NotePropertyName 'paramName' -NotePropertyValue $ParamLabel
                $TMActionParams.Add($NewParamConfig) | Out-Null
            }

        }

        ## Note where the Action Code is located
        $StartCodeBlockLine = $ConfigBlockEndLine + 1
        $EndCodeBlockLine = $ast[-1].Extent.EndLineNumber

        ## Create a Text StrinBuilder to collect the Script into
        $ActionScriptStringBuilder = New-Object System.Text.StringBuilder

        ## For each line in the Code Block, add it to the Action Script Code StringBuilder
        $StartCodeBlockLine..$EndCodeBlockLine | ForEach-Object {
            $ActionScriptStringBuilder.AppendLine($ContentLines[$_]) | Out-Null
        }

        ## Convert the StringBuilder to a Multi-Line String
        $ActionScriptCode = $ActionScriptStringBuilder.ToString()

        ## If no Parameters were assembled, provide an empty array
        if (-not $TMActionParams) { $TMActionParams = [System.Collections.ArrayList] @() }
        # }


        ## Assemble the Action Object
        $TMAction = [pscustomobject]@{
            id                     = $null
            name                   = $ActionConfig.ActionName
            description            = $ActionConfig.Description
            debugEnabled           = $false

            methodParams           = ($TMActionParams | ConvertTo-Json -Compress)
            script                 = $ActionScriptCode
            reactionScripts        = '{"PRE":"","ERROR":"// Put the task on hold and add a comment with the cause of the error\n task.error( response.stderr )","FINAL":"","FAILED":"","LAPSED":"","STATUS":"// Check the HTTP response code for a 200 OK \n if (response.status == SC.OK) { \n \t return SUCCESS \n } else { \n \t return ERROR \n}","DEFAULT":"// Put the task on hold and add a comment with the cause of the error\n task.error( response.stderr )\n","STALLED":"","SUCCESS":"// Update Asset Fields\nif(response?.data?.assetUpdates){\n\tfor (field in response.data.assetUpdates) {\n \t\tasset.\"${field.key}\" = field.value;\n\t}\n}\ntask.done()"}'

            provider               = @{
                id   = $null
                name = $ActionConfig.ProviderName
            }
            project                = @{
                id   = $null
                name = $null
            }

            remoteCredentialMethod = 'USER_PRIV'
            credential             = $null

            asyncQueue             = $null

            version                = 1
            dateCreated            = Get-Date
            lastUpdated            = Get-Date

            timeout                = 0
            commandLine            = $null
            dictionaryMethodName   = 'Select...'
            callbackMethod         = $null
            connectorMethod        = $null
            pollingStalledAfter    = 0
            pollingInterval        = 0
            pollingLapsedAfter     = 0
            defaultDataScript      = $null
            useWithTask            = 0
            reactionScriptsValid   = 1
            docUrl                 = ''
            isRemote               = $true
            actionType             = 'POWER_SHELL'
            useWithAsset           = 0
            isPolling              = 0
            endpointUrl            = ''
            apiCatalog             = $null
        }
    }

    ## Ensure the methodParams is a class object
    if ($TMAction.methodParams) {
        $UpdatedMethodParams = @()
        switch ($TMAction.methodParams.GetType().Name) {
            'String' {
                $ConvertedParameters = $TMAction.methodParams | ConvertFrom-Json
                foreach ($ConvertedParameter in $ConvertedParameters) {
                    $UpdatedMethodParams += [TMActionParameter]::new($ConvertedParameter)
                }
            }
            'Object[]' {
                foreach ($MethodParameters in $TMAction.methodParams) {
                    $UpdatedMethodParams += [TMActionParameter]::new($MethodParameters)
                }
                break
            }
            'TMActionParameter' {
                $UpdatedMethodParams = @($TMAction.methodParams)
                break
            }
            Default {
                throw "Action Method parameter propety was an unexpected Type [$($_)] and could not be converted."
            }
        }
        $TMAction.methodParams = $UpdatedMethodParams
    }

    ## Return the Action Object
    return $TMAction

}

function Invoke-TMActionScript {
    <#
    .SYNOPSIS
    Runs the Script Code in the specified TransitionManager Action
 
    .DESCRIPTION
    Runs the Script Code in the specified TransitionManager Action
 
    .PARAMETER TMSession
    Name of an open TMSession. Use Get-TMSession to get open sessions
    .PARAMETER Name
    Name of the action in TM that will be invoked.
 
    .EXAMPLE
    Invoke-TMActionScript -Name 'Ping Remote Machine'
 
    .OUTPUTS
    This command does not output any content
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][String]$Name,
        [Parameter(Mandatory = $false)][PSObject]$Project = $global:TMSessions[$TMSession].UserContext.Project,
        [Parameter(Mandatory = $false)][bool]$Api = $false
    )

    ## Get Session Configuration
    $TMSession = Get-TMSession -Name $TMSession

    if (-not (Test-TMSession $TMSession)) {
        Write-Host 'TMSession: [' -NoNewline
        Write-Host $TMSession -ForegroundColor Cyan
        Write-Host '] was not Found. Please use the New-TMSession command.'
        Throw 'TM Session Not Found. Use New-TMSession command before using features.'
    }

    ## Get the TM Action
    $TMAction = Get-TMAction @PSBoundParameters -Api $Api
    if (-Not $TMAction) {
        throw 'Unable to get the Action'
    }

    ## Try converting and running the script block from the Action
    try {

        ## Create a Temporary Filename
        $TempPsFile = New-TemporaryFile |
            Rename-Item -NewName { $_.FullName -replace '\.tmp', '.ps1' } -PassThru |
            Select-Object -ExpandProperty FullName

        $ActionScriptBlock = [scriptblock]::Create($TMAction.Script)
        Set-Content -Value $ActionScriptBlock.ToString() -Path $TempPsFile -Force -Confirm:$false

        ## Invoke the script block, No New Scope to use the available variables,
        ## and with Debug enabled to allow stepping into the file via debugging mode
        try {
            $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
            & ($TempPsFile)
            $Stopwatch.Stop()
            Write-Host -Message "Task $($Name) completed in $(Get-TimeSpanString -Timespan $Stopwatch.Elapsed)"
        } catch {
            $Stopwatch.Stop()
            Write-Warning -Message "Task $($Name) completed (with errors) in $(Get-TimeSpanString -Timespan $Stopwatch.Elapsed)" -WarningAction Continue
            throw $_
        } finally {
            Remove-Item $TempPsFile -Force
        }
    } catch {
        throw $_
    }
}