Handy.Crm.Powershell.Cmdlets.psm1

# Enabling TLS 1.2
# More details: https://blogs.msdn.microsoft.com/crm/2017/09/28/updates-coming-to-dynamics-365-customer-engagement-connection-security/
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

function Assert-CRMOrganizationResponse {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true)]
        [Microsoft.Xrm.Sdk.Messages.ExecuteMultipleResponse]$Response
    )

    Begin {}
    Process {
        if ($Response.IsFaulted -eq $true) {
            Write-Verbose -Message "ExecuteMultiple finished with faults"

            $message = "ExecuteMultpleResponse finished with fault."
            foreach ($r in $Response.Responses) {
                if ($null -ne $r.Fault) {
                    $message += "`r`n$($r.RequestIndex): $($r.Fault.Message)"
                }
            }
            throw $message
        }
        else {
            Write-Verbose -Message "ExecuteMultiple finished without faults"
        }
    }
    End {}
}


function Get-CRMOptionSetValue {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.OptionSetValue])]
    Param(
        [Parameter(Mandatory=$true)]
        [int]$Value
    )

    Begin {}
    Process {
        $optionSetValue = New-Object -TypeName 'Microsoft.Xrm.Sdk.OptionSetValue' -ArgumentList $Value

        $optionSetValue
    }
    End {}
}


function Get-CRMEntityMetadata {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Metadata.EntityMetadata])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$LogicalName,

        [Parameter(Mandatory=$false)]
        [Microsoft.Xrm.Sdk.Metadata.EntityFilters]$EntityFilters = [Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Default,

        [Parameter(Mandatory=$false)]
        [switch]$RetrieveAsIfPublished
    )

    Begin {}
    Process {
        $parameters = @{}

        $parameters['EntityFilters'] = $EntityFilters
        $parameters['RetrieveAsIfPublished'] = $RetrieveAsIfPublished.IsPresent

        if($PSBoundParameters.ContainsKey('LogicalName')) {
            $parameters['LogicalName'] = $LogicalName
            $parameters['MetadataId'] = [guid]::Empty

            $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveEntity' -Parameters $parameters
        }
        else {
            $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveAllEntities' -Parameters $parameters
        }

        $response['EntityMetadata']
    }
    End {}
}


function Get-CRMEntityReference {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.EntityReference])]
    Param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$EntityName,

        [Parameter(Mandatory=$true)]
        [guid]$Id
    )

    Begin {}
    Process {
        $entityReference = New-Object -TypeName 'Microsoft.Xrm.Sdk.EntityReference' -ArgumentList $EntityName, $Id

        $entityReference
    }
    End {}
}


function Merge-CRMAttribute {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [hashtable]$From,

        [Parameter(Mandatory=$true)]
        [hashtable]$To
    )

    foreach ($key in $From.Keys)
    {
        $To[$key] = $From[$key]
    }
}


function Add-CRMPrivilegesRole {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [guid]$RoleId,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [Microsoft.Crm.Sdk.Messages.RolePrivilege[]]$Privileges
    )

    $parameters = @{
        RoleId = $RoleId;
        Privileges = $Privileges
    }

    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'AddPrivilegesRole' -Parameters $parameters
    # $response
}


function Remove-CRMPrivilegeRole {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [guid]$RoleId,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [guid]$PrivilegeId
    )

    $parameters = @{
        RoleId = $RoleId;
        PrivilegeId = $PrivilegeId
    }

    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RemovePrivilegeRole' -Parameters $parameters
    # $response
}


function Get-CRMPrivilege {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Entity])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true,
            ParameterSetName='Id')]
        [ValidateNotNull()]
        [guid]$Id,

        [Parameter(Mandatory=$true,
            ParameterSetName='Name')]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" count="1">
  <entity name="privilege">
    <all-attributes />
    <filter type="and">
      {0}
    </filter>
  </entity>
</fetch>
"@


    switch ($PSCmdlet.ParameterSetName) {
        'Id' {
            $condition = '<condition attribute="privilegeid" operator="eq" value="{0}" />' -f $Id
        }

        'Name' {
            $condition = '<condition attribute="name" operator="eq" value="{0}" />' -f $Name
        }
    }

    $privilege = (Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $condition)) | Select-Object -Index 0
    $privilege
}


