PowerShell.PowerLibrary.IncrementalUpdate.psm1

<#
    IncrementalUpdate Module
#>


#region Functions
FUNCTION Get-DynamicParams 
{
    <#
    .Synopsis
        Generate dynamic parameters
    .DESCRIPTION
        Generate dynamically the provider parameters that are specified in param section.
        Filters the parameters by removing "CommonParameters".
        Generate AttributeCollection by iterating on parameter attributes .
        It return [System.Management.Automation.RuntimeDefinedParameterDictionary].
    .PARAMETER ParameterDictionary
        Dictionary contains all parameters for specified provider
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $ParameterDictionary
    )

    begin 
    {
        $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | `
        Get-Member -MemberType Properties | `
        Select-Object -ExpandProperty Name;

        # Param attributes will be copied later. You basically have to create a blank attrib, then change the
        # properties. Knowing the writable ones up front helps:
        $WritableParamAttributePropertyNames = [System.Management.Automation.ParameterAttribute]::new() | `
            Get-Member -MemberType Property | `
            Where-Object { $_.Definition -match "{.*set;.*}$" } | `
            Select-Object -ExpandProperty Name;
    }

    process 
    {
        # Convert to object array and get rid of Common params and (Parameters hashtable):
        $Parameters = $ParameterDictionary.GetEnumerator() | Where-Object `
        { 
            ($CommonParameterNames -notcontains $_.Key) -and ($_.Key -ne "Parameters")
        };

        # Create the Runtime Param dictionary
        $DynParamDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new();

        FOREACH ($Parameter in $Parameters) 
        {
            $AttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new();

            $ParameterType = $Parameter.Value.ParameterType;

            FOREACH ($CurrentAttribute in $Parameter.Value.Attributes) 
            {
                $AttributeTypeName = $CurrentAttribute.TypeId.FullName;

                switch -wildcard ($AttributeTypeName) 
                {
                    System.Management.Automation.ArgumentTypeConverterAttribute 
                    {
                        continue;  # So blank param doesn't get added
                    }

                    System.Management.Automation.Validate*Attribute 
                    {
                        $NewParamAttribute = $CurrentAttribute;
                        $AttributeCollection.Add($NewParamAttribute);
                    }

                    System.Management.Automation.ParameterAttribute 
                    {

                        $NewParamAttribute = [System.Management.Automation.ParameterAttribute]::new();
                        
                        FOREACH ($PropName in $WritableParamAttributePropertyNames) 
                        {
                            IF ($NewParamAttribute.$PropName -ne $CurrentAttribute.$PropName) 
                            {  
                                # nulls cause an error if you assign them to some of the properties
                                $NewParamAttribute.$PropName = $CurrentAttribute.$PropName;
                            }
                        }

                        $NewParamAttribute.ParameterSetName = $CurrentAttribute.ParameterSetName;

                        $AttributeCollection.Add($NewParamAttribute);
                    }

                    default 
                    {
                        Write-Warning "Function doesn't handle dynamic param copying for $AttributeTypeName";
                        continue;
                    }
                }
            }

            $DynamicParameter = [System.Management.Automation.RuntimeDefinedParameter]::new(
                $Parameter.Key,
                $ParameterType,
                $AttributeCollection
            );
            Write-Verbose "Added Parameter $($Parameter.Key)";
            $DynParamDictionary.Add($Parameter.Key, $DynamicParameter);
        }
        Write-Verbose "Successfully created dynamic parameter dictionary";
        # Return the dynamic parameters
        RETURN $DynParamDictionary;
    }
}

FUNCTION Get-Provider 
{
    <#
    .Synopsis
        Get full path of the provider
    .DESCRIPTION
        Get full path of the provider by specifing "ProviderType" only or "ProviderType" and "Custom".
        - ProviderType Only : "Database" or "FileSystem"
        - ProviderType and Custom : ProviderType as "Custom" and Custom contain the full path to the custom provider.
    .PARAMETER ProviderType
        Specify the provider type that will be used for logging registry. "Database", "FileSystem" or "Custom"
    .PARAMETER Custom
        Specify full path of the custom provider. This is used with ProviderType = "Custom"
    .NOTES
        Convenstion Used for defined providers :
            Providers are found under "Providers" folder in module root directory.
        In case selected provider not found, Database provider will be used.
    #>

    Param
    (
        [Parameter(Mandatory=$true)]
        [string]
        $ProviderType,

        [Parameter(Mandatory=$false)]
        [string]
        $Custom
    )
    
    Write-Verbose "Getting provider : $ProviderType";

    IF($ProviderType -eq "Custom")
    {
        IF($Custom -eq $null)
        {
            Throw "Custom param is needed !";
        }
        IF(-not ([System.IO.File]::Exists($Custom)))
        {
            Throw "Custom Path is needed ! Make sure it's full path";
        }
        RETURN $Custom;
    }
    ELSE
    {
        $BaseDirectory = "$PSScriptRoot\Providers\";
        $Provider = Get-ChildItem -Path $BaseDirectory -Recurse -Filter "$($ProviderType)Provider.ps1";
        IF($Provider -eq $null -or $Provider.Length -eq 0)
        {
            Write-Host "$ProviderType does not exist.";
            Write-Host "Database Provider will be used.";
            $Provider = Get-ChildItem -Path $BaseDirectory -Recurse -Filter "DatabaseProvider.ps1";
        }
        RETURN $Provider.FullName;
    }
}

