Drm.Templates.Powershell.psm1

#Dot source all functions in all ps1 files located in the module's public and private folders, excluding tests and profiles.
Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -Exclude *.tests.ps1, *profile.ps1 -ErrorAction SilentlyContinue |
ForEach-Object {
    . $_.FullName
}

function Set-SolutionCloudflowsState{
    <#
    .SYNOPSIS
        Connect to Dataverse and bulk turn off/on cloudflows associated with a specific solution.
 
    .DESCRIPTION
        Connect to a Dataverse instance using Connect-CrmOnline. Provide the solution name where
        cloudflows have been configured and that you want to turn off or on.
 
    .NOTES
        Author : Danny Styles
     
    .PARAMETER RequiredState
        The required state for the workflows.
     
    .PARAMETER SolutionName
        The unique name of the solution.
 
    .EXAMPLE
        Set-SolutionCloudflowsState -RequiredState On -SolutionName drmCloudflowsSolution
    #>


    [CmdletBinding()]  
    PARAM 
    ( 
    [Parameter(Mandatory=$true)][ValidateSet('On','Off')][String]$RequiredState,
    [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][String]$SolutionName
    ) 

    # Check Microsoft.Xrm.Data.PowerShell has been imported first
    $m = Get-Module -Name 'Microsoft.Xrm.Data.PowerShell' | measure

    if($m.Count -ne 1)
    {
        Write-Warning "Please import Microsoft.Xrm.Data.PowerShell to use this command."
        throw "Microsoft.Xrm.Data.PowerShell is required."
    }

    # Check connection has been made to dataverse env.
    if ($null -eq $conn) 
    { 
        Write-Error "Please use Connect-CrmOnline to connect to a dataverse environment first." 
        throw "No connection to Dataverse environment."
    }

    # Prepare...
    Write-Output "Required state: $requiredState ... getting cloudflow information"
    switch ($requiredState) {
        "on" {
            $statecode=0
            $message = "contains no flows in the OFF state - nothing to do"
        }
        "off"{
            $statecode=1
            $message = "contains no flows in the ON state - nothing to do"
        }  
        Default{}
    }

    # the $statecode from above and solutionName are passed into the fetchXML below for our query
    # Get the flows that are turned off/on
    $fetchFlows = @"
        <fetch>
            <entity name='workflow'>
            <attribute name='category' />
            <attribute name='name' />
            <attribute name='statecode' />
            <filter>
                <condition attribute='category' operator='eq' value='5' />
                <condition attribute='statecode' operator='eq' value='$($statecode)' />
            </filter>
            <link-entity name='solutioncomponent' from='objectid' to='workflowid'>
                <link-entity name='solution' from='solutionid' to='solutionid'>
                <filter>
                    <condition attribute='uniquename' operator='eq' value='$($SolutionName)' />
                </filter>
                </link-entity>
            </link-entity>
            </entity>
        </fetch>
"@
;
    # perform the query
    $flows = (Get-CrmRecordsByFetch  -conn $conn -Fetch $fetchFlows -Verbose).CrmRecords
    
    # if nothing comes back.. exit
    if ($flows.Count -eq 0) {
        Write-Host "##vso[task.logissue type=warning]No Flows that are turned $requiredState in '$SolutionName'."
        Write-Output "'$SolutionName' $message"
        exit(0)
    }        
    
    # Turn on/off flows
    foreach ($flow in $flows) {
        $flowname = $flow.name
        Write-Output "Turning $requiredState Flow:$flowname"
        switch ($requiredState) {
            "on" {
                Set-CrmRecordState -EntityLogicalName workflow -Id $flow.workflowid -StateCode Activated -StatusCode Activated -Verbose 
            }
            "off" {
                Set-CrmRecordState -EntityLogicalName workflow -Id $flow.workflowid -StateCode Draft -StatusCode Draft -Verbose 
            
            }
            Default {}
        } 
    }
}