function Get-CRMRole {
    <#
    .DESCRIPTION
    Without All switch it returns only root role, which is usually used for editing roles.
    With All switch present it returns all roles it can find in organization.
 
    .LINK
    Get-CRMConnection
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Entity])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [switch]$All
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical">
  <entity name="role">
    <all-attributes />
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}" />
      <condition attribute="parentroleid" operator="null" />
    </filter>
  </entity>
</fetch>
"@


    if ($All) {
        $fetchXml = $fetchXml.Replace('<condition attribute="parentroleid" operator="null" />', '')
    }

    $roles = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $Name)

    $roles
}


function Get-CRMRolePrivilege {
    [CmdletBinding()]
    [OutputType([Microsoft.Crm.Sdk.Messages.RolePrivilege])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Crm.Sdk.Messages.PrivilegeDepth]$Depth,

        [Parameter(Mandatory=$true)]
        [guid]$Id

    )

    Begin {}
    Process {
        $rolePrivilege = New-Object -TypeName 'Microsoft.Crm.Sdk.Messages.RolePrivilege' -ArgumentList $Depth, $Id

        $rolePrivilege
    }
    End {}
}


function Get-CRMSolution {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Entity])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" count="1">
  <entity name="solution">
    <all-attributes />
    <filter type="and">
      <condition attribute="uniquename" operator="eq" value="{0}" />
    </filter>
  </entity>
</fetch>
"@


    $solution = (Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $Name)) | Select-Object -Index 0
    $solution
}


function Set-CRMSDKStepState {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [guid]$SolutionId,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Enabled', 'Disabled')]
        [string]$State,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$Include = [string]::Empty,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$Exclude = [string]::Empty
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="sdkmessageprocessingstep">
    <all-attributes />
    <filter type="and">
      <condition attribute="solutionid" operator="eq" value="{0}" />
      {1}
      {2}
    </filter>
  </entity>
</fetch>
"@


    if ($Include -ne [string]::Empty) {
        $includeCondition = "<condition attribute=`"name`" operator=`"like`" value=`"$Include`" />"
    }
    else {
        $includeCondition = [string]::Empty
    }

    if ($Exclude -ne [string]::Empty) {
        $excludeCondition = "<condition attribute=`"name`" operator=`"not-like`" value=`"$Exclude`" />"
    }
    else {
        $excludeCondition = [string]::Empty
    }

    Write-Verbose -Message "FetchXML:"
    Write-Verbose -Message ($fetchXml -f $SolutionId, $includeCondition, $excludeCondition)

    $steps = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $SolutionId, $includeCondition, $excludeCondition)

    if (($null -eq $steps) -or ($steps.Count -eq 0)) {
        Write-Warning -Message "Found nothing to update"
        return
    }

    Write-Verbose -Message "Found $($steps.Count) steps"
    Write-Verbose -Message "Updating them"

    switch ($State) {
        'Enabled' {
            $response = Set-CRMState -Connection $Connection -Entity $steps -State 0 -Status 1
        }

        'Disabled' {
            $response = Set-CRMState -Connection $Connection -Entity $steps -State 1 -Status 2
        }
    }

    $response | Assert-CRMOrganizationResponse
}


function Get-CRMBusinessUnit {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">
  <entity name="businessunit">
    <all-attributes />
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}"/>
    </filter>
  </entity>
</fetch>
"@


    $bu = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $Name)

    $bu
}


function New-CRMBusinessUnit {
    [CmdletBinding()]
    [OutputType([System.Guid])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [guid]$ParentBusinessUnitId = [guid]::Empty
    )

    if ($ParentBusinessUnitId -eq [guid]::Empty) {
        Write-Verbose -Message "Getting root BU"

        $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" count="1">
  <entity name="businessunit">
    <all-attributes />
    <filter type="and">
      <condition attribute="parentbusinessunitid" operator="null" />
    </filter>
  </entity>
</fetch>
"@


        $bu = (Get-CRMEntity -Connection $Connection -FetchXml $fetchXml) | Select-Object -Index 0
        $ParentBusinessUnitId = $bu.Id
    }

    $buAttributes = @{}
    $buAttributes['name'] = $Name
    $buAttributes['parentbusinessunitid'] = Get-CRMEntityReference -EntityName 'businessunit' -Id $ParentBusinessUnitId
    
    $resp = New-CRMEntity -Connection $Connection -EntityName 'businessunit' -Attributes $buAttributes -ReturnResponses

    $resp | Assert-CRMOrganizationResponse

    $resp.Responses[0].Response.id
}


