AMSoftware.Dataverse.PowerShell.Development.psm1

# PowerShell Module for Power Platform Dataverse
# Copyright(C) 2024 AMSoftwareNL
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

function Wait-AsyncOperation {
    param (
        [guid]$AsyncOperationId,
        [string]$ProgressActivity
    )

    $progressActivityId = [System.Random]::new().Next()

    # Wait for asyncjob to complete (success or fail)
    $exportStart = [datetime]::UtcNow
    $checkcount = 0
    $loopcount = 1
    do {
        Write-Progress -Id $progressActivityId -Activity $ProgressActivity -Status ([datetime]::UtcNow - $exportStart).ToString('c')
        Start-Sleep -Seconds 1

        $checkcount++
        if (($checkcount % (10 * $loopcount)) -eq 0) {
            $asyncOperation = Get-DataverseRow -Table 'asyncoperation' -Id $AsyncOperationId -Columns @(
                'statecode',
                'statuscode',
                'message'
            )
            if ($loopcount -gt 10) {
                $loopcount = 10
            }
            else {
                $loopcount++
            }
            $checkcount = 0
        }
    } until ($asyncOperation.statecode -eq 3)
    Write-Progress -Id $progressActivityId -Completed

    Write-Output $asyncOperation
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Export-DataverseWebResource {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [guid]$Id,

        [Parameter(Mandatory = $true)]
        [string]$OutputPath
    )

    begin {
        $resolvedPath = Resolve-Path -Path $OutputPath
    }

    process {
        $resource = Get-DataverseRow -Table 'webresource' -Id $Id -Columns 'name', 'content', 'contentfileref'

        # Only keep filename part of the webresource path
        $resourceFilename = $resource.Name
        [System.IO.Path]::GetInvalidFileNameChars() | ForEach-Object { $resourceFilename = $resourceFilename.Replace($_, '-') }

        $resourceFilepath = Join-Path -Path $resolvedPath -ChildPath $resourceFilename

        if (-not [string]::IsNullOrWhiteSpace($resource.content)) {
            # Content is in the table
            Set-Content -LiteralPath $resourceFilepath -Value ([System.Convert]::FromBase64String($resource.content)) -AsByteStream -Force
        }
        else {
            # Content is a fileattachment
            Export-DataverseFile -Table 'webresource' -Row $Id -Column 'contentfileref' | `
                Set-Content -LiteralPath $resourceFilepath -Force
        }
        Get-Item -LiteralPath $resourceFilepath
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Export-DataversePluginAssembly {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [guid]$Id,

        [Parameter(Mandatory = $true)]
        [string]$OutputPath
    )

    begin {
        $resolvedPath = Resolve-Path -Path $OutputPath
    }

    process {
        $assembly = Get-DataverseRow -Table 'pluginassembly' -Id $Id -Columns 'name', 'path', 'content'
        $assemblyFilepath = Join-Path -Path $resolvedPath -ChildPath $assembly.path

        if (-not [string]::IsNullOrWhiteSpace($assembly.content)) {
            # Content is in the table
            Set-Content -LiteralPath $assemblyFilepath -Value ([System.Convert]::FromBase64String($assembly.content)) -AsByteStream -Force
            Get-Item -LiteralPath $assemblyFilepath
        }
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Export-DataverseTranslation {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNull()]
        [Alias('Name')]
        [string]$SolutionName,

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

    process {
        $solution = Get-DataverseRows -Table 'solution' -Query @{uniquename = $SolutionName } -Top 1

        $folderPath = Resolve-Path $OutputPath
        $filePath = Join-Path -Path $folderPath -ChildPath "$($solution.uniquename)_translation.zip"

        $result = Send-DataverseRequest -Name 'ExportTranslation' -Parameters @{'SolutionName' = $solution.uniquename }

        Set-Content -LiteralPath $filePath -Value $result.ExportTranslationFile -AsByteStream -Force
        Get-Item -LiteralPath $filePath
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Import-DataverseTranslation {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Alias('PSPath')]
        [ValidateNotNullOrEmpty()]
        [string]$LiteralPath
    )

    process {
        $translationsPath = Resolve-Path -LiteralPath $LiteralPath

        $asyncResponse = Send-DataverseRequest -Name 'ImportTranslationAsync' -Parameters @{
            'TranslationFile' = Get-Content -LiteralPath $translationsPath -Raw
            'ImportJobId'     = [guid]::Newguid()
        }

        $asyncOperation = Wait-AsyncOperation `
            -AsyncOperationId $asyncResponse.AsyncOperationId `
            -ProgressActivity "Import Translation: $translationsPath"

        if ($asyncOperation.statuscode -eq 30) {
        }
        else {
            Write-Error -Message $asyncOperation.message
        }
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Export-DataverseSolution {
    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNull()]
        [Alias('Name')]
        [string]$SolutionName,

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

        [Parameter()]
        [switch]$AsManaged
    )

    process {
        $solution = Get-DataverseRows -Table 'solution' -Query @{uniquename = $SolutionName } -Top 1

        if ($AsManaged.ToBool()) {
            $filename = "$($solution.uniquename)_$($solution.version.Replace('.','_'))_managed.zip"
        }
        else {
            $filename = "$($solution.uniquename)_$($solution.version.Replace('.','_')).zip"
        }

        $folderPath = Resolve-Path $OutputPath
        $filePath = Join-Path -Path $folderPath -ChildPath $filename

        $asyncResponse = Send-DataverseRequest -Name 'ExportSolutionAsync' -Parameters @{
            SolutionName = $SolutionName
            Managed      = $AsManaged.ToBool()
        }

        $asyncOperation = Wait-AsyncOperation `
            -AsyncOperationId $asyncResponse.AsyncOperationId `
            -ProgressActivity "Export Solution: $($solution.uniquename)"

        if ($asyncOperation.statuscode -eq 30) {
            $result = Send-DataverseRequest -Name 'DownloadSolutionExportData' -Parameters @{
                ExportJobId = $asyncResponse.ExportJobId
            }

            Set-Content -LiteralPath $filePath -Value $result.ExportSolutionFile -AsByteStream -Force
            Get-Item -LiteralPath $filePath
        }
        else {
            Write-Error -Message $asyncOperation.message
        }
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Import-DataverseSolution {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding(DefaultParameterSetName = 'ImportSolution')]
    [OutputType([guid])]
    [OutputType([Microsoft.Xrm.Sdk.StageSolutionResults])]
    param (
        [Parameter(Mandatory = $true)]
        [Alias('PSPath')]
        [ValidateNotNullOrEmpty()]
        [string]$LiteralPath,

        [Parameter(ParameterSetName = 'ImportAsStaged')]
        [switch]$Stage,

        [Parameter(ParameterSetName = 'ImportAsUpgrade')]
        [switch]$Upgrade,

        [Parameter()]
        [switch]$Overwrite,

        [Parameter(ParameterSetName = 'ImportSolution')]
        [switch]$Hold,

        [Parameter(ParameterSetName = 'ImportSolution')]
        [Parameter(ParameterSetName = 'ImportAsUpgrade')]
        [switch]$PublishWorkflows,

        [Parameter(ParameterSetName = 'ImportSolution')]
        [Parameter(ParameterSetName = 'ImportAsUpgrade')]
        [Microsoft.Xrm.Sdk.EntityCollection]$ComponentParameters
    )

    process {
        $solutionPath = Resolve-Path -LiteralPath $LiteralPath

        switch ($PSCmdlet.ParameterSetName) {
            'ImportAsStaged' {
                $stageResponse = Send-DataverseRequest -Name 'StageSolution' -Parameters @{
                    CustomizationFile = (Get-Content -LiteralPath $solutionPath -Raw)
                }

                Write-Output $stageResponse.StageSolutionResults
            }
            'ImportAsUpgrade' {
                $asyncResponse = Send-DataverseRequest -Name 'StageAndUpgradeAsync' -Parameters @{
                    CustomizationFile                = (Get-Content -LiteralPath $solutionPath -Raw)
                    OverwriteUnmanagedCustomizations = $Overwrite.ToBool()
                    PublishWorkflows                 = $PublishWorkflows.ToBool()
                    ComponentParameters              = $ComponentParameters
                }

                $asyncOperation = Wait-AsyncOperation `
                    -AsyncOperationId $asyncResponse.AsyncOperationId `
                    -ProgressActivity "Stage and Upgrade Solution: $solutionPath"

                if ($asyncOperation.statuscode -eq 30) {
                    Write-Output $asyncResponse.ImportJobKey
                }
                else {
                    Write-Error -Message $asyncOperation.message
                }
            }
            Default {
                $asyncResponse = Send-DataverseRequest -Name 'ImportSolutionAsync' -Parameters @{
                    CustomizationFile                = (Get-Content -LiteralPath $solutionPath -Raw)
                    HoldingSolution                  = $Hold.ToBool()
                    OverwriteUnmanagedCustomizations = $Overwrite.ToBool()
                    PublishWorkflows                 = $PublishWorkflows.ToBool()
                    ComponentParameters              = $ComponentParameters
                }

                $asyncOperation = Wait-AsyncOperation `
                    -AsyncOperationId $asyncResponse.AsyncOperationId `
                    -ProgressActivity "Import Solution: $solutionPath"

                if ($asyncOperation.statuscode -eq 30) {
                    Write-Output $asyncResponse.ImportJobKey
                }
                else {
                    Write-Error -Message $asyncOperation.message
                }
            }
        }
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Update-DataverseSolution {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([guid])]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'UpgradeFromHold')]
        [ValidateNotNullOrEmpty()]
        [Alias('UniqueName')]
        [string]$SolutionName,

        [Parameter(Mandatory = $true, ParameterSetName = 'UpgradeFromStaged')]
        [guid]$Stage,

        [Parameter(ParameterSetName = 'UpgradeFromStaged')]
        [switch]$PublishWorkflows,

        [Parameter(ParameterSetName = 'UpgradeFromStaged')]
        [Microsoft.Xrm.Sdk.EntityCollection]$ComponentParameters
    )

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'UpgradeFromHold' {
                $solution = Get-DataverseRows -Table 'solution' -Query @{uniquename = $SolutionName } -Top 1

                $asyncResponse = Send-DataverseRequest -Name 'DeleteAndPromoteAsync' -Parameters @{
                    UniqueName = $solution.uniquename
                }

                $asyncOperation = Wait-AsyncOperation `
                    -AsyncOperationId $asyncResponse.AsyncOperationId `
                    -ProgressActivity "Upgrade Holding Solution: $($solution.uniquename)"

                if ($asyncOperation.statuscode -eq 30) {
                }
                else {
                    Write-Error -Message $asyncOperation.message
                }
            }
            'UpgradeFromStaged' {
                $solutionParameters = New-Object `
                    -TypeName 'Microsoft.Xrm.Sdk.SolutionParameters' `
                    -Property @{ StageSolutionUploadId = $Stage }

                $asyncResponse = Send-DataverseRequest -Name 'ImportSolutionAsync' -Parameters @{
                    SolutionParameters               = $solutionParameters
                    OverwriteUnmanagedCustomizations = $Overwrite.ToBool()
                    PublishWorkflows                 = $PublishWorkflows.ToBool()
                    ComponentParameters              = $ComponentParameters
                }

                $asyncOperation = Wait-AsyncOperation `
                    -AsyncOperationId $asyncResponse.AsyncOperationId `
                    -ProgressActivity "Upgrade Solution from Stage: $Stage"

                if ($asyncOperation.statuscode -eq 30) {
                    Write-Output $asyncResponse.ImportJobKey
                }
                else {
                    Write-Error -Message $asyncOperation.message
                }
            }
        }
    }
}

# .EXTERNALHELP AMSoftware.Dataverse.PowerShell.Development.psm1-help.xml
function Publish-DataverseComponent {
    [CmdletBinding(DefaultParameterSetName = 'PublishAll')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'PublishComponent')]
        [ValidateSet('Table', 'Choice', 'WebResource')]
        [string]$Type,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'PublishComponent')]
        [ValidateNotNullOrWhiteSpace()]
        [string]$ComponentId
    )

    begin {
        $ids = @()
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'PublishComponent' {
                $ids += $ComponentId
            }
        }
    }

    end {
        switch ($PSCmdlet.ParameterSetName) {
            'PublishAll' {
                $asyncResponse = Send-DataverseRequest -Name 'PublishAllXmlAsync'

                $asyncOperation = Wait-AsyncOperation `
                    -AsyncOperationId $asyncResponse.AsyncOperationId `
                    -ProgressActivity 'Publish Customizations'

                if ($asyncOperation.statuscode -eq 30) {
                }
                else {
                    Write-Error -Message $asyncOperation.message
                }
            }
            'PublishComponent' {
                switch ($Type) {
                    'Table' {
                        $grouplabel = 'entities'
                        $itemlabel = 'entity'
                    }
                    'Choice' {
                        $grouplabel = 'optionsets'
                        $itemlabel = 'optionset'
                    }
                    'WebResource' {
                        $grouplabel = 'webresources'
                        $itemlabel = 'webresource'
                    }
                }

                $publishxml = $ids | `
                    Select-Object -Property @{l = 'Item'; e = { "<$($itemlabel)>$_</$($itemlabel)>" } } | `
                    Join-String -Property 'Item' -OutputPrefix "<$($grouplabel)>" -OutputSuffix "</$($grouplabel)>"
                $parameterxml = "<importexportxml>$publishxml</importexportxml>"

                Send-DataverseRequest -Name 'PublishXml' -Parameters @{ParameterXml = $parameterxml } | Out-Null
            }
        }
    }
}