internal/functions/repair-bacpacmodelqualifier.ps1


<#
    .SYNOPSIS
        Repair a bacpac model file - using qualification logic
         
    .DESCRIPTION
        Will use a search pattern, qualification and end pattern, to remove an element from 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 Search
        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=\"SqlRoleMembership\">*"
         
    .PARAMETER Qualifier
        Qualifier pattern that is used to qualify the element, based on a nested line value
         
        Supports wildcard - as it utilizes the -Like operation that is available directly in powershell
         
        E.g. "*<References Name=*ms_db_configwriter*"
         
    .PARAMETER End
        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>*"
         
    .EXAMPLE
        PS C:\> Repair-BacpacModelQualifier -Path c:\temp\model.xml -OutputPath c:\temp\repaired_model.xml -Search '*<Element Type="SqlRoleMembership">*' -Qualifier "*<References Name=*ms_db_configwriter*" -End "*</Element>*"
         
        This will remove the below section from the model file
         
        <Element Type="SqlRoleMembership">
        <Relationship Name="Member">
        <Entry>
        <References Name="[ms_db_configwriter]" />
        </Entry>
        </Relationship>
        <Relationship Name="Role">
        <Entry>
        <References ExternalSource="BuiltIns" Name="[db_ddladmin]" />
        </Entry>
        </Relationship>
        </Element>
         
    .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
#>

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

        [string] $OutputPath,

        [string] $Search,

        [string] $Qualifier,

        [string] $End
        
    )
    
    Invoke-TimeSignal -Start
    
    Write-PSFMessage -Level Verbose -Message "Will search for: $Search - Qualify with: $Qualifier" -Target @($Search, $Qualifier, $End)
    
    [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)) {

                if ($line -like $Search) {
                    # We hit search pattern, will read lines until hitting the end pattern.
                    # Will buffer all lines going forward, in case we need to keep them in the output file

                    $bufTemp = [System.Collections.Generic.List[string]]::new(20)
                    $bufTemp.Add($line)
                    $foundQualifier = $false

                    while ($stream.Peek() -ge 0) {
                        $line = $stream.ReadLine();
                        
                        $bufTemp.Add($line)
                    
                        if ($line -like $Qualifier) {
                            $foundQualifier = $true
                        }

                        if ($line -like $End) {
                            if (-not $foundQualifier) {
                                # This tells us to keep all the lines that we buffered
                                $buffer.AddRange($bufTemp.ToArray())
                            }

                            continue LineLoop
                        }
                    }
                }
                            
                # Persisting all lines that didn't meet the search pattern
                $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 {
        # We need to close the stream object, to release the file system lock on the input file
        $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
}