function New-CRMUser {
    [CmdletBinding()]
    [OutputType([System.Guid])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$DomainName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$FirstName,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$LastName,

        [Parameter(Mandatory=$true)]
        [guid]$BusinessUnitId,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [hashtable]$AdditionAttributes = @{}
    )

    $userAttributes = @{}
    $userAttributes['domainname'] = $DomainName
    $userAttributes['firstname'] = $FirstName
    $userAttributes['lastname'] = $LastName
    $userAttributes['fullname'] = "$($FirstName) $($LastName)"
    $userAttributes['businessunitid'] = Get-CRMEntityReference -EntityName 'businessunit' -Id $BusinessUnitId

    Merge-CRMAttribute -From $AdditionAttributes -To $userAttributes

    $resp = New-CRMEntity -Connection $Connection -EntityName 'systemuser' -Attributes $userAttributes -ReturnResponses

    $resp | Assert-CRMOrganizationResponse

    $resp.Responses[0].Response.id
}


function Get-CRMDuplicateRule {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Entity])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [int]$StateCode = 0,

        [Parameter(Mandatory=$false)]
        [int]$StatusCode = 0
    )
    # statecode statecodename statuscode statuscodename
    # 0 Inactive 0 Unpublished
    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">
  <entity name="duplicaterule">
    <all-attributes />
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}" />
      <condition attribute="statecode" operator="eq" value="{1}" />
      <condition attribute="statuscode" operator="eq" value="{2}" />
    </filter>
  </entity>
</fetch>
"@


    $duplicateRule = (Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $Name, $StateCode, $StatusCode)) | Select-Object -Index 0

    $duplicateRule
}


function New-CRMQueue {
    [CmdletBinding()]
    [OutputType([System.Guid])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [guid]$OwnerId,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$Email = [string]::Empty
    )

    $queueAttributes = @{}
    $queueAttributes['name'] = $Name
    $queueAttributes['ownerid'] = Get-CRMEntityReference -EntityName 'systemuser' -Id $OwnerId
    $queueAttributes['outgoingemaildeliverymethod'] = Get-CRMOptionSetValue -Value 2 # Email Router

    if ($Email -ne [string]::Empty) {
        $queueAttributes['emailaddress'] = $Email
    }

    $resp = New-CRMEntity -Connection $Connection -EntityName 'queue' -Attributes $queueAttributes -ReturnResponses

    $queueId = $resp.Responses[0].Response.id

    if ($Email -ne [string]::Empty) {
        $queueEntity = Get-CRMEntityById -Connection $Connection -EntityName 'queue' -Id $queueId -Columns 'emailrouteraccessapproval'
        $queueEntity['emailrouteraccessapproval'] = Get-CRMOptionSetValue -Value 1 # Approved
        $resp = Update-CRMEntity -Connection $Connection -Entity $queueEntity
        $resp | Assert-CRMOrganizationResponse
    }

    $queueId
}


function Set-CRMQueueForUser {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [guid]$UserId,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [guid]$QueueId
    )

    $userEntity = Get-CRMEntityById -Connection $Connection -EntityName 'systemuser' -Id $UserId -Columns 'queueid'

    $userEntity['queueid'] = Get-CRMEntityReference -EntityName 'queue' -Id $QueueId

    $resp = Update-CRMEntity -Connection $Connection -Entity $userEntity -ReturnResponses

    $resp | Assert-CRMOrganizationResponse
}


function Add-CRMRoleForUser {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [Microsoft.Xrm.Sdk.Entity]$User,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$RoleName
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" count="1">
  <entity name="role">
    <all-attributes />
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}" />
      <condition attribute="businessunitid" operator="eq" value="{1}" />
    </filter>
  </entity>