FUNCTION Validate-File 
{
    <#
    .Synopsis
        Validate template file
    .DESCRIPTION
        Validating template file if changed.
    .PARAMETER SourceHash
        Hash generated by Get-FileHash
    .PARAMETER Source
        Source is the source of template. (part of the path)
    .PARAMETER RegistryList
        List of registry returned from Get-Registry
    .NOTES
        It return valid in case there is no registry logged.
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true)]
        [object]
        $SourceHash,

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

        [Array]
        $RegistryList
    )

    $IsValid = $true;
    Write-Verbose "Validating file if changed";
    IF($RegistryList -eq $null)
    {
        RETURN $IsValid;
    }
    
    FOREACH($Item in $RegistryList)
    {
        IF(($Item.Hash -eq $SourceHash.Hash) -and ($Item.Source -eq $Source))
        {
            $IsValid = $false;
            break;
        }
    }
    RETURN $IsValid;
}

FUNCTION Write-Note 
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $PreMessage,

        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $Message,

        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $PostMessage,

        [ValidateSet('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')]
        [ConsoleColor]
        $ForegroundColor,

        [ValidateSet('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')]
        [ConsoleColor]
        $MessageForegroundColor,

        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [switch]
        $NoNewline = $false
    )

    Write-Host $PreMessage -ForegroundColor $ForegroundColor -NoNewline;
    Write-Host $Message -ForegroundColor $MessageForegroundColor -NoNewline;
    Write-Host $PostMessage -ForegroundColor $ForegroundColor;
    Write-Host -NoNewline:$NoNewline;
}