function Set-CloudflowsOwner {
    <#
    .SYNOPSIS
        Update the owner of all cloudflows in a Dynamics environment.
 
    .DESCRIPTION
        Connect to a Dataverse instance using Connect-CrmOnline and provide
        the full name of a system user account to update all workflows to a new owner.
 
    .NOTES
        Author : Danny Styles
 
    .PARAMETER NewFlowOwner
        The full name of a system user account.
 
    .EXAMPLE
        Set-CloudflowsOwner -NewFlowOwner "Danny Styles"
    #>


    [CmdletBinding()]    
    PARAM (
    [Parameter(Mandatory=$true)] [string]$NewFlowOwner
    )

    # Check connection has been made to dataverse env.
    if ($null -eq $conn) 
    { 
        Write-Error "Please use Connect-CrmOnline to connect to a dataverse environment first." 
        throw "No connection to Dataverse environment."
    }

    $DataverseEnvUrl = $conn.CrmConnectOrgUriActual.Scheme+"://"+$conn.CrmConnectOrgUriActual.Host

    ##########################################################
    # Call Dataverse WebAPI using Authentication Token
    ##########################################################

    # Parameters for the Dataverse WebAPI call which includes our header
    # that carries the access token.
    $apiCallParams =
    @{
        URI = $DataverseEnvUrl+ "/api/data/v9.2/systemusers"
        Headers = @{
            "Authorization" = "Bearer $($conn.CurrentAccessToken)";
            "Content-Type" = "application/json"; 
            "Accept" = "application/json";
            "Prefer" = "odata.include-annotations="*"";
        }
        Method = 'GET'
    }

    # Call the Dataverse WebAPI
    $apiCallRequest = Invoke-RestMethod @apiCallParams -ErrorAction Stop
    $SystemUser = $apiCallRequest.value | where-object fullname -eq 'SYSTEM'
    $NewUser = $apiCallRequest.value | where-object fullname -eq "$NewFlowOwner"

    $apiCallParams =
    @{
        URI = $dataverseEnvUrl+ '/api/data/v9.2/workflows?$filter=category eq 5&$expand=ownerid,owninguser($select=fullname)'
        Headers = @{
            "Authorization" = "Bearer $($conn.CurrentAccessToken)";
            "Content-Type" = "application/json"; 
            "Accept" = "application/json";

        }
        Method = 'GET'
    }
    $apiCallRequest = Invoke-RestMethod @apiCallParams -ErrorAction Stop
    $workflows = $apiCallRequest.value | where-object _owninguser_value -ne $SystemUser.systemuserid
    $workflows = $workflows | where-object _owninguser_value -ne $NewUser.systemuserid

    foreach($Flow in $workflows){
        $uri = $dataverseEnvUrl+ '/api/data/v9.2/workflows(' + $flow.workflowid  + ')'
        $apiCallParams =
        @{
            URI =    $Uri
            Headers = @{
                "Authorization" = "Bearer $($conn.CurrentAccessToken)";
                "Content-Type" = "application/json"; 
                "Accept" = "application/json";

            }
            Method = 'PATCH'
        }

        $params = @{
        "ownerid@odata.bind"="/systemusers(" + $NewUser.systemuserid + ")"
        } | ConvertTo-Json

        write-host "Updating " $Flow.name
        $apiCallRequest = Invoke-RestMethod @apiCallParams -Body $params -ErrorAction Stop
    }
}