</fetch>
"@


    $role = (Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $RoleName, $User['businessunitid'].Id)) | Select-Object -Index 0

    if ($null -eq $role) {
        throw "Couldn't find role $($RoleName) or something went wrong."
    }

    $reference = Get-CRMEntityReference -EntityName $role.LogicalName -Id $role.Id

    Add-CRMAssociation -Connection $Connection -EntityName $User.LogicalName -Id $User.Id -Relationship 'systemuserroles_association' -RelatedEntity $reference
}


function Remove-CRMRoleForUser {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [Microsoft.Xrm.Sdk.Entity]$User,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$RoleName
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" count="1">
  <entity name="role">
    <all-attributes />
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}" />
      <condition attribute="businessunitid" operator="eq" value="{1}" />
    </filter>
  </entity>
</fetch>
"@


    $role = (Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $RoleName, $User['businessunitid'].Id)) | Select-Object -Index 0

    if ($null -eq $role) {
        throw "Couldn't find role $($RoleName) or something went wrong."
    }

    $reference = Get-CRMEntityReference -EntityName $role.LogicalName -Id $role.Id

    Remove-CRMAssociation -Connection $Connection -EntityName $User.LogicalName -Id $User.Id -Relationship 'systemuserroles_association' -RelatedEntity $reference
}


function Get-CRMTransactionCurrency {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Entity])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [CurrencyCodeEnum]$CurrencyCode
    )

    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" count="1">
  <entity name="transactioncurrency">
    <all-attributes />
    <filter type="and">
      <condition attribute="isocurrencycode" operator="eq" value="{0}" />
    </filter>
  </entity>
</fetch>
"@


    $tc = (Get-CRMEntity -Connection $Connection -FetchXml ($fetchXml -f $CurrencyCode)) | Select-Object -Index 0

    $tc
}


function New-CRMTransactionCurrency {
    [CmdletBinding()]
    [OutputType([System.Guid])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,
     
        [Parameter(Mandatory=$true)]
        [string]$CurrencyName,

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

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

        [Parameter(Mandatory=$false)]
        [int]$CurrencyPrecision = 2,

        [Parameter(Mandatory=$false)]
        [decimal]$ExchangeRate = [decimal]1.0,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [hashtable]$AdditionAttributes = @{}
    )

    $currencyAttributes = @{}
    $currencyAttributes['currencyprecision'] = $CurrencyPrecision
    $currencyAttributes['exchangerate'] = $ExchangeRate
    $currencyAttributes['isocurrencycode'] = $IsoCurrencyCode

    $currencyAttributes['currencyname'] = $CurrencyName
    $currencyAttributes['currencysymbol'] = $CurrencySymbol

    Merge-CRMAttribute -From $AdditionAttributes -To $currencyAttributes

    $resp = New-CRMEntity -Connection $Connection -EntityName 'transactioncurrency' -Attributes $currencyAttributes -ReturnResponses

    $resp | Assert-CRMOrganizationResponse

    $resp.Responses[0].Response.id
}


function Set-CRMSDKStepMode {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [guid]$Id,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Asynchronous', 'Synchronous')]
        [string]$Mode,

        [Parameter(Mandatory=$false)]
        [switch]$SetAutoDelete
    )

    $step = Get-CRMEntityById -Connection $Connection -EntityName 'sdkmessageprocessingstep' -Id $Id -Columns 'mode', 'asyncautodelete'
    # Sync - 0, Async - 1
    # Yes - 1, No - 0

    switch ($Mode) {
        'Asynchronous' {
            $step['mode'] = Get-CRMOptionSetValue -Value 1
            $step['asyncautodelete'] = $SetAutoDelete.IsPresent
            break
        }

        'Synchronous' {
            $step['mode'] = Get-CRMOptionSetValue -Value 0
            $step['asyncautodelete'] = $false
            break
        }
    }

    $resp = Update-CRMEntity -Connection $Connection -Entity $step
    $resp | Assert-CRMOrganizationResponse
}


