functions/Remove-UnifiedEnvironmentModule.ps1

<#
    .SYNOPSIS
        Remove UDE environment modules.
 
    .DESCRIPTION
        Removes UDE environment modules (msprov_fnomodule records) for a specified environment.
 
        Modules are matched by module id and name. Nothing is removed unless the -Force switch is supplied.
        Without -Force the cmdlet lists the modules that would be removed.
 
    .PARAMETER EnvironmentId
        The id of the environment that you want to work against
 
    .PARAMETER ModuleId
        The id of the module that you want to remove.
 
        Supports wildcard characters for flexible matching against the module id.
 
    .PARAMETER Name
        The name of the module that you want to remove.
 
        Supports wildcard characters for flexible matching against the module name.
 
    .PARAMETER Force
        Instructs the function to proceed with removing the modules.
 
        Nothing happens unless this parameter is supplied.
 
    .EXAMPLE
        PS C:\> Remove-UnifiedEnvironmentModule -EnvironmentId "env-123" -ModuleId "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
 
        This will show the module that would be removed for the specified environment id.
        It will NOT remove the module yet, allowing you to review it before deciding to remove it.
 
    .EXAMPLE
        PS C:\> Remove-UnifiedEnvironmentModule -EnvironmentId "env-123" -ModuleId "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" -Force
 
        This will remove the specified module for the specified environment id without further confirmation.
 
    .EXAMPLE
        PS C:\> Get-UnifiedEnvironmentModule -EnvironmentId "env-123" | Remove-UnifiedEnvironmentModule -Force
 
        This will remove all modules returned for the specified environment id without further confirmation.
 
    .NOTES
        Author: Mötz Jensen (@Splaxi)
#>

function Remove-UnifiedEnvironmentModule {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter (Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("PpacEnvId")]
        [string] $EnvironmentId,

        [Parameter (ValueFromPipelineByPropertyName = $true)]
        [string] $ModuleId = "*",

        [Parameter (ValueFromPipelineByPropertyName = $true)]
        [string] $Name = "*",

        [switch] $Force
    )

    begin {
    }

    process {
        if (Test-PSFFunctionInterrupt) { return }

        $envObj = Get-UnifiedEnvironment -EnvironmentId $EnvironmentId -SkipVersionDetails | Select-Object -First 1

        if ($null -eq $envObj) {
            $messageString = "Could not find environment with Id <c='em'>$EnvironmentId</c>. Please verify the Id and try again, or list available environments using <c='em'>Get-UnifiedEnvironment</c>. Consider using wildcards if needed."

            Write-PSFMessage -Level Important -Message $messageString
            Stop-PSFFunction -Message "Stopping because environment was NOT found based on the id." `
                -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        $baseUri = $envObj.PpacEnvUri + "/" #! Very important to have the trailing slash

        $secureToken = (Get-AzAccessToken -ResourceUrl $baseUri -AsSecureString).Token
        $tokenWebApiValue = ConvertFrom-SecureString -AsPlainText -SecureString $secureToken

        $headers = @{
            "Authorization"    = "Bearer $($tokenWebApiValue)"
            "Accept"           = "application/json;odata.metadata=minimal" # minimal || full
            "OData-MaxVersion" = "4.0"
            "OData-Version"    = "4.0"
            "Prefer"           = "odata.include-annotations=*"
        }

        $localUri = $baseUri + "api/data/v9.0/msprov_fnomodules"

        $colModules = Invoke-RestMethod -Uri $localUri `
            -Method Get `
            -Headers $headers | `
            Select-Object -ExpandProperty value | `
            Sort-Object -Property modifiedon -Descending

        $resCol = @(
            $colModules | Select-PSFObject -TypeName 'D365Bap.Tools.UdeEnvironmentModule' `
                -Property "msprov_name As Name",
            "msprov_fnomoduleid As ModuleId",
            "'_createdby_value@OData.Community.Display.V1.FormattedValue' As CreatedBy",
            "'_modifiedby_value@OData.Community.Display.V1.FormattedValue' As ModifiedBy",
            "'statuscode@OData.Community.Display.V1.FormattedValue' As Status",
            "'statecode@OData.Community.Display.V1.FormattedValue' As State",
            "createdon As Created",
            "modifiedon As Modified",
            "versionnumber As Version",
            * `
                -ExcludeProperty '@odata.etag'
        )

        $colFiltered = @($resCol | Where-Object { $_.ModuleId -like $ModuleId -and $_.Name -like $Name })

        if ($colFiltered.Count -lt 1) {
            $messageString = "No modules found for environment <c='em'>$EnvironmentId</c> matching ModuleId <c='em'>$ModuleId</c> and Name <c='em'>$Name</c>. Please verify the filters and try again, or list available modules using <c='em'>Get-UnifiedEnvironmentModule</c>."

            Write-PSFMessage -Level Important -Message $messageString
            Stop-PSFFunction -Message "Stopping because no modules were found based on the supplied filters." `
                -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        if (-not $Force) {
            Write-PSFMessage -Level Important -Message "The following modules would be removed:"
            $colFiltered | ForEach-Object { Write-PSFMessage -Level Important -Message " - <c='em'>$($_.Name)</c> ($($_.ModuleId))" }

            $messageString = "This will remove the listed modules. If you are sure, please re-run the command with the <c='em'>-Force</c> parameter."

            Write-PSFMessage -Level Important -Message $messageString
            Stop-PSFFunction -Message "Stopping because Force parameter wasn't supplied." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
            return
        }

        [System.Collections.Generic.List[System.Object]] $arrFailed = @()

        foreach ($moduleObj in $colFiltered) {
            $deleteUri = $baseUri + "api/data/v9.0/msprov_fnomodules($($moduleObj.ModuleId))"

            $deleteParams = @{
                Method             = 'Delete'
                Uri                = $deleteUri
                Headers            = $headers
                SkipHttpErrorCheck = $true
                StatusCodeVariable = 'statusDelete'
            }

            Invoke-RestMethod @deleteParams > $null 4>$null

            if (-not ($statusDelete -like "2**")) {
                Write-PSFMessage -Level Important -Message "Failed to remove module <c='em'>$($moduleObj.Name)</c> ($($moduleObj.ModuleId)). HTTP status code: $statusDelete."
                $arrFailed.Add($moduleObj)
            }
            else {
                Write-PSFMessage -Level Verbose -Message "Removed module '$($moduleObj.Name)' ($($moduleObj.ModuleId))."
            }
        }

        if ($arrFailed.Count -gt 0) {
            $messageString = "The following modules <c='em'>failed to be removed</c>:"

            Write-PSFMessage -Level Important -Message $messageString
            $arrFailed.ToArray()

            if ($arrFailed.Count -eq $colFiltered.Count) {
                Stop-PSFFunction -Message "Stopping because all module removals failed." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', '')))
                return
            }
        }
    }

    end {
    }
}