function Get-DynamicsAutoNumber{
    <#
    .SYNOPSIS
    Connect to Dataverse and Get the value needed for a seed autonumber field this is to
    be run before release, then call the function Set-DynamicsAutoNumber to set the field after release
 
    .NOTES
    Author : Dave Langan
     
    .PARAMETER $TenantID
    The tenant ID for the environment
     
    .PARAMETER $appId
    The Application (client) ID of the App registration
 
    .PARAMETER $clientSecret
    The client secret generated within the App registration
 
    .PARAMETER $DataverseEnvUrl
    The url of the Dataverse environment you want to connect to
 
    .PARAMETER $EntityName
    The name of the entity in the dataverse to use
 
    .PARAMETER $FieldName
    The name of the field to get the next autonumber for
 
    .PARAMETER $VarName
    The name of the variable to set in ADO with the Seed value which is incremented by 1
    #>


    [CmdletBinding()]    
    PARAM (
    [Parameter(Mandatory=$true)] [string]$TenantId,
    [Parameter(Mandatory=$true)] [string]$ClientId,
    [Parameter(Mandatory=$true)] [string]$ClientSecret,
    [Parameter(Mandatory=$true)] [string]$DataverseEnvUrl,
    [Parameter(Mandatory=$true)] [string]$EntityName, 
    [Parameter(Mandatory=$true)] [string]$FieldName, 
    [Parameter(Mandatory=$true)] [string]$VarName 
    )

    $appId = $ClientId

    $oAuthTokenEndpoint = 'https://login.microsoftonline.com/' + $TenantId + '/oauth2/v2.0/token'
    
    $appId = $ClientId


    ##########################################################
    # Access Token Request
    ##########################################################

    # OAuth Body Access Token Request
    $authBody = 
    @{
        client_id = $appId;
        client_secret = $clientSecret;    
        # The v2 endpoint for OAuth uses scope instead of resource
        scope = "$($dataverseEnvUrl)/.default"    
        grant_type = 'client_credentials'
    }

    # Parameters for OAuth Access Token Request
    $authParams = 
    @{
        URI = $oAuthTokenEndpoint
        Method = 'POST'
        ContentType = 'application/x-www-form-urlencoded'
        Body = $authBody
    }

    # Get Access Token
    $authRequest = Invoke-RestMethod @authParams -ErrorAction Stop
    $authResponse = $authRequest

    ##########################################################
    # Call Dataverse WebAPI using Authentication Token
    ##########################################################

    # Params related to the Dataverse WebAPI call you will be making.
    # These need to be in single quotes to ensure they are not expanded.
    $uriParams = '$select=DisplayName,Settings'

    # Parameters for the Dataverse WebAPI call which includes our header
    # that carries the access token.
    $apiCallParams =
    @{
        URI = "$($dataverseEnvUrl)/api/data/v9.2/GetNextAutoNumberValue"
        Headers = @{
            "Authorization" = "$($authResponse.token_type) $($authResponse.access_token)";
            "Content-Type" = "application/json"; 
        }
        Method = 'POST'
    }

    $params = @{
        "EntityName"="$EntityName"
        "AttributeName"="$FieldName"
    # "Value"=1002

    } | ConvertTo-Json

    # Call the Dataverse WebAPI
    $apiCallRequest = Invoke-RestMethod @apiCallParams -Body $params -ErrorAction Stop
    $apiCallResponse = $apiCallRequest

    #Output the results
    Write-Host "Seed value is set to " $apiCallResponse.NextAutoNumberValue.ToString() 

    $SeedNumber1 = $apiCallResponse.NextAutoNumberValue.tostring()
    [int]$SeedNumber = $SeedNumber1 + 1
    Write-Host ("##vso[task.setvariable variable=$VarName;isOutput=true;]$SeedNumber")
    Write-Host "##vso[task.setvariable variable=Seed;]$SeedNumber"
    return $SeedNumber1
}

