internal/functions/repair-bacpacmodelsimpleandreplace.ps1


<#
    .SYNOPSIS
        Repair a bacpac model file - using simple remove AND replace logic
         
    .DESCRIPTION
        Will use a search pattern, and end pattern, to remove an element from the model file
         
        Will use a search pattern, with the replacement value, to replace the string within the model file
         
    .PARAMETER Path
        Path to the bacpac model file that you want to work against
         
    .PARAMETER OutputPath
        Path to where the repaired model file should be placed
         
    .PARAMETER RemoveInstructions
        Search pattern that is used to start the removable of the element
         
        Supports wildcard - as it utilizes the -Like operation that is available directly in powershell
         
        E.g. "*<Element Type=\"SqlPermissionStatement\"*ms_db_configreader*"
         
        End pattern that is used to conclude the removable of the element
         
        Supports wildcard - as it utilizes the -Like operation that is available directly in powershell
         
        E.g. "*</Element>*"
         
    .PARAMETER ReplaceInstructions
        Search pattern that is used to replace the value
         
        Works directly on the value entered, no wildcard or regex is supported at all
         
        E.g. "<Property Name=\"AutoDrop\" Value=\"True\" />"
         
        Replace value that you want to substitute your search value with
         
        E.g. ""
         
    .EXAMPLE
        PS C:\> $removeIns1 = [pscustomobject][ordered]@{Search = '*<Element Type="SqlPermissionStatement"*ms_db_configreader*';End = '*</Element>*'}
        PS C:\> $replace1 = [pscustomobject][ordered]@{Search = '<Property Name="AutoDrop" Value="True" />';Replace = ''}
        PS C:\> Repair-BacpacModelSimpleAndReplace -Path c:\temp\model.xml -OutputPath c:\temp\repaired_model.xml -RemoveInstructions @($removeIns1) -ReplaceInstructions @($replace1)
         
        This will remove the below section from the model file, based on the RemoveInstructions:
         
        <Element Type="SqlPermissionStatement" Name="[Grant.Delete.Object].[ms_db_configreader].[dbo].[dbo].[AutotuneBase]">
        <Property Name="Permission" Value="4" />
        <Relationship Name="Grantee">
        <Entry>
        <References Name="[ms_db_configreader]" />
        </Entry>
        </Relationship>
        <Relationship Name="Grantor">
        <Entry>
        <References ExternalSource="BuiltIns" Name="[dbo]" />
        </Entry>
        </Relationship>
        <Relationship Name="SecuredObject">
        <Entry>
        <References Name="[dbo].[AutotuneBase]" />
        </Entry>
        </Relationship>
        </Element>
         
        This will remove the below section from the model file, based on the ReplaceInstructions:
         
        <Property Name="AutoDrop" Value="True" />
         
    .NOTES
        Author: Mötz Jensen (@Splaxi)
         
        Json files has to be an array directly in the root of the file. All " (double quotes) has to be escaped with \" - otherwise it will not work as intended.
         
        This cmdlet is inspired by the work of "Brad Bateman" (github: @batetech)
         
        His github profile can be found here:
        https://github.com/batetech
         
        Florian Hopfner did a gist implementation, which has been used as the foundation for this implementation
         
        The original gist is: https://gist.github.com/FH-Inway/f485c720b43b72bffaca5fb6c094707e
         
        His github profile can be found here:
        https://github.com/FH-Inway
         
        https://devblog.pekspro.com/posts/multiple-find-and-replace-with-powershell
#>

function Repair-BacpacModelSimpleAndReplace {
    [CmdletBinding()]
    param (
        [string] $Path,

        [string] $OutputPath,

        [Object[]] $RemoveInstructions,

        [Object[]] $ReplaceInstructions
    )
    Invoke-TimeSignal -Start
    
    Write-PSFMessage -Level Verbose -Message "RemoveInstructions count is: $($RemoveInstructions.Count)" -Target $RemoveInstructions
    Write-PSFMessage -Level Verbose -Message "ReplaceInstructions count is: $($ReplaceInstructions.Count)" -Target $ReplaceInstructions
    
    [int]$flushCounter = 500000

    $buffer = [System.Collections.Generic.List[string]]::new($flushCounter) #much faster than PS array using +=
    $bufferCounter = 0;
    
    try {
        $stream = [System.IO.StreamReader]::new($Path)

        :LineLoop while ($stream.Peek() -ge 0) {
            $line = $stream.ReadLine()
        
            # Skipping empty lines
            if (-not [string]::IsNullOrEmpty($line)) {

                # Implement Replace Logic directly here - so we only handle replace once, if the line contains data..
                foreach ($ReplaceIns in $ReplaceInstructions) {
                    $line = $line.Replace($ReplaceIns.Search, $ReplaceIns.Replace)
                }

                foreach ($remove in $RemoveInstructions) {
                    
                    if ($line -like $remove.Search) {
                        # We found the search pattern - next is just removing lines, until we find the end pattern

                        while ($stream.Peek() -ge 0) {
                            $line = $stream.ReadLine();

                            if ($line -like $remove.End) {
                                # We found the end tag, so we need to start the line loop again.
                                continue LineLoop
                            }
                        }
                    }
                }
            
                $buffer.Add($line)
            }
            else {
                $buffer.Add($line)
            }

            $bufferCounter++;
            if ($bufferCounter -ge $flushCounter) {
                $buffer | Add-Content -LiteralPath $OutputPath -Encoding UTF8
                $buffer = [System.Collections.Generic.List[string]]::new($flushCounter);
                $bufferCounter = 0;
            }
        }
    }
    finally {
        $stream.Close()
        $stream.Dispose()
    }

    # Flush anything still remaining in the buffer
    if ($bufferCounter -gt 0) {
        $buffer | Add-Content -LiteralPath $OutputPath -Encoding UTF8
        $buffer = $null;
        $bufferCounter = 0;
    }

    Invoke-TimeSignal -End
}