FUNCTION Start-DatabaseMaintenance
{
    <#
    .Synopsis
        Execute incremental sql scripts
    .DESCRIPTION
        Execute Incremental SQL scripts while setting up registry based on the specified provider.
        Registry will be used to skip SQL scripts that are already executed.
    .PARAMETER SqlQueryTimeout
        Specifies Invoke-Sqlcmd QueryTimeout
    .PARAMETER SQLScriptsPathes
        Specifies list of script pathes that contain the sql scripts.
        There is a convention to be followed inorder to use this module correctly and execute scripts in order :
        - SQLScriptsPathes should contain the path for the main folder that contains the patches ex: C:\Example\PaTcheS\R4
        - Folders inside a path of SQLScriptsPathes are named with date as "20181217" ex: C:\Example\PaTcheS\R4\20181217
        - Folders inside "20181217" should contain at least one folder with index "0000" ex: C:\Example\PaTcheS\R4\20181217\0000
        - SQL template must end with ".template" as extension
    .PARAMETER SQLServerName
        Specifies SQL server name.
    .PARAMETER SQLServerUserName
        Specifies SQL server username.
    .PARAMETER SQLServerPassword
        Specifies SQL server password.
    .PARAMETER TemplateModel
        Specifies the path of binding model.
    .PARAMETER ModelName
        Specifies the model name used for binding.
    .PARAMETER ProviderType
        Specifies the provider type that will be used for logging the execution.
    .PARAMETER Custom
        Specifies the path of the custom provider. This is used when you specify "ProviderType" as Custom.
    .PARAMETER DatabaseName
        This is required when specifing ProviderType as "Database".
        DatabaseName : specifies the database of the registry.
    .PARAMETER OutputPath
        This is required when specifing ProviderType as "FileSystem".
        OutputPath : specifies the full path of registry JSON file. ex: C:\Example\XXX.json
    .EXAMPLE
        ProviderType = Database
        Start-DatabaseMaintenance `
        -SQLScriptsPathes @("C:\Example\PaTcheS\R4") `
        -SQLServerName . `
        -SQLServerUserName sa `
        -SQLServerPassword password `
        -TemplateModel "C:\Example\XXX.json" `
        -ProviderType Database `
        -DatabaseName Saico_Portal `
        -ModelName "Model";
    .EXAMPLE
        ProviderType = FileSystem
        Start-DatabaseMaintenance `
        -SQLScriptsPathes @("C:\Example\PaTcheS\R4") `
        -SQLServerName . `
        -SQLServerUserName sa `
        -SQLServerPassword password `
        -TemplateModel "C:\Example\XXX.json" `
        -ProviderType FileSystem `
        -OutputPath "C:\Example\XXX.json" `
        -ModelName "Model";
    .EXAMPLE
        ProviderType = Custom
        Start-DatabaseMaintenance `
        -SQLScriptsPathes @("C:\Example\PaTcheS\R4") `
        -SQLServerName . `
        -SQLServerUserName sa `
        -SQLServerPassword password `
        -TemplateModel "C:\Example\XXX.json" `
        -ProviderType Custom `
        -Custom "C:\Example\XXXProvider.ps1" `
        -ModelName "Model";
    .NOTES
        Convenstion to be followed :
            Additional parameters will be requested at runtime based on specified provider.
            There is a convention to be followed inorder to use this module correctly :
            - SQLScriptsPathes should contain the path for the main folder that contains the patches ex: C:\Example\PaTcheS\R4
            - Folders inside a path of SQLScriptsPathes are named with date as "20181217" ex: C:\Example\PaTcheS\R4\20181217
            - Folders inside "20181217" should contain at least one folder with index "0000" ex: C:\Example\PaTcheS\R4\20181217\0000
            - SQL template must end with ".template" as extension
        Author: Ibrahim Al Khaled
        Date: Dec 18, 2018
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [int]
        $SqlQueryTimeout = 0,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Array]
        $SQLScriptsPaths,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $SQLServerName,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $SQLServerUserName,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $SQLServerPassword,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String]
        $TemplateModel,

        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $ModelName = "Model",

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateSet("Database", "FileSystem", "Custom")]
        [String]
        $ProviderType,

        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [String]
        $Custom
    )

    DynamicParam 
    {
        IF($ProviderType -eq $null)
        {
            throw "ProviderType is needed !!";
        }

        $ProviderFullPath = Get-Provider -ProviderType $ProviderType -Custom $Custom;
        $FolderName = [System.IO.Path]::GetFileNameWithoutExtension($(split-path -path $ProviderFullPath -Leaf));

        #$ProviderParameters = $(Get-COmmand $ProviderFullPath).Parameters;

        $DynamicParams = Get-DynamicParameters $ProviderFullPath; #Get-DynamicParams $ProviderParameters;
        return $DynamicParams;
    }

    Begin 
    {
        $ParameterObj = @{};
        FOREACH($DynamicParameter in $DynamicParams.Keys)
        {
            $ParameterObj[$DynamicParameter] = $PSCmdlet.MyInvocation.BoundParameters[$DynamicParameter];
        }

        . $ProviderFullPath @ParameterObj;
        $RegistryList = Get-Registry;
        $Buffer = @{};
        $Buffer.Succeeded = [System.Collections.ArrayList]::new();
        $Buffer.Failed = [System.Collections.ArrayList]::new();
        $Buffer.Skipped = [System.Collections.ArrayList]::new();
        Write-Host "Maintaining database releases..." -ForegroundColor Cyan;
        Write-Host;
    }

    Process
    {
        FOREACH($ReleasePath in $SQLScriptsPaths)
        {     
            IF([System.IO.Directory]::Exists($ReleasePath))
            {
                $Release = [System.IO.DirectoryInfo]::new($ReleasePath);
                Write-Note -PreMessage "Maintaining database release (" -Message $Release.Name -PostMessage ")..." -ForegroundColor DarkCyan -MessageForegroundColor White;

                $Patches = Get-ChildItem -Path $Release.FullName -Directory | Sort-Object Name;
                IF($Patches -eq $null)
                {
                    Write-Note -PreMessage "Patch folder for (" -Message $Release.Name -PostMessage ") is missing !" -ForegroundColor Magenta -MessageForegroundColor Yellow;
                }
                foreach($Patch in $Patches)
                {       
                    Write-Note -PreMessage "Maintaining database release patch (" -Message $Patch.Name -PostMessage ")..." -ForegroundColor DarkGray -MessageForegroundColor White;
                    $Indexes = Get-ChildItem -Path $Patch.FullName -Directory | Sort-Object Name;
                    IF($Indexes -eq $null)
                    {
                        Write-Note -PreMessage "Index folder for (" -Message $Patch.Name -PostMessage ") is missing !" -ForegroundColor Magenta -MessageForegroundColor Yellow;
                    }
                    foreach($Index in $Indexes)
                    {   
                        Write-Note -PreMessage "Processing index (" -Message $Index.Name -PostMessage ")..." -ForegroundColor DarkYellow -MessageForegroundColor White;
                        $Templates = Get-ChildItem -Path $Index.FullName -File -Recurse -Depth 0 -Filter '*.template' | Sort-Object LastWriteTime;
                        IF($Templates -eq $null)
                        {
                            Write-Note -PreMessage "There is no templates to be executed under (" -Message $Index.Name -PostMessage ")..." -ForegroundColor Magenta -MessageForegroundColor Yellow;
                        }
                        foreach($Template in $Templates)
                        {
                            Write-Note -PreMessage "Processing Template (" -Message $Template.Name -PostMessage ")..." -ForegroundColor DarkGreen -MessageForegroundColor White;
                            $ReleaseName = $(Split-Path $ReleasePath -leaf);
                            $Source = $("$ReleaseName\$($Patch.Name)\$($Index.Name)");
                            $SourceHash = Get-FileHash -Path $Template.FullName -Algorithm SHA256;
                            $IsValid = Validate-File -SourceHash $SourceHash -Source $Source -RegistryList $RegistryList;
                            IF($IsValid -eq $true)
                            {
                                $TemplateQuery = Get-FormattedTemplate -ModelName $ModelName -Model $TemplateModel -Path $Template.FullName -Silent:$true;
                                IF($TemplateQuery -ne $null)
                                {
                                    Write-Verbose "Executing the below QUERY...";
                                    Write-Verbose $TemplateQuery;

                                    [psobject]$Model = `
                                    @{
                                        FileName = $Template.Name;  
                                        Hash = $SourceHash.Hash;
                                        ExceptionMessage = "NULL";
                                        ExceptionDetails = "NULL";
                                        Source = $Source;
                                        CreatedDate = $(Get-Date -Format "yyyy-MM-dd HH:mm:ss");
                                    };

                                    Try 
                                    {
                                        Invoke-Sqlcmd `
                                        -AbortOnError:$false `
                                        -ErrorAction Stop `
                                        -Query $TemplateQuery `
                                        -QueryTimeout $SqlQueryTimeout `
                                        -ServerInstance $SQLServerName `
                                        -Username $SQLServerUserName `
                                        -Password $SQLServerPassword;

                                        $Buffer.Succeeded += ($Template.FullName);
                                        Write-Note -PreMessage "Template (" -Message $Template.Name -PostMessage ") executed successfully" -ForegroundColor Green -MessageForegroundColor White;
                                        Set-Registry -Model $Model;
                                    }
                                    Catch
                                    {
                                        Write-Note -PreMessage "Error while executing (" -Message $Template.Name -PostMessage ")" -ForegroundColor Magenta -MessageForegroundColor White -NoNewline;
                                        Write-Host "Error: $_.Exception" -ForegroundColor Red;
                                        Write-Host;
                                        $Buffer.Failed += ($Template.FullName);
                                        $Model.ExceptionDetails = $([string]$_.Exception).Replace("`'", "`"");
                                        $Model.ExceptionMessage = $([string]$_.Exception.Message).Replace("`'", "`"");
                                        Set-Registry -Model $Model;
                                    }
                                }
                            }
                            ELSE
                            {
                                $Buffer.Skipped += ($Template.FullName);
                                Write-Note -PreMessage "Template (" -Message $Template.Name -PostMessage ") skipped already executed." -ForegroundColor Green -MessageForegroundColor White;
                            }
                        }
                        Write-Note -PreMessage "Processing index (" -Message $Index.Name -PostMessage ") Completed" -ForegroundColor DarkYellow -MessageForegroundColor White;
                    }
                    Write-Note -PreMessage "Maintaining database release patch (" -Message $Patch.Name -PostMessage ") Completed" -ForegroundColor DarkGray -MessageForegroundColor White;
                }

                Write-Note -PreMessage "Maintaining database release (" -Message $Release.Name -PostMessage ") Completed." -ForegroundColor DarkCyan -MessageForegroundColor White;
            }
        }
    }

    End 
    {    
        Write-Host "Maintaining database releases completed..." -ForegroundColor DarkCyan;
        RETURN $Buffer;
    }
}
#endregion