function Set-DynamicsAutoNumber{
    <#
    .SYNOPSIS
    Connect to Dataverse and Set the Seed value for an autonumber field this is to
    be run after release, it useses the ADO variable created by Get-DynamicsAutoNumber
 
    .NOTES
    Author : Dave Langan
     
    .PARAMETER $TenantID
    The tenant ID for the environment
     
    .PARAMETER $appId
    The Application (client) ID of the App registration
 
    .PARAMETER $clientSecret
    The client secret generated within the App registration
 
    .PARAMETER $DataverseEnvUrl
    The url of the Dataverse environment you want to connect to
 
    .PARAMETER $EntityName
    The name of the entity in the dataverse to use
 
    .PARAMETER $FieldName
    The name of the field to set the next autonumber seed value for
 
    .PARAMETER $SeedValue
    The name of the variable to use in ADO to set the seed value populated by the script Dynamicsautonumberget.ps1
    #>


    [CmdletBinding()]    
    PARAM (
    [Parameter(Mandatory=$true)] [string]$TenantId,
    [Parameter(Mandatory=$true)] [string]$ClientId,
    [Parameter(Mandatory=$true)] [string]$ClientSecret,
    [Parameter(Mandatory=$true)] [string]$DataverseEnvUrl,
    [Parameter(Mandatory=$true)] [string]$EntityName, 
    [Parameter(Mandatory=$true)] [string]$FieldName, 
    [Parameter(Mandatory=$true)] [int]$SeedValue 
    )

    $appId = $ClientId


    $oAuthTokenEndpoint = 'https://login.microsoftonline.com/' + $TenantId + '/oauth2/v2.0/token'
    
    $appId = $ClientId

    ##########################################################
    # Access Token Request
    ##########################################################

    # OAuth Body Access Token Request
    $authBody = 
    @{
        client_id = $appId;
        client_secret = $clientSecret;    
        # The v2 endpoint for OAuth uses scope instead of resource
        scope = "$($dataverseEnvUrl)/.default"    
        grant_type = 'client_credentials'
    }

    # Parameters for OAuth Access Token Request
    $authParams = 
    @{
        URI = $oAuthTokenEndpoint
        Method = 'POST'
        ContentType = 'application/x-www-form-urlencoded'
        Body = $authBody
    }

    # Get Access Token
    $authRequest = Invoke-RestMethod @authParams -ErrorAction Stop
    $authResponse = $authRequest

    ##########################################################
    # Call Dataverse WebAPI using Authentication Token
    ##########################################################

    # Params related to the Dataverse WebAPI call you will be making.
    # These need to be in single quotes to ensure they are not expanded.
    $uriParams = '$select=DisplayName,Settings'

    # Parameters for the Dataverse WebAPI call which includes our header
    # that carries the access token.
    $apiCallParams =
    @{
        URI = "$($dataverseEnvUrl)/api/data/v9.2/SetAutoNumberSeed"
        Headers = @{
            "Authorization" = "$($authResponse.token_type) $($authResponse.access_token)";
            "Content-Type" = "application/json"; 
        }
        Method = 'POST'
    }

    $params = @{
        "EntityName"="$EntityName"
        "AttributeName"="$FieldName"
        "Value"= $SeedValue

    } | ConvertTo-Json

    # Call the Dataverse WebAPI
    $apiCallRequest = Invoke-RestMethod @apiCallParams -Body $params -ErrorAction Stop
    $apiCallResponse = $apiCallRequest

    #Output the results
    Write-Host "Seed value is set to $SeedValue for field $FieldName in entity $EntityName"
}