function Enable-CRMWorkflow {
    <#
    .SYNOPSIS
    Activates workflow.
 
    .NOTES
    Only owner of workflow can change its state.
    There may be several workflows with same name. This cmdlet will perfom action on all of them.
 
    .LINK
    Get-CRMConnection
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true,
            ParameterSetName='Name')]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$true,
            ParameterSetName='SolutionId')]
        [ValidateNotNull()]
        [guid]$SolutionId,

        [Parameter(Mandatory=$true,
            ParameterSetName='Id')]
        [ValidateNotNull()]
        [guid]$Id
    )


    switch ($PSCmdlet.ParameterSetName) {
        'Id' {
            $fetchXmlWorkflow = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="workflow">
    <filter type="and">
      <condition attribute="workflowid" operator="eq" value="{0}" />
      <condition attribute="type" operator="eq" value="1" />
    </filter>
  </entity>
</fetch>
"@


            $workflows  = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXmlWorkflow -f $Id)

            if ($workflows.Count -gt 0) {
                Write-Verbose -Message "Found $($workflows.Count) workflows with Id '$Id'"

                $response = Set-CRMState -Connection $Connection -Entity $workflows -State 1 -Status 2 -ContinueOnError

                $response | Assert-CRMOrganizationResponse
            }
            else {
                Write-Verbose -Message "No workflows with Id '$Id' were found"
            }
        }

        'Name' {
            $fetchXmlWorkflow = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="workflow">
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}" />
      <condition attribute="type" operator="eq" value="1" />
    </filter>
  </entity>
</fetch>
"@


            $workflows  = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXmlWorkflow -f $Name)

            if ($workflows.Count -gt 0) {
                Write-Verbose -Message "Found $($workflows.Count) workflows with name '$Name'"

                $response = Set-CRMState -Connection $Connection -Entity $workflows -State 1 -Status 2 -ContinueOnError

                $response | Assert-CRMOrganizationResponse
            }
            else {
                Write-Verbose -Message "No workflows with name '$Name' were found"
            }
        }

        'SolutionId' {
            $fetchXmlWorkflow = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="workflow">
    <filter type="and">
      <condition attribute="solutionid" operator="eq" value="{0}" />
      <condition attribute="type" operator="eq" value="1" />
    </filter>
  </entity>
</fetch>
"@


            $workflows  = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXmlWorkflow -f $SolutionId)

            if ($workflows.Count -gt 0)
            {
                Write-Verbose -Message "Found $($workflows.Count) workflows in solution with Id '$SolutionId'"

                $response = Set-CRMState -Connection $Connection -Entity $workflows -State 1 -Status 2 -ContinueOnError

                $response | Assert-CRMOrganizationResponse
            }
            else {
                Write-Verbose -Message "No workflows in solution with Id '$SolutionId' were found"
            }
        }
    }
}


function Disable-CRMWorkflow {
    <#
    .SYNOPSIS
    Deactivates workflow.
 
    .NOTES
    Only owner of workflow can change its state.
    There may be several workflows with same name. This cmdlet will perfom action on all of them.
 
    .LINK
    Get-CRMConnection
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )

    $fetchXmlWorkflow = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="workflow">
    <filter type="and">
      <condition attribute="name" operator="eq" value="{0}" />
      <condition attribute="type" operator="eq" value="1" />
    </filter>
  </entity>
</fetch>
"@


    $workflows  = Get-CRMEntity -Connection $Connection -FetchXml ($fetchXmlWorkflow -f $Name)

    if ($workflows.Count -gt 0) {
        Write-Verbose -Message "Found $($workflows.Count) workflows with name '$Name'"

        $response = Set-CRMState -Connection $Connection -Entity $workflows -State 0 -Status 1 -ContinueOnError

        $response | Assert-CRMOrganizationResponse
    }
    else {
        Write-Verbose -Message "No workflows with name '$Name' were found"
        # Or throw?
    }
}


function Get-CRMSiteMap {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection
    )
    
    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
    <entity name="sitemap">
    </entity>
</fetch>
"@


    $sitemap = (Get-CRMEntity -Connection $Connection -FetchXml $fetchXml) | Select-Object -Index 0

    $sitemap['sitemapxml']
}


function Set-CRMSiteMap {
    [CmdletBinding()]
    [OutputType([void])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,
        
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$SiteMapXml,
        
        [Parameter(Mandatory=$false)]
        [switch]$Publish
    )
    
    $fetchXml = @"
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
    <entity name="sitemap">
    </entity>
</fetch>
"@


    Write-Verbose -Message "Getting sitemap"
    $sitemap = (Get-CRMEntity -Connection $Connection -FetchXml $fetchXml) | Select-Object -Index 0
    $sitemap['sitemapxml'] = $SiteMapXml
    
    Write-Verbose -Message "Updating sitemap"
    Update-CRMEntity -Connection $Connection -Entity $sitemap | Assert-CRMOrganizationResponse
    
    if ($Publish) {
        Write-Verbose -Message "Publishing (only sitemap)"
        Publish-CRMXml -Connection $Connection -ParameterXml '<importexportxml><sitemaps><sitemap></sitemap></sitemaps></importexportxml>'
    }
}


function Publish-CRMXml {
    <#
    .SYNOPSIS
    Publishes only specific solution components.
 
    .LINK
    https://msdn.microsoft.com/en-us/library/microsoft.crm.sdk.messages.publishxmlrequest.parameterxml.aspx
    Get-CRMConnection
    #>

    [CmdletBinding()]
    [OutputType([void])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,
        
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$ParameterXml
    )
    
    $parameters = @{}
    $parameters['ParameterXml'] = $ParameterXml
    
    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'PublishXml' -Parameters $parameters
}


function Get-CRMVersion {
    <#
    .SYNOPSIS
    Returns CRM version.
    #>

    [CmdletBinding()]
    [OutputType([System.Version])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection
    )
    
    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveVersion'
    
    $version = [System.Version]::Parse($response['Version'])
    
    $version
}


function Get-CRMRelationship {
    [CmdletBinding()]
    [OutputType([Microsoft.Xrm.Sdk.Metadata.RelationshipMetadataBase])]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [switch]$RetrieveAsIfPublished
    )

    $parameters = @{}

    $parameters['MetadataId'] = [guid]::Empty
    $parameters['RetrieveAsIfPublished'] = $RetrieveAsIfPublished.IsPresent
    $parameters['Name'] = $Name

    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveRelationship' -Parameters $parameters

    $response['RelationshipMetadata']    
}


function Update-CRMRelationship {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.Metadata.RelationshipMetadataBase]$Relationship,

        [Parameter(Mandatory=$false)]
        [switch]$MergeLabels
    )

    $parameters = @{}

    $parameters['MergeLabels'] = $MergeLabels.IsPresent
    $parameters['Relationship'] = $Relationship

    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'UpdateRelationship' -Parameters $parameters
}


function Wait-CRMAsyncJob {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [guid]$AsyncJobId,

        [Parameter(Mandatory=$false)]
        [int]$CheckPeriod = 5
    )
    
    do {
        try {
            Write-Verbose -Message "Checking AsyncJob $($AsyncJobId)"
            $asyncjob = Get-CRMEntityById -Connection $Connection -EntityName 'asyncoperation' -Id $AsyncJobId -AllColumns

            Write-Verbose "AsyncJob $($AsyncJobId) state and status: $($asyncjob.FormattedValues['statecode']), $($asyncjob.FormattedValues['statuscode'])"
        }
        catch {
            if ($_.Exception.Message -eq 'SQL timeout expired.') {
                Write-Warning -Message $_.Exception.Message 
            } else {
                throw $_
            }
        }

        Start-Sleep -Seconds $CheckPeriod
    } until ($asyncjob['statecode'] -eq 3) # 3 - Completed

    Write-Verbose -Message "AsyncJob $($AsyncJobId) completed in $($asyncjob['executiontimespan']) seconds."

    if ($asyncjob['statuscode'] -eq 31) {
        Write-Warning -Message "AsyncJob $($AsyncJobId) failed"
    }
}


function Save-CRMFormattedImportJobResult {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [guid]$ImportJobId,

        [Parameter(Mandatory=$false)]
        [string]$FilePath
    )

    $parameters = @{'ImportJobId' = $ImportJobId}

    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveFormattedImportJobResults' -Parameters $parameters

    Out-File -FilePath $FilePath -InputObject $response['FormattedResults']
}