function Set-SolutionEnvironmentVariables{
    <#
    .SYNOPSIS
        Set environment variable values directly in an unpacked solution.
    .DESCRIPTION
        Update the current or default value of an environment variable definition in an unpacked solution,
        using the provided schemaname. If the variable does not have a current or default value,
        it will be added as the default value.
    .PARAMETER UnpackedSolutionFolder
        Required: Location of the unpakced solution
    .PARAMETER VariableReplacements
        A Hashtable of variable replacements e.g. { <schemaname> = <value>;}
    .PARAMETER JsonVariableReplacements
        Location of a .json file with variable replacements
   .EXAMPLE
        Set-SolutionEnvironmentVariables -UnpackedSolutionFolder C:\drmdemo\unpackedSolution -VariableReplacements @{ "new_helloDrm" = "example"; "new_exampleSchemaName"= "Example2"}
   .EXAMPLE
        Set-SolutionEnvironmentVariables -UnpackedSolutionFolder C:\drmdemo\unpackedSolution -JsonVariableReplacements C:\drmdemo\variablereplacements.json
    #>


    [CmdletBinding()]    
    PARAM(
        [parameter(Position=1, Mandatory=$true)]
        [ValidateScript({
            if(-NOT ($_ | Test-Path) ){
                throw "Cannot find unpacked solution at $_"
            }
            return $true 
        })]
        [System.IO.FileInfo]$UnpackedSolutionFolder,    
        [parameter(Position=2, Mandatory=$false,ParameterSetName="hashtable")]
        [Hashtable]$VariableReplacements,
        [parameter(Position=3, Mandatory=$false,ParameterSetName="json")]
        [ValidateScript({
            if(-Not ($_ | Test-Path) ){
                throw "File or folder does not exist" 
            }
            if(-Not ($_ | Test-Path -PathType Leaf) ){
                throw "The Path argument must be a file. Folder paths are not allowed."
            }
            return $true 
        })]
        [System.IO.FileInfo]$JsonVariableReplacements
    )

    # get schema definitions from folder names
    $variableSchemaNames = Get-ChildItem -Path $UnpackedSolutionFolder/environmentvariabledefinitions -Recurse -Directory -Force | Select -ExpandProperty Name

    # use the hashtable or json file and update the entries.
    if($variableSchemaNames.count -ne 0){
        
        if($JsonVariableReplacements){
            # read json into $VariableReplacements
            Write-Host "Reading variable replacements from json file..."
            $jsonReplacements = Get-Content -Raw -Path $JsonVariableReplacements | ConvertFrom-Json
        
            $VariableReplacements = @{}
            $jsonReplacements.psobject.properties | foreach{$VariableReplacements[$_.Name]= $_.Value}
        } else {
            Write-Host "Reading variable replacements from object..."
        }

        foreach($envReplacementKey in $VariableReplacements.keys){
            # find env definition in $variableSchemaNames list
            if($variableSchemaNames.Contains($envReplacementKey)){
                Write-Host "`nUpdating environment variable definition: '$($envReplacementKey)'"

                $envvarPath = "$UnpackedSolutionFolder/environmentvariabledefinitions/$envReplacementKey/environmentvariablevalues.json"
                $envdefinitionPath = "$UnpackedSolutionFolder/environmentvariabledefinitions/$envReplacementKey/environmentvariabledefinition.xml"

                # check for json file, if exists, update it.
                if(Test-Path -Path $envvarPath -PathType Leaf){
                  $json = Get-Content -Raw -Path $envvarPath | ConvertFrom-Json
                  Write-Host "...changing current value from : '$($json.environmentvariablevalues.environmentvariablevalue.value)' to '$($VariableReplacements[$envReplacementKey])'"
                  $json.environmentvariablevalues.environmentvariablevalue.value = $VariableReplacements[$envReplacementKey]
                  $json | ConvertTo-Json -depth 100 | Set-Content $envvarPath
                } elseif(Test-Path -Path $envdefinitionPath -PathType Leaf){

                  [xml]$xmlElm = Get-Content -Path $envdefinitionPath
                  
                  $testDefaultNodeExists = $xmlElm.SelectSingleNode("./environmentvariabledefinition/defaultvalue")

                  if($testDefaultNodeExists){
                    Write-Host "...changing default value from: '$($xmlElm.environmentvariabledefinition.defaultvalue)' to '$($VariableReplacements[$envReplacementKey])'"

                    $xmlElm.environmentvariabledefinition.defaultvalue = $VariableReplacements[$envReplacementKey]
                    $xmlElm.Save($envdefinitionPath)
                  } else {
                    Write-Host "...adding default value: '$($VariableReplacements[$envReplacementKey])'"

                    $child = $xmlElm.CreateElement("defaultvalue")
                    $out = $xmlElm.DocumentElement.AppendChild($child)
                    $xmlElm.environmentvariabledefinition.defaultvalue = $VariableReplacements[$envReplacementKey]
                    $xmlElm.Save($envdefinitionPath)
                  }
                }
            } else {
                Write-Host "`nNo environment variable definition was found in the solution for: '$($envReplacementKey)'"
            }
        }
    } else {
        throw "No variable definitions found. Please check the unpacked solution contains environmentvariable definitions."
    }
}