function Get-CRMAuditDetail {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection,

        [Parameter(Mandatory=$true)]
        [guid]$AuditId
    )

    $parameters = @{'AuditId' = $AuditId}

    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveAuditDetails' -Parameters $parameters

    $response['AuditDetail']
}


function Expand-CRMSolution {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ZipPath,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$Folder = '.',

        [Parameter(Mandatory = $false)]
        [Microsoft.Crm.Tools.SolutionPackager.SolutionPackageType]$PackageType = [Microsoft.Crm.Tools.SolutionPackager.SolutionPackageType]::Both,

        [Parameter(Mandatory = $false)]
        [string]$SingleComponent = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [Microsoft.Crm.Tools.SolutionPackager.AllowDelete]$AllowDeletes = [Microsoft.Crm.Tools.SolutionPackager.AllowDelete]::No,

        [Parameter(Mandatory = $false)]
        [switch]$Localize
    )

    $packagerArguments = New-Object -TypeName 'Microsoft.Crm.Tools.SolutionPackager.PackagerArguments'
    $packagerArguments.Action = [Microsoft.Crm.Tools.SolutionPackager.CommandAction]::Extract
    $packagerArguments.PathToZipFile = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ZipPath)
    $packagerArguments.PackageType = $PackageType
    $packagerArguments.Folder = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Folder)
    $packagerArguments.SingleComponent = $SingleComponent
    $packagerArguments.AllowDeletes = $AllowDeletes
    $packagerArguments.Localize = $Localize

    $solutionPackager = New-Object -TypeName 'Microsoft.Crm.Tools.SolutionPackager.SolutionPackager' -ArgumentList $packagerArguments
    Write-Verbose -Message "Extracting: $($packagerArguments.PathToZipFile) -> $($packagerArguments.Folder)"
    $solutionPackager.Run()
}


function Compress-CRMSolution {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ZipPath,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$Folder = '.',

        [Parameter(Mandatory = $false)]
        [Microsoft.Crm.Tools.SolutionPackager.SolutionPackageType]$PackageType = [Microsoft.Crm.Tools.SolutionPackager.SolutionPackageType]::Both,

        [Parameter(Mandatory = $false)]
        [string]$SingleComponent = [string]::Empty,

        [Parameter(Mandatory = $false)]
        [Microsoft.Crm.Tools.SolutionPackager.AllowDelete]$AllowDeletes = [Microsoft.Crm.Tools.SolutionPackager.AllowDelete]::No,

        [Parameter(Mandatory = $false)]
        [switch]$Localize
    )

    $packagerArguments = New-Object -TypeName 'Microsoft.Crm.Tools.SolutionPackager.PackagerArguments'
    $packagerArguments.Action = [Microsoft.Crm.Tools.SolutionPackager.CommandAction]::Pack
    $packagerArguments.PathToZipFile = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ZipPath)
    $packagerArguments.PackageType = $PackageType
    $packagerArguments.Folder = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Folder)
    $packagerArguments.SingleComponent = $SingleComponent
    $packagerArguments.AllowDeletes = $AllowDeletes
    $packagerArguments.Localize = $Localize
    
    $solutionPackager = New-Object -TypeName 'Microsoft.Crm.Tools.SolutionPackager.SolutionPackager' -ArgumentList $packagerArguments
    Write-Verbose -Message "Packing: $($packagerArguments.Folder) -> $($packagerArguments.PathToZipFile)"
    $solutionPackager.Run()
}


function Get-CRMCurrentOrganization {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [Microsoft.Xrm.Sdk.IOrganizationService]$Connection
    )

    $parameters = @{}
    $parameters['AccessType'] = [Microsoft.Xrm.Sdk.Organization.EndpointAccessType]::Default
    
    $response = Invoke-CRMOrganizationRequest -Connection $Connection -RequestName 'RetrieveCurrentOrganization' -Parameters $parameters

    $response['Detail']
}


Set-Alias -Name 'Activate-CRMWorkflow' -Value 'Enable-CRMWorkflow'
Set-Alias -Name 'Deactivate-CRMWorkflow' -Value 'Disable-CRMWorkflow'