function New-DrmTemplate{
    <#
    .SYNOPSIS
        Generates a new DRM Template.
    .DESCRIPTION
        Connect to a Dynamics environment and use the Web API to build a barebones DRM template for use in automation.
    .PARAMETER Url
        Optional: [string] The Dynamics environment to connect.
    .PARAMETER EntityName
        Required: Select the entity you want to target here https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/entitytypes?view=dataverse-latest
    .PARAMETER Filter
        Optional: Add your web api filter for example '$select=name'
    .PARAMETER SetupTemplateForAutomation
        Optional: If set, it will generate the template for connecting to a dynamics environment using application credentials.
    .PARAMETER SubscriptionId
        Required: Enter your subscrtiption id.
    .PARAMETER OutputToFile
        Optional: Custom path of location to save the template
    .EXAMPLE
        New-DrmTemplate -Url https://demo.crm11.com -Entity queues -Filter '$select=name' -SetupTemplateForAutomation -SubscriptionId 'xxxxxx'
    #>

    [CmdletBinding()]
    PARAM(
        [parameter(Position=1, Mandatory=$false)]
        [ValidatePattern('([\w-]+).crm([0-9]*).(microsoftdynamics|dynamics|crm[\w-]*).(com|de|us|cn)')]
        [string]$Url,
        [parameter(Position=2, Mandatory=$true)]
        [string]$EntityName, 
        [parameter(Position=3, Mandatory=$false)]
        [string]$Filter,
        [parameter(Position=4, Mandatory=$false)]
        [switch]$SetupTemplateForAutomation,
        #[parameter(Position=5, Mandatory=$false)]
        #[string]$SubscriptionId,
        [parameter(Position=6, Mandatory=$false)]
        [ValidateScript({
            if(-Not ($_.DirectoryName | Test-Path) ){
                throw "Folder location does not exist"
            }
            if((Get-Item -Path $_).PSIsContainer) {
                throw "You must include the file name e.g. 'template.json'"
            }
            return $true 
        })]
        [System.IO.FileInfo]$OutputToFile
        #[parameter(Position=7, DontShow=$true)]
        #[switch]$UseBetaEnvironment
    )

    #DynamicParam {
    # if ([string]::IsNullOrEmpty($drm.SubscriptionId)) {
    # $subAttribute = New-Object System.Management.Automation.ParameterAttribute
    # $subAttribute.Position = 5
    # $subAttribute.Mandatory = $true
    #
    # $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    # $attributeCollection.Add($subAttribute)
    # $SubscriptionId = New-Object System.Management.Automation.RuntimeDefinedParameter('SubscriptionId', [string], $attributeCollection)
    # $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    # $paramDictionary.Add('SubscriptionId', $SubscriptionId)
    # return $paramDictionary
    # } else {
    # $SubscriptionId = $drm.SubscriptionId
    # }
    #}

    process {

        if($conn.CurrentAccessToken) {
                           
            [hashtable]$postParams = @{}

            if($Url) {
                $postParams.Add('url', $Url)
                Write-Host("Connecting to Dynamics Instance: " + $Url)
            }
            else {
                # try and get the url from the connection.
                if($conn.ConnectedOrgPublishedEndpoints.Get_Item("WebApplication")) {
                    $dynamicsUrl = $conn.ConnectedOrgPublishedEndpoints.Get_Item("WebApplication")

                    $postParams.Add('url', $dynamicsUrl)
                    Write-Host("Connecting to Dynamics Instance: " + $dynamicsUrl)
                }
                else {
                    throw "Unable to set the Dynamics url to fetch the data, published endpoint web application not available in connection details."
                }
            }

            $postParams.Add('entityname', $EntityName)
            Write-Verbose "Entityname set to $EntityName" 

            if($Filter) {
                $postParams.Add('filter', $Filter)
                Write-Verbose "Filter set to $Filter" 
            }

            if($SetupTemplateForAutomation.IsPresent) {
                $postParams.Add('setupTemplateForAutomation', $true)
                Write-Verbose "SetupTemplateForAutomation set to 'true'"
            } 
            else {
                $postParams.Add('setupTemplateForAutomation', $false)
                Write-Verbose "SetupTemplateForAutomation set to 'false'"
            }

            $postParams.Add('token', $conn.CurrentAccessToken)
            Write-Verbose "Using token from Connect-CrmOnline connection object."

            try
            {
                Write-Host("Generating Template...")
                    
                $verboseLogging = $false

                if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent)
                {
                    $verboseLogging = $true
                }

                $templateManager= New-Object drm.Powershell.DrmTemplates.PowershellGenerateTemplate -ArgumentList $verboseLogging

                $response = $templateManager.GenerateTemplateAsync(($postParams|ConvertTo-Json)).GetAwaiter().GetResult() | ConvertFrom-Json

                if($response.Data.Template) {

                    if($OutputToFile) {
                        $templateManager.WriteTemplateToFile($OutputToFile)
                        
                        Write-Host "Template created at " $OutputToFile
                    }
                    else {
                        $pathInfo = Get-Location

                        $outputPath = Join-Path -Path $pathInfo.Path -ChildPath "\template.json"

                        $templateManager.WriteTemplateToFile($outputPath)
                        Write-Host "Template created at " $outputPath
                    }
                }
                else {
                    $JoinedString = $response.Error -join ","
                    Write-Error $JoinedString
                }

            }
            catch {
                throw $_
            }
        }
        else {
            throw "No connection to CRM online. Please connect to a Dynamics environment using the 'Connect-CrmOnline' cmdlet."
        }
    }
}

function Connect-CrmOnline{
    [CmdletBinding()]
    PARAM( 
        [parameter(Position=1, Mandatory=$true, ParameterSetName="connectionstring")]
        [string]$ConnectionString, 
        [parameter(Position=1, Mandatory=$true, ParameterSetName="Secret")]
        [Parameter(Position=1,Mandatory=$true, ParameterSetName="Creds")]
        [Parameter(Position=1,Mandatory=$true, ParameterSetName="NoCreds")]
        [ValidatePattern('([\w-]+).crm([0-9]*).(microsoftdynamics|dynamics|crm[\w-]*).(com|de|us|cn)')]
        [string]$ServerUrl, 
        [parameter(Position=2, Mandatory=$true, ParameterSetName="Creds")]
        [PSCredential]$Credential,
        [Parameter(Position=4,Mandatory=$false, ParameterSetName="Creds")]
        [Parameter(Position=3,Mandatory=$false, ParameterSetName="NoCreds")]
        [switch]$ForceOAuth,
        [parameter(Position=2, Mandatory=$true, ParameterSetName="Secret")]
        [Parameter(Position=5,Mandatory=$false, ParameterSetName="Creds")]
        [Parameter(Position=4,Mandatory=$false, ParameterSetName="NoCreds")]
        [ValidateScript({
            try {
                [System.Guid]::Parse($_) | Out-Null
                $true
            } catch {
                $false
            }
        })]
        [string]$OAuthClientId,
        [parameter(Position=3, Mandatory=$false, ParameterSetName="Secret")]
        [Parameter(Position=6,Mandatory=$false, ParameterSetName="Creds")]
        [Parameter(Position=5,Mandatory=$false, ParameterSetName="NoCreds")]
        [string]$OAuthRedirectUri, 
        [parameter(Position=4, Mandatory=$true, ParameterSetName="Secret")]
        [string]$ClientSecret, 
        [parameter(Position=5, Mandatory=$false, ParameterSetName="NoCreds")]
        [string]$Username, 
        [int]$ConnectionTimeoutInSeconds,
        [string]$LogWriteDirectory, 
        [switch]$BypassTokenCache,
        [parameter(Position=7, Mandatory=$false, ParameterSetName="Interactive")]
        [switch]$InteractiveMode
    )

    if($InteractiveMode){
        $global:conn = Get-CrmConnection -InteractiveMode
        return $global:conn
    }

    if(-not [string]::IsNullOrEmpty($ServerUrl) -and $ServerUrl.StartsWith("https://","CurrentCultureIgnoreCase") -ne $true){
        Write-Verbose "ServerUrl is missing https, fixing URL: https://$ServerUrl"
        $ServerUrl = "https://" + $ServerUrl
    }

    #starting default connection string with require new instance and server url
    $cs = "RequireNewInstance=True"
    $cs += ";Url=$ServerUrl"
    if($BypassTokenCache){
        $cs += ";TokenCacheStorePath="
    }

    if($ConnectionTimeoutInSeconds -and $ConnectionTimeoutInSeconds -gt 0){
        $newTimeout = New-Object System.TimeSpan -ArgumentList 0,0,$ConnectionTimeoutInSeconds
        Write-Verbose "Setting new connection timeout of $newTimeout"
        #set the timeout on the MaxConnectionTimeout static
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient]::MaxConnectionTimeout = $newTimeout
    }

    if($ConnectionString){
        if(!$ConnectionString -or $ConnectionString.Length -eq 0){
            throw "Cannot create the CrmServiceClient, the connection string is null"
        }
        Write-Verbose "ConnectionString provided - skipping all helpers/known parameters"
        
        $global:conn = New-Object Microsoft.Xrm.Tooling.Connector.CrmServiceClient -ArgumentList $ConnectionString
        if($global:conn){
            ApplyCrmServiceClientObjectTemplate($global:conn)  #applyObjectTemplateFormat
        }
        return $global:conn
    }
    elseif($ClientSecret){
        $cs += ";AuthType=ClientSecret"
        $cs += ";ClientId=$OAuthClientId"
        if(-not [string]::IsNullOrEmpty($OAuthRedirectUri)){
            $cs += ";redirecturi=$OAuthRedirectUri"
        }
        $cs += ";ClientSecret='$ClientSecret'"
        Write-Verbose ($cs.Replace($ClientSecret, "*******"))
        try
        {
            if(!$cs -or $cs.Length -eq 0){
                throw "Cannot create the CrmServiceClient, the connection string is null"
            }

            #$global:conn = [Microsoft.Xrm.Tooling.Connector.CrmServiceClient]::new($cs)
            $global:conn = Get-CrmConnection -ConnectionString $cs
            
            #ApplyCrmServiceClientObjectTemplate($global:conn) #applyObjectTemplateFormat
            $global:conn
            return
        }
        catch
        {
            throw $_
        }   
    }
    else{
        if(-not [string]::IsNullOrEmpty($Username) -and $ForceOAuth -eq $false){
            $cs += ";Username=$UserName"
            Write-Warning "UserName parameter is only compatible with oAuth, forcing auth mode to oAuth"
            $ForceOAuth = $true
        }
        #Default to Office365 Auth, allow oAuth to be used
        if(!$OAuthClientId -and !$ForceOAuth){
            Write-Verbose "Using AuthType=Office365"
            if(-not $Credential){
                #user did not provide a credential
                Write-Warning "Cannot create the CrmServiceClient, no credentials were provided. Credentials are required for an AuthType of Office365."
                $Credential = Get-Credential 
                if(-not $Credential){
                    throw "Cannot create the CrmServiceClient, no credentials were provided. Credentials are required for an AuthType of Office365."
                }
            }
            $cs+= ";AuthType=Office365"
            $cs+= ";Username=$($Credential.UserName)"
            $cs+= ";Password='$($Credential.GetNetworkCredential().Password)'"
        }
        elseif($ForceOAuth){
            #use oAuth if requested -ForceOAuth
            Write-Verbose "Params Provided -> ForceOAuth: {$ForceOAuth} ClientId: {$OAuthClientId} RedirectUri: {$OAuthRedirectUri}"
            #try to use the credentials if they're provided
            if($Credential){
                Write-Verbose "Using provided credentials for oAuth"
                $cs+= ";Username=$($Credential.UserName)"
                $cs+= ";Password='$($Credential.GetNetworkCredential().Password)'"
            }else{
                Write-Verbose "No credential provided, attempting single sign on with no credentials in the connectionstring"
            }

            if($OAuthClientId){
                #use the clientid if provided, else use a provided clientid
                Write-Verbose "Using provided oAuth clientid"
                $cs += ";AuthType=OAuth;ClientId=$OAuthClientId"
                if($OAuthRedirectUri){
                    $cs += ";redirecturi=$OAuthRedirectUri"
                }
            }
            else{
                #else fallback to a known clientid
                $cs+=";AuthType=OAuth;ClientId=2ad88395-b77d-4561-9441-d0e40824f9bc"
                $cs+=";redirecturi=app://5d3e90d6-aa8e-48a8-8f2c-58b45cc67315"
            }
        }

        try
        {
            if(!$cs -or $cs.Length -eq 0){
                throw "Cannot create the CrmServiceClient, the connection string is null"
            }
            #log the connection string to be helpful
            $loggedConnectionString = $cs
            if($Credential){
                $loggedConnectionString = $cs.Replace($Credential.GetNetworkCredential().Password, "*******") 
            }
            Write-Verbose "ConnectionString:{$loggedConnectionString}"

            #$global:conn = New-Object Microsoft.Xrm.Tooling.Connector.CrmServiceClient -ArgumentList $cs
            $global:conn = Get-CrmConnection -ConnectionString $cs

            #ApplyCrmServiceClientObjectTemplate($global:conn) #applyObjectTemplateFormat

            if($global:conn.LastCrmError -and $global:conn.LastCrmError -match "forbidden with client authentication scheme 'Anonymous'"){
                Write-Error "Warning: Exception encountered when authenticating, if you're using oAuth you might want to include the -username paramter to disambiguate the identity used for authenticate"
            }

            return $global:conn
        }
        catch
        {
            throw $_
        }  
    }
}