ConfluenceIngest.psm1

#Requires -Module SimplySQL
#Requires -Module ConfluencePS

# Module Constants

# GUID used to identify the connection names by SimplySQL
Set-Variable ConnectionNames -option Constant -value (@{
    Automatic = (New-Guid)
    Manual = (New-Guid)
})
Function Close-SimplySQL {
<#
    .SYNOPSIS
        Closes a SimplySQL connection, by its name
 
    .PARAMETER ConnectionName
        A name to identify this Connection (default: a GUID created automatically when the Module is laoded in memory)
 
    .EXAMPLE
        # Connect to a database using a Windows Named Pipe, prompting the user for credentials
        Open-SimplySQL -Pipe MariaDb -Credential root
        Get-SimplySQLResultset -Sql 'SELECT 1'
        Close-SimplySQL
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory=$false)]
    [string]
    $ConnectionName = $ConnectionNames.Manual
)
End {
    "Closing ConnectionName: $ConnectionName" | Write-Verbose

    if((Test-SqlConnection -ConnectionName ($ConnectionName))) {
        Close-SqlConnection -ConnectionName ($ConnectionName)
    }
}
}
Function ConvertFrom-ConfluencePageBody {
<#
    .SYNOPSIS
    Converts a "Body" String from for ConfluencePS' Page functions into a XDocument object
#>

[CmdletBinding()]
[OutputType([System.Xml.Linq.XDocument])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [AllowEmptyString()]
    [String]
    $Body
)
Process {
    $XmlBody = '<xml xmlns:ac="ac" xmlns:ri="ri">{0}</xml>' -f ($Body | Repair-HtmlEntities)
    [System.Xml.Linq.XDocument]::Parse($XmlBody) | Write-Output
}
}
Function ConvertTo-ConfluencePageBody {
<#
    .SYNOPSIS
    Converts a XDocument object into a String suitable for ConfluencePS' Page functions
#>

[CmdletBinding()]
[OutputType([string])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Xml.Linq.XDocument]
    $Xml
)
Process {
    $Xml | Select-Xml -Namespace @{ac='ac';ri='ri'} -XPath '/*[1]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml | Write-Output
}
}
Function Get-SimplySQLResultset {
<#
    .SYNOPSIS
        Executes a SQL query using SimplySQL, returns a resultset if possible
 
    .PARAMETER SQL
        The SQL query to execute
 
    .PARAMETER NoResultset
        If set, the query is not expected to return a resultset (e.g. an UPDATE or DELETE statement)
 
    .PARAMETER ConnectionName
        A name to identify this Connection (default: a GUID created automatically when the Module is laoded in memory)
 
    .EXAMPLE
        # Run a select query 'Select 1 as one from dual'
        Open-SimplySQL -Pipe MariaDb -Credential root
        Get-SimplySQLResultset -Sql 'SELECT 1'
        Close-SimplySQL
 
    .EXAMPLE
        # Run a query stored in a file
        Open-SimplySQL -Pipe MariaDb -Credential root
        Get-Content -Raw C:\query.sql | Get-SimplySQLResultset
        Close-SimplySQL
 
    .EXAMPLE
        # Run a query that doesn't return a result set: 'delete from a where 1 = 0'
        Open-SimplySQL -Pipe MariaDb -Credential root
        Get-SimplySQLResultset -NonQuery -Sql 'delete from a where 1 = 0'
        Close-SimplySQL
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory,ValueFromPipeline)]
    [string]
    $SQL

    , [Parameter(Mandatory=$false)]
    [Alias('NonQuery')]
    [switch] $NoResultset

    , [Parameter(Mandatory=$false)]
    [string]
    $ConnectionName = $ConnectionNames.Manual
)
Process {
    "Querying ConnectionName: $ConnectionName" | Write-Verbose

    if(-not (Test-SqlConnection -ConnectionName ($ConnectionName))) {
        throw "Connection: $ConnectionName. Connection was not open"
    }

    if ($NonQuery) {
        Invoke-SqlUpdate -ConnectionName ($ConnectionName) -Query $SQL
    } else {
        Invoke-SqlQuery -ConnectionName ($ConnectionName) -Query $SQL
    }
}
}
Function Invoke-MariaDBClient {
<#
    .SYNOPSIS
        Invoke the mariadb client (mariadb.exe), and execute some SQL query
 
    .PARAMETER SQL
        The SQL query to execute
 
    .PARAMETER Credential
        The credential to use to connect to the database
 
    .PARAMETER Pipe
        The named pipe to use to connect to the database
     
    .PARAMETER Hostname
        The hostname to use to connect to the database
 
    .PARAMETER Port
        The port to use to connect to the database (3306 by default)
 
    .PARAMETER MariaDb
        The path to the mariadb client executable (default: 'c:\Program Files\MariaDB 11.4\bin\mariadb.exe')
 
    .EXAMPLE
        Invoke-MariaDBClient -Pipe MariaDb -Credential root -SQL 'SELECT 1'
#>

[CmdletBinding(DefaultParameterSetName='Pipe')]
param(
    [Parameter(Mandatory,ValueFromPipeline)]
    [string]
    $SQL

    , [Parameter(Mandatory)]
    [System.Management.Automation.Credential()]
    [System.Management.Automation.PSCredential]
    $Credential

    , [Parameter(Mandatory,ParameterSetName='Pipe')]
    [string]
    $Pipe

    , [Parameter(Mandatory,ParameterSetName='Server')]
    [string]
    $Hostname
    , [Parameter(Mandatory=$false,ParameterSetName='Server')]
    [int]
    $Port = 3306

    , [Parameter(Mandatory=$false)]
    [ValidateScript({ Test-Path $_ -PathType Leaf })]
    [string]
    $MariaDb = 'c:\Program Files\MariaDB 11.4\bin\mariadb.exe'
)
Begin {
    if(-not $Pipe -and -not $Hostname) {
        throw "Either a pipe or a server must be specified"
    }
    if(-not $Credential -or $Credential -eq [System.Management.Automation.PSCredential]::Empty) {
        throw "A credential must be specified"
    }
}
End {
}
Process {
    $ExtraConfig = New-TemporaryFile -ErrorAction Stop
    try {
        Set-Content -Path $ExtraConfig -Value '[client]'
        Add-Content -Path $ExtraConfig -Value "user=$($Credential.GetNetworkCredential().UserName)"
        Add-Content -Path $ExtraConfig -Value "password=$($Credential.GetNetworkCredential().Password)"
        if($Pipe) {
            Add-Content -Path $ExtraConfig -Value 'pipe'
            Add-Content -Path $ExtraConfig -Value 'protocol=pipe'
            Add-Content -Path $ExtraConfig -Value "socket=$($Pipe)"
        } elseif($Hostname) {
            Add-Content -Path $ExtraConfig -Value 'protocol=tcp'
            Add-Content -Path $ExtraConfig -Value "host=$($Hostname)"
            Add-Content -Path $ExtraConfig -Value "port=$($Port)"
        }

        $SQL | &($MariaDb) "--defaults-extra-file=$ExtraConfig" --table 2>&1 | ForEach-Object {
            if ($_ -is [System.Management.Automation.ErrorRecord]) {
                Write-Error $_
            } else {
                $_
            }
        }
    } finally {
        Remove-Item -Force -Path $ExtraConfig
    }
}
}
Function Open-SimplySQL {
<#
    .SYNOPSIS
        Open a connection with a database using SimplySQL
 
    .PARAMETER Credential
        The credential to use to connect to the database
 
    .PARAMETER ConnectionString
        The connection string to use to connect to the database
 
    .PARAMETER Pipe
        The pipe to use to connect to the database
 
    .PARAMETER Hostname
        The hostname to use to connect to the database
 
    .PARAMETER Port
        The port to use to connect to the database (3306 by default)
 
    .PARAMETER ConnectionName
        A name to identify this Connection (default: a GUID created automatically when the Module is laoded in memory)
 
    .EXAMPLE
        # Connect to a database using a Windows Named Pipe, prompting the user for credentials
        Open-SimplySQL -Pipe MariaDb -Credential root
        Get-SimplySQLResultset -Sql 'SELECT 1'
        Close-SimplySQL
 
    .EXAMPLE
        # Connect to a database using a TCP/IP connection, prompting the user for credentials
        Open-SimplySQL -Server localhost -Port 3306 -Credential root
        Get-SimplySQLResultset -Sql 'SELECT 1'
        Close-SimplySQL
 
    .EXAMPLE
        # Connect to a database using a ConnectionString, using TCP/IP, and prompting the user for credentials
        Open-SimplySQL -ConnectionString 'Server=127.0.0.1; Port=3306; Allow User Variables=true; SSLMode=Disabled' -Credential root
        Get-SimplySQLResultset -Sql 'SELECT 1'
        Close-SimplySQL
 
    .EXAMPLE
        # Connect to a database using a ConnectionString, using Named Pipes, and prompting the user for credentials
        Open-SimplySQL -ConnectionString 'Server=.; Protocol=pipe; PipeName=MariaDB; Allow User Variables=true; SSLMode=Disabled' -Credential root
        Get-SimplySQLResultset -Sql 'SELECT 1'
        Close-SimplySQL
#>

[CmdletBinding(DefaultParameterSetName='Server')]
param(
    [Parameter(Mandatory)]
    [System.Management.Automation.Credential()]
    [System.Management.Automation.PSCredential]
    $Credential

    , [Parameter(Mandatory,ParameterSetName='ConnectionString')]
    [string]
    $ConnectionString

    , [Parameter(Mandatory,ParameterSetName='Pipe')]
    [string]
    $Pipe

    , [Parameter(Mandatory,ParameterSetName='Server')]
    [string]
    $Hostname
    , [Parameter(Mandatory=$false,ParameterSetName='Server')]
    [int]
    $Port = 3306

    , [Parameter(Mandatory=$false)]
    [string]
    $ConnectionName = $ConnectionNames.Manual
)
End {
    "Opening ConnectionName: $ConnectionName" | Write-Verbose

    if((Test-SqlConnection -ConnectionName ($ConnectionName))) {
        throw "Connection: $ConnectionName. Connection is already opened"
    }

    if(-not $Credential -or $Credential -eq [System.Management.Automation.PSCredential]::Empty) {
        throw "A credential must be specified"
    }

    if($Pipe) {
        $ConnectionString = "Server=.; Protocol=pipe; PipeName=$($Pipe); Allow User Variables=true; SSLMode=Disabled"
        Open-MySqlConnection -ConnectionName ($ConnectionName) -Credential ($Credential) -ConnectionString ($ConnectionString)
    } elseif($Hostname) {
        Open-MySqlConnection -ConnectionName ($ConnectionName) -Server ($Hostname) -Port ($Port) -Credential ($Credential)
    } elseif($ConnectionString) {
        Open-MySqlConnection -ConnectionName ($ConnectionName) -ConnectionString ($ConnectionString) -Credential ($Credential)
    } else {
        throw "Either a pipe, server or ConnectionString must be specified"
    }

    if(-not (Test-SqlConnection -ConnectionName ($ConnectionName))) {
        throw "Connection: $ConnectionName. Connection failed"
    }
}
}
Function Repair-HtmlEntities {
<#
    .SYNOPSIS
        This function will convert all Named HTML entities into XML numeric entities
 
        It helps dealing with Confluence documents, as Confluence prefers Named HTML entities, that are not readily
        available/defined in xml, and therefore not understood by [System.Xml.Linq.XDocument]
 
    .PARAMETER Html
        The HTML string to convert
 
    .EXAMPLE
        'This is a &nbsp; test' | Repair-HtmlEntities
        # This is a &#160; test
#>

[CmdletBinding()]
[OutputType([string])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [String]
    $Html
)
Process {
    $NewHtml = $Html.PSObject.Copy()
    # Find all named HTML entities (&nbsp;)
    [regex]::matches($NewHtml,'&[A-Za-z][A-Za-z0-9]+;') | Select-Object -ExpandProperty Value -Unique | Foreach-Object {
        $OriginalEntity = $_
        $ReencodedEntity = [System.Net.WebUtility]::HtmlEncode([System.Net.WebUtility]::HtmlDecode($OriginalEntity))
        if($OriginalEntity -eq $ReencodedEntity) {
            # "$Originalentity would not be changed, skipping" | Write-Verbose
            return
        }
        # Replace the named html entity by a XML numeric entity (e.g. &nbsp; becomes &#160;)
        "Re-encoding $OriginalEntity into $ReencodedEntity" | Write-Verbose
        $NewHtml = $NewHtml.Replace($OriginalEntity,$ReencodedEntity)
    }
    $NewHtml | Write-Output
}
}
Function Select-ConfluenceExcerpt {
<#
    .SYNOPSIS
    Extract "Confluence Excerpt" macros from a XDocument or Node
 
    .PARAMETER Xml
        The XDocument or Node to extract the Excerpt macros from
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:structured-macro[@ac:name="excerpt"]' | Foreach-Object {
        [pscustomobject]@{
            Name = $_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="name"]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText
            Body = $_ | Select-Xml -namespace $ns -XPath './ac:rich-text-body' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml
        } | Write-Output
    }
}
}
Function Select-ConfluenceImage {
<#
    .SYNOPSIS
    Extract "Confluence Image" macros from a XDocument or Node
 
    .PARAMETER Xml
        The XDocument or Node to extract the Image macros from
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:image/ri:attachment' | Foreach-Object {
        [pscustomobject]@{
            Title = $_ | Select-Xml -namespace $ns -XPath './parent::ac:image/@ac:alt' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            Filename = $_ | Select-Xml -namespace $ns -XPath './@ri:filename' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
        } | Write-Output
    }
}
}
Function Select-ConfluenceJira {
<#
    .SYNOPSIS
    Extract "Confluence Jira" macros from a XDocument or Node
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:structured-macro[@ac:name="jira"]' | Foreach-Object {
        [pscustomobject]@{
            Key = $_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="key"]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText
            ServerId = $_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="serverId"]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText
            Server = $_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="server"]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText
        } | Where-Object {
            $_.Key -ne [string]::Empty
        } | Foreach-Object {
            $_ | Write-Output
        }
    }
}
}
Function Select-ConfluencePageLink {
<#
    .SYNOPSIS
    Extract "Confluence Page Link" macros from a XDocument or Node
 
    .PARAMETER Xml
        The XDocument or Node to extract the Page Link macros from
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:link/ri:page' | Foreach-Object {
        [pscustomobject]@{
            Title = $_ | Select-Xml -namespace $ns -XPath './@ri:content-title' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            SpaceKey = $_ | Select-Xml -namespace $ns -XPath './@ri:space-key' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            # The version number of the referenced confluence page at the moment the link was created
            PageVersion = $_ | Select-Xml -namespace $ns -XPath './@ri:version-at-save' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
        } | Write-Output
    }
}
}
Function Select-ConfluencePageLinkFollowedByVersionNumber {
<#
    .SYNOPSIS
        Like Select-PageLink, it extracts "Confluence Page Link" macros from a XDocument or Node.
        But only the Links "followed by a version number" would be returned.
        e.g.: the following XML matches:
                <p><ac:link><ri:page ri:content-title="Java" ri:space-key="ARC" ri:version-at-save="1">Java</ac:link> v8.2</p>
        -> it will be parsed into:
            @{Title=Java;SpaceKey=ARC;PageVersion=1;VersionString=v8.2;Version={Major=8;Minor=2;Build=0;Revision=0}}
 
    .PARAMETER Xml
        The XDocument or Node to extract the Page Link macros from
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:link[ri:page]' | Foreach-Object {
        [pscustomobject]@{
            Title = $_ | Select-Xml -namespace $ns -XPath './ri:page/@ri:content-title' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            SpaceKey = $_ | Select-Xml -namespace $ns -XPath './ri:page/@ri:space-key' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            PageVersion = $_ | Select-Xml -namespace $ns -XPath './ri:page/@ri:version-at-save' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty Value
            VersionString = $_ | Select-Xml -namespace $ns -xpath '(./following-sibling::text()|./following-sibling::*//text())' | select-object -ExpandProperty Node | Select-Object -ExpandProperty Value
            Version = $null
            # ParentInner = $_ | Select-Xml -namespace $ns -xpath './parent::*' | select-object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml
        } | Where-Object {
            # Is there a 'version' string immediatelly following the link?
            $_.VersionString | Select-Version
        } | Foreach-Object {
            $_.Version = $_.VersionString | Select-Version
            $_ | Write-Output
        }
    }
}
}
Function Select-ConfluencePageProperties {
<#
    .SYNOPSIS
        Extract "Confluence PageProperties" macros from a XDocument or Node
 
    .PARAMETER Xml
        The XDocument or Node to extract the PageProperties macros from
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml

    , [switch]
        $ReturnXml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:structured-macro[@ac:name="details"]' | ForEach-Object {
        $PagePropertyId = ($_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="id"]').Node.InnerText
        $_ | Select-Xml -Namespace $ns -XPath './/tr' | Foreach-Object {
            if($ReturnXml) {
                $Value = ($_ | Select-Xml -Namespace $ns -XPath './/td')
            } else {
                $Value = ($_ | select-xml -namespace $ns -xpath '(.//td//*[local-name()=name()]/text()|.//td//ac:link/ac:link-body|.//td//ac:structured-macro[@ac:name="status"]/ac:parameter[@ac:name="title"])').Node.InnerText # or: .Node.Value
            }
            if ($DebugPreference -eq 'Continue') {
                [pscustomobject]@{
                    PagePropertyId = $PagePropertyId
                    PropertyId = ($_ | Select-Xml -Namespace $ns -XPath '(.//th//*[local-name()=name()]/text()|.//th//ac:link/ac:link-body|.//th//ac:structured-macro[@ac:name="status"]/ac:parameter[@ac:name="title"])').Node.InnerText
                    Value = $value
                    PropertyIdInnerXml = ($_ | select-xml -namespace $ns -xpath './/th' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml)
                    ValueInnerXml = ($_ | select-xml -namespace $ns -xpath './/td' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerXml)
                } | Write-Output
            } else {
                [pscustomobject]@{
                    PagePropertyId = $PagePropertyId
                    PropertyId = ($_ | Select-Xml -Namespace $ns -XPath '(.//th//*[local-name()=name()]/text()|.//th//ac:link/ac:link-body|.//th//ac:structured-macro[@ac:name="status"]/ac:parameter[@ac:name="title"])').Node.InnerText
                    Value = $value
                } | Write-Output
            }
        }
    }
}
}
Function Select-ConfluenceStatus {
<#
    .SYNOPSIS
    Extract "Confluence Status" macros from a XDocument or Node
#>

[CmdletBinding()]
[OutputType([PSCustomObject[]])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [System.Object]
    $Xml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $Xml | Select-Xml -namespace $ns -XPath './/ac:structured-macro[@ac:name="status"]' | Foreach-Object {
        [pscustomobject]@{
            Color = $_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="colour"]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText
            Title = $_ | Select-Xml -namespace $ns -XPath './ac:parameter[@ac:name="title"]' | Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText
        } | Write-Output
    }
}
}
Function Select-TextFromXmlValue {
<#
    .SYNOPSIS
        Extract text from a XDocument or Node, including from special XML elements like "Confluence Status" and "Confluence Jira" macros
    .PARAMETER ValueXml
        The XDocument or Node to extract the text from
#>

[CmdletBinding()]
[OutputType([string])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [AllowNull()]
    [System.Object]
    $ValueXml
)
Begin {
    $ns = @{ac='ac';ri='ri'}
}
Process {
    $ValueXml | Where-Object { $_ -ne $null } | ForEach-Object {
        $_ |
            Select-Xml -namespace $ns -xpath '(.//*[namespace-uri()=""][text()]/text()|.//ac:link/ac:link-body|.//ac:structured-macro[@ac:name="status"]/ac:parameter[@ac:name="title"])' |
                Select-Object -ExpandProperty Node | Select-Object -ExpandProperty InnerText | Write-Output
    }
}
}
Function Select-Version {
<#
.SYNOPSIS
    Produce [version] objects out of [string] representations.
    It can convert version strings matching the following patterns:
    - 1.2.3
    - v. 1.2
    - v1.2
    - ver. 1.2.3
    - version 1.2.3
    - version: 1.2.3
 
.PARAMETER InputString
    The input string to select from
 
.PARAMETER AsString
    By default the Version property is returned as a [Version] object. If -AsString is set, the string representation of the version is sent instead
#>

[CmdletBinding()]
[OutputType([string])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [AllowEmptyString()]
    [String]
    $InputString,

    [Parameter(Mandatory=$false)]
    [switch]
    $AsString
)
Process {
    # Does the string matching a 'version' pattern?

    $VersionString = $InputString.Trim()
    if(-not ($VersionString -imatch '^\s*(?:(?:v|ver|version)\s*[\.:-]?\s*)?(\d+(?:\.\d+)*)')) {
        # No version found, skip
        return
    }
    # Convert in a [version] object
    $InputArray = ($Matches[1] -split '\.',5)
    $VersionArray = @(0,0,0,0)
    for($i = 0; ($i -lt $VersionArray.Length) -and ($i -lt $InputArray.Length); $i ++) {
        $VersionArray[$i] = [int]$InputArray[$i]
    }
    $VersionObject = ([version]::new($VersionArray -join '.'))
    if($versionObject -eq '0.0.0.0') {
        # Null version, skip
        return
    }
    if($AsString) {
        $VersionObject.ToString() | Write-Output
    } else {
        $VersionObject | Write-Output
    }
}
}
Function ConvertFrom-ConfluencePage {
<#
    .SYNOPSIS
    Converts a ConfluencePS.Page into an "Artifact" (a PSCustomObject understandable by the Portfolio module)
 
    .PARAMETER Page
    The ConfluencePS.Page object to convert
 
    .PARAMETER LabelSet
    The ConfluencePS.ContentLabelSet object to use as the LabelList of the Artifact
 
    .EXAMPLE
    # Get a ConfluencePS.Page object and convert it into an Artifact
    Get-ConfluencePage -Id 46564202 | ConvertFrom-ConfluencePage
#>

[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [ConfluencePS.Page]
    $Page

    , [Parameter(Mandatory=$false,Position=1)]
        [ConfluencePS.ContentLabelSet]
        $LabelSet
)
Process {
    if(-not $Page.Body) {
        Write-Warning ("Page {0} '{1}' has no body!" -f $Page.ID,$Page.Title)
    } else {
        # Populate a new object from confluence's page data
        $Artifact = $Page | Select-Object ID,Title,Body,Status,URL,ShortURL,
            @{label='VersionNumber';expression={[uint64]$_.Version.Number}},
            @{label='SpaceKey';expression={$_.Space.Key}},
            @{label='AuthorName';expression={$_.Version.By.DisplayName}},
            @{label='Version';expression={$_.Version | Select-Object Number,When,Message,MinorEdit}},
            @{label='Space';expression={$_.Space | Select-Object Id,Key,Name}},
            @{label='Author';expression={$_.Version.By | Select-Object DisplayName,UserKey,UserName,@{label='Avatar';expression={$_.ProfilePicture | Select-Object Path,Width,Height}}}},
            @{label='AncestorList';expression={$_.Ancestors | Select-Object ID,Title}}
        if($null -ne $LabelSet) {
            # Add labels to the confluence's page data
            $Artifact | Add-Member -MemberType NoteProperty -Name LabelList -Value ($LabelSet | Select-Object -ExpandProperty Labels | Select-Object -ExpandProperty Name)
        } else {
            $Artifact | Add-Member -MemberType NoteProperty -Name LabelList -Value ([string[]]@())
        }
        # Body as an XML XDocument object
        $Artifact | Add-Member -MemberType NoteProperty -Name BodyXml -Value ($Artifact.Body | ConvertFrom-ConfluencePageBody)

        # Remove Confluence 'Place Holders'
        $PlaceHolderXName = ([System.Xml.Linq.XNamespace]::Get('ac')+'placeholder')
        $PlaceHolderList = $Artifact.BodyXml.Descendants($PlaceHolderXName)
        while($PlaceHolderList|Measure-Object|Where-Object{$_.Count}) {
            $PlaceHolderList | Select-Object -Last 1 | Foreach-Object {
                $_.Remove()
            }
            $PlaceHolderList = $Artifact.BodyXml.Descendants($PlaceHolderXName)
        }

        # XML Extraction - Extract more information from the Confluence's page body
        # Page Properties
        $Artifact | Add-Member -MemberType NoteProperty -Name PagePropertyList -Value ($Artifact.BodyXml | Select-ConfluencePageProperties)
        # Exerpts
        $Artifact | Add-Member -MemberType NoteProperty -Name ExcerptList -Value ($Artifact.BodyXml | Select-ConfluenceExcerpt)

        # Links in Page Properties
        $PagePropertiesXml = $Artifact.BodyXml | Select-ConfluencePageProperties -ReturnXml
        # All Links within any "Page Property"
        $Artifact | Add-Member -MemberType NoteProperty -Name LinkList -Value ($PagePropertiesXml | Foreach-Object {
                $PageProperty = $_
                $PageProperty.Value | Select-ConfluencePageLink | Foreach-Object {
                    $_ | Add-Member -MemberType NoteProperty -Name PagePropertyId -Value ($PageProperty.PagePropertyId)
                    $_ | Add-Member -MemberType NoteProperty -Name PropertyId -Value ($PageProperty.PropertyId)
                    $_ | Write-Output
                }
            })
        # All "Version Links" within any "Page Property"
        $Artifact | Add-Member -MemberType NoteProperty -Name VersionLinkList -Value ($PagePropertiesXml | Foreach-Object {
                $PageProperty = $_
                $PageProperty.Value | Select-ConfluencePageLinkFollowedByVersionNumber| Foreach-Object {
                    $_ | Add-Member -MemberType NoteProperty -Name PagePropertyId -Value ($PageProperty.PagePropertyId)
                    $_ | Add-Member -MemberType NoteProperty -Name PropertyId -Value ($PageProperty.PropertyId)
                    $_ | Write-Output
                }
            })

        $Artifact | Write-Output
    }
}
}
Function Expand-Artifact {
<#
    .SYNOPSIS
    Expands an Artifact, issuing from ConvertFrom-ConfluencePage extracting the Artifact's specific data
 
    .PARAMETER Artifact
    The Artifact to expand
 
    .EXAMPLE
    # Get a ConfluencePS.Page object and convert it into an Artifact
    Get-ConfluencePage -Id 46564202 | ConvertFrom-ConfluencePage | Expand-Artifact
#>

[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [PSCustomObject]
    $Artifact
)
Begin {

    Function Select-LinkListFromXmlValue {
        <#
            .SYNOPSIS
            Selects links matching a specific name within LinkList or VersionLinkList
 
            .PARAMETER LinkName
            The name of the link that have to be selected
 
            .PARAMETER Artifact
            The Artifact containing the list of links
 
            .PARAMETER UseVersionLinkList
            If set, the function will search in the VersionLinkList instead of the LinkList
 
        #>

        [CmdletBinding()]
        [OutputType([string])]
        param(
            [Parameter(Mandatory,ValueFromPipeline,Position=0)]
            [AllowNull()]
            [string]
            $LinkName,

            [Parameter(Mandatory,Position=1)]
            [System.Object]
            $Artifact,

            [Parameter(Mandatory=$false)]
            [switch]
            $UseVersionLinkList
        )
        Begin {
            $ns = @{ac='ac';ri='ri'}
        }
        Process {
            $LinkName | Foreach-Object {
                $CurrentName = $_
                if($UseVersionLinkList) {
                    $Artifact.VersionLinkList | Where-Object {
                        $_.Title -eq $CurrentName
                    } | Write-Output
                } else {
                    $Artifact.LinkList | Where-Object {
                        $_.Title -eq $CurrentName
                    } | Write-Output
                }
            }
        }
    }

}
Process {
    # Artifact's Name and Type from Page Properties
    $Artifact.BodyXml | Select-ConfluencePageProperties -ReturnXml | Foreach-Object {
        # Product; Name is in info.Name
        if($_.PagePropertyId -eq 'info' -and $_.PropertyId -eq 'Name') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'Product' -Force
        }
        # Container; Name is in container.Container
        if($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Container') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'Container' -Force
        }
        # Technology; Name is in technology.Technology
        if ($_.PagePropertyId -eq 'technology' -and $_.PropertyId -eq 'Technology') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'Technology' -Force
        }
        # Database instance; Name is in instance.Name
        if ($_.PagePropertyId -eq 'instance' -and $_.PropertyId -eq 'Name') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'DatabaseInstance' -Force
        }
        # Actor; Name is in actor.Actor
        if ($_.PagePropertyId -eq 'actor' -and $_.PropertyId -eq 'Actor') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'Actor' -Force
        }
        # Server; Name is in server.Name
        if ($_.PagePropertyId -eq 'server' -and $_.PropertyId -eq 'Name') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'Server' -Force
        }
        # Domain; Name is in domain.Domain
        if ($_.PagePropertyId -eq 'domain' -and $_.PropertyId -eq 'Domain') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'Domain' -Force
        }
        # BoundedContext; Name is in info.Name
        if($_.PagePropertyId -eq 'context' -and $_.PropertyId -eq 'Bounded context') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'BoundedContext' -Force
        }
        # KeyDataEntity; Name is in key-data-entity.Name
        if($_.PagePropertyId -eq 'key-data-entity' -and $_.PropertyId -eq 'Name') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
            $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value 'KeyDataEntity' -Force
        }
        # Disambiguation, ...; Name is in [null].Name
        if (-not ($Artifact.ArtifactName)) {
            if (($null -eq $_.PagePropertyId -or $_.PagePropertyId -eq [string]::Empty) -and $_.PropertyId -eq 'Name') {
                $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactName -Value ($_.Value | Select-TextFromXmlValue) -Force
                $Artifact | Add-Member -MemberType NoteProperty -Name ArtifactType -Value $null  -Force
            }
        }

        # Bounded Context;
        if ($_.PagePropertyId -eq 'context' -and $_.PropertyId -eq 'Data context') {
            $Artifact | Add-Member -MemberType NoteProperty -Name BoundedContextDataContext -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'context' -and $_.PropertyId -eq 'Relations') {
            $Artifact | Add-Member -MemberType NoteProperty -Name BoundedContextRelationLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'context' -and $_.PropertyId -eq 'Data context') {
            $Artifact | Add-Member -MemberType NoteProperty -Name BoundedContextDataContextLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        # Domain;
        if ($_.PagePropertyId -eq 'domain' -and $_.PropertyId -eq 'GUID') {
            $Artifact | Add-Member -MemberType NoteProperty -Name DomainSharepointGUID -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        # Product;
        if ($_.PagePropertyId -eq 'info' -and $_.PropertyId -eq 'Description') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ProductDescription -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'info' -and $_.PropertyId -eq 'System architecture') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ProductArchitecture -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'info' -and $_.PropertyId -eq 'Status') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ProductStatus -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        # Container;
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Product') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerProductLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Type') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerType -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Authentication') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerAuthenticationLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Authorization') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerAuthorizationLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Code repository') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerCodeLink -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'URL Production') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerUri -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Hosting') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerServerLink -Value $Links -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Dependencies (internal)') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerInternalLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Dependencies (external)') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerExternalLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Actors') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerActorLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Main frameworks') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerTechnologyLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -UseVersionLinkList -Artifact $Artifact) -Force
        }
        if ($_.PagePropertyId -eq 'container' -and $_.PropertyId -eq 'Frameworks & libraries') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ContainerDependencyLink -Value ($_.Value | Select-TextFromXmlValue | Select-LinkListFromXmlValue -UseVersionLinkList -Artifact $Artifact) -Force
        }
        # Server;
        if ($_.PagePropertyId -eq 'server' -and $_.PropertyId -eq 'Os') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ServerOperatingSystem -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'server' -and $_.PropertyId -eq 'IP') {
            $Artifact | Add-Member -MemberType NoteProperty -Name ServerIPAddress -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        # Database instance;
        if ($_.PagePropertyId -eq 'instance' -and $_.PropertyId -eq 'Vendor') {
            $Artifact | Add-Member -MemberType NoteProperty -Name DatabaseVendor -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'instance' -and $_.PropertyId -eq 'Version') {
            $Artifact | Add-Member -MemberType NoteProperty -Name DatabaseVersion -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        # KeyDateEntity;

        # ??
        if (($null -eq $_.PagePropertyId -or $_.PagePropertyId -eq [string]::Empty) -and $_.PropertyId -eq 'A.k.a.') {
            $Artifact | Add-Member -MemberType NoteProperty -Name DataEntityAlias -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if (($null -eq $_.PagePropertyId -or $_.PagePropertyId -eq [string]::Empty) -and $_.PropertyId -eq 'Bounded context') {
            $Artifact | Add-Member -MemberType NoteProperty -Name DataEntityContext -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        # Technology;
        if ($_.PagePropertyId -eq 'technology' -and $_.PropertyId -eq 'Vendor') {
            $Artifact | Add-Member -MemberType NoteProperty -Name TechnologyVendor -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'technology' -and $_.PropertyId -eq 'Status') {
            $Artifact | Add-Member -MemberType NoteProperty -Name TechnologyStatus -Value ($_.Value | Select-TextFromXmlValue) -Force
        }
        if ($_.PagePropertyId -eq 'technology' -and $_.PropertyId -eq 'Outlook') {
            $Artifact | Add-Member -MemberType NoteProperty -Name TechnologyReplacementLink -Value $Links -Force
        }
        if ($_.PagePropertyId -eq 'technology-version' -and $_.PropertyId -eq 'Recommended') {
            $Artifact | Add-Member -MemberType NoteProperty -Name TechnologyReplacementLink -Value $Links -Force
        }
        # Technology; "Technology's Version Support" information
        if ($_.PagePropertyId -eq 'technology-version') {
            $VersionString =  ($_.Value | select-xml -namespace @{ac='ac';ri='ri'} -xpath ("(.//text()|.//*/text())")).Node.Value
            $Version = $VersionString | Select-Version
            if($Version -ne '0.0.0.0') {
                $Artifact | Add-Member -MemberType NoteProperty -Name ("$($_.PropertyId)Version") -Value ($Version)
                # $Artifact | Add-Member -MemberType NoteProperty -Name ("$($_.PropertyId)VersionString") -Value ($VersionString)
            }
        }
    }

    $Artifact | Write-Output
}
}
Function Export-Artifact {
<#
    .SYNOPSIS
        Stores an Artifact into the database
 
    .PARAMETER ConnectionName
        The connection name to use (See SimplySQL module's manual for more information)
 
    .PARAMETER Artifact
        The Artifact to store
 
    .EXAMPLE
        $MySqlConnection = @{
            ConnectionString = 'Server=.; Protocol=pipe; PipeName=MariaDb; Allow User Variables=true; SSLMode=Disabled'
            Credential = (Get-Credential -Message 'Provide the MariaDB user and password')
            ConnectionName = (New-GUID).ToString()
        }
        Open-MySqlConnection @MySqlConnection
        Read-Confluence | Export-Artifact -ConnectionName $ConnectionName
        Close-SqlConnection -ConnectionName $ConnectionName
#>

[CmdletBinding()]
[OutputType([void])]
param(
    [Parameter(Mandatory,Position=0)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({Test-SqlConnection -ConnectionName $_})]
    [String]
    $ConnectionName

    ,[Parameter(Mandatory,ValueFromPipeline,Position=1)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_ | Test-Artifact})]
    [object]
    $Artifact
)
Begin {
    $InsertArtifactStatement = @'
REPLACE INTO portfolio.confluence (
    Id
, Version
, Space
, Title
, Attributes
)
VALUES (
    ?Id
, ?Version
, ?Space
, ?Title
, JSON_COMPACT(?Attributes)
)
'@

    $DeleteLabelStatement = @'
DELETE FROM portfolio.label
WHERE Id = ?Id
'@

    $InsertLabelStatement = @'
INSERT INTO portfolio.label (
    Id
, Label
)
VALUES (
    ?Id
, ?Label
)
'@

    $DeleteBodyStatement = @'
DELETE FROM portfolio.body WHERE Id = ?Id
'@

    $InsertBodyStatement = @'
INSERT INTO portfolio.body (
    Id
, Body
)
VALUES (
    ?Id
, ?Body
)
'@

    $DeleteBodyXmlStatement = @'
DELETE FROM portfolio.bodyxml WHERE Id = ?Id
'@

    $InsertBodyXmlStatement = @'
INSERT INTO portfolio.bodyxml (
    Id
, Body
)
VALUES (
    ?Id
, ?Body
)
'@


    $DeleteLinkStatement = @'
DELETE FROM portfolio.Link WHERE Id = ?Id
'@

    $InsertLinkStatement = @'
INSERT INTO portfolio.Link (
    Id
, PagePropertyId
, PropertyId
, LinkTitle
, LinkSpace
, LinkVersion
, LinkId
)
VALUES (
    ?Id
, ?PagePropertyId
, ?PropertyId
, ?LinkTitle
, ?LinkSpace
, ?LinkVersion
, portfolio.get_confluence_id_by_title(?LinkIdTitle)
)
'@


    $DeleteAncestorStatement = @'
DELETE FROM portfolio.ancestor WHERE Id = ?Id
'@

    $InsertAncestorStatement = @'
INSERT INTO portfolio.ancestor (
    Id
, Ancestor
)
VALUES (
    ?Id
, ?Ancestor
)
'@

}
Process {
    "Inserting artifact for page $($Artifact.Id)" | Write-Verbose
    <#if(-not ($Artifact Test-Artifact)) {
        throw "Invalid input object"
    }#>


    try {
        Start-SqlTransaction -ConnectionName $ConnectionName

        # Delete associated content (Labels, Body and BodyXml), if any
        $DeleteAssociatedParameters = @{
            Id = $Artifact.ID
        }
        $DeleteAssociatedCount = 0
        $DeleteAssociatedCount += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $DeleteLabelStatement -Parameters $DeleteAssociatedParameters | Out-Null
        $DeleteAssociatedCount += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $DeleteBodyStatement -Parameters $DeleteAssociatedParameters | Out-Null
        $DeleteAssociatedCount += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $DeleteBodyXmlStatement -Parameters $DeleteAssociatedParameters | Out-Null
        $DeleteAssociatedCount += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $DeleteLinkStatement -Parameters $DeleteAssociatedParameters | Out-Null
        $DeleteAssociatedCount += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $DeleteAncestorStatement -Parameters $DeleteAssociatedParameters | Out-Null
        "$DeleteAssociatedCount associated rows deleted" | Write-Verbose

        # Store the artifact (without Body and BodyXml)
        $Properties = $Artifact | Get-Member -MemberType Property,NoteProperty | Select-Object -ExpandProperty Name | Where-Object {$_ -notin 'Body','BodyXml'}
        $Attributes = $Artifact | Select-Object -Property $Properties | ConvertTo-Json -Compress -Depth 10

        $InserArtifactParameters = @{
            Id = $Artifact.ID
            Version = $Artifact.VersionNumber
            Space = $Artifact.SpaceKey
            Title = $Artifact.Title
            Attributes = $Attributes
        }
        $ArtifactInserted = Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $InsertArtifactStatement -Parameters $InserArtifactParameters | Out-Null
        "$ArtifactInserted Artifact inserted" | Write-Verbose

        # Store the artifact's Body
        $InsertBodyParameters = @{
            Id = $Artifact.ID
            Body = $Artifact.Body
        }
        $BodyInserted = Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $InsertBodyStatement -Parameters $InsertBodyParameters | Out-Null
        "$BodyInserted Body inserted" | Write-Verbose

        # Store the artifact's BodyXml
        $InsertBodyXmlParameters = @{
            Id = $Artifact.ID
            Body = $Artifact.BodyXml.ToString([System.Xml.Linq.SaveOptions]::None)
        }
        $BodyXmlInserted = Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $InsertBodyXmlStatement -Parameters $InsertBodyXmlParameters | Out-Null
        "$BodyXmlInserted Body(Xml) inserted" | Write-Verbose

        # Store the acenstor's list
        $InsertAncestorParameters = @{
            Id = $Artifact.ID
            Ancestor = $Artifact | Select-Object -Property $AncestorList | ConvertTo-Json -Compress -Depth 10
        }
        $AncestorsInserted = Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $InsertAncestorStatement -Parameters $InsertAncestorParameters | Out-Null
        "$AncestorsInserted Ancestors inserted" | Write-Verbose
        

        # Store the artifact's Labels
        $LabelsInserted = 0
        $Artifact.LabelList | Where-Object { $_ } | Foreach-Object {
            $LabelsInserted += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $InsertLabelStatement -Parameters @{
                Id = $Artifact.ID
                Label = $_
            } | Out-Null
        }
        if($LabelsInserted -gt 1) {
            "$LabelsInserted Label(s) inserted" | Write-Verbose
        } elseif($LabelsInserted -eq 1) {
            "$LabelsInserted Label inserted" | Write-Verbose
        }

        # Store the artifact's Links
        $LinksInserted = 0
        $Artifact.LinkList | Where-Object { $_ } | Foreach-Object {
            $LinksInserted += Invoke-SqlUpdate -ConnectionName $ConnectionName -Query $InsertLinkStatement -Parameters @{
                Id = $Artifact.ID
                PagePropertyId = $_.PagePropertyId
                PropertyId = $_.PropertyId
                LinkTitle = $_.Title
                LinkSpace = $_.SpaceKey -as [string]
                LinkVersion = $_.PageVersion
                LinkIdTitle = $_.Title
            } | Out-Null
        }
        if($LinksInserted -gt 1) {
            "$LinksInserted Link(s) inserted" | Write-Verbose
        } elseif($LinksInserted -eq 1) {
            "$LinksInserted Link inserted" | Write-Verbose
        }

        Complete-SqlTransaction -ConnectionName $ConnectionName
    } catch {
        Undo-SqlTransaction -ConnectionName $ConnectionName
        throw $_
    }
}
}
Function Import-Artifact {
<#
    .SYNOPSIS
        Load/Import specific artifacts from the Portfolio Database
 
    .PARAMETER ConnectionName
        The connection name to use (See SimplySQL module's manual for more information)
 
    .PARAMETER WithIdIn
        The list of Ids to load
 
    .EXAMPLE
        $MySqlConnection = @{
            ConnectionString = 'Server=.; Protocol=pipe; PipeName=MariaDb; Allow User Variables=true; SSLMode=Disabled'
            Credential = (Get-Credential -Message 'Provide the MariaDB user and password')
            ConnectionName = (New-GUID).ToString()
        }
        Open-MySqlConnection @MySqlConnection
        Import-Artifact -ConnectionName $ConnectionName
        Close-SqlConnection -ConnectionName $ConnectionName
#>

[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
    [Parameter(Mandatory,Position=0)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({Test-SqlConnection -ConnectionName $_})]
    [String]
    $ConnectionName

    , [Parameter(Mandatory=$false)]
    [int[]]
    $WithIdIn
)
Begin {
    $WithIdStatement = @('SELECT 123456789 Id FROM DUAL WHERE 1 = 0')
    $WithIdStatement += ($WithIdIn | Where-Object { $null -ne $_ }| Foreach-Object {'SELECT {0} Id FROM DUAL' -f ($_ -as [int])})
    $WithIdStatement = $WithIdStatement -join ' UNION ALL '

    $SelectArtifactStatement = @'
WITH Ids AS (
    {0}
)
SELECT confluence.Id
    , confluence.Version
    , confluence.Space
    , confluence.Title
    , confluence.Attributes
    , body.Body
FROM portfolio.confluence
LEFT JOIN portfolio.body
  ON body.Id = confluence.Id
WHERE (NOT EXISTS(SELECT * FROM Ids) OR confluence.Id IN (SELECT Id FROM Ids))
'@
 -f $WithIdStatement

    $SelectArtifactStatement | Write-Debug

}
Process {
    Invoke-SqlQuery -ConnectionName $ConnectionName -Query $SelectArtifactStatement | ForEach-Object {
        $Artifact = $_.Attributes | ConvertFrom-Json
        $Artifact.ID = [uint64]$Artifact.ID
        $Artifact | Add-Member -MemberType NoteProperty -Name Body -Value ($_.Body -as [string])
        $Artifact | Add-Member -MemberType NoteProperty -Name BodyXml -Value ($_.Body | ConvertFrom-ConfluencePageBody)
        $Artifact | Write-Output
    }
}
}
Function Invoke-SimplySQL {
<#
    .SYNOPSIS
        Invoke SimplySQL: Connect to the database, execute a SQL query, Close the connection and return the resultset
 
    .PARAMETER Credential
        Database Credential [example: (Get-Credential root)]
 
    .PARAMETER Pipe
        Database Named Pipe's name [example: MariaDB]
 
    .PARAMETER Hostname
        Database server's Hostname [example: 127.0.0.1]
 
    .PARAMETER Port
        Database server's Port number [example: 3306]
 
    .PARAMETER SQL
        A single SQL query to execute
 
    .PARAMETER NonQuery
        If set, the SQL query is a statement that does not return a resultset (such an update, an insert, a delete, etc.)
 
    .EXAMPLE
        # Invoke a SELECT query and dump the results
        Invoke-SimplySQL -Pipe MariaDb -Credential root -Sql 'SELECT 1 AS a, 2 AS b, 3 AS c' | Format-Table
 
    .EXAMPLE
        # Invoke a INSERT query
        Invoke-SimplySQL -Pipe MariaDb -Credential root -Sql 'INSERT INTO test(a,b,c) VALUES (1,2,3)' -NonQuery
 
    .EXAMPLE
        # Invoke a SELECT query from a file
        Get-Content -Path MyQuery.sql | Invoke-SimplySQL -Pipe MariaDb -Credential root
#>

[CmdletBinding(DefaultParameterSetName='Pipe')]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [string]
    $SQL

    , [Parameter(Mandatory)]
    [System.Management.Automation.Credential()]
    [System.Management.Automation.PSCredential]
    $Credential

    , [Parameter(Mandatory,ParameterSetName='Pipe')]
    [string]
    $Pipe

    , [Parameter(Mandatory,ParameterSetName='Server')]
    [string]
    $Hostname
    , [Parameter(Mandatory=$false,ParameterSetName='Server')]
    [int]
    $Port = 3306

    , [Parameter(Mandatory=$false)]
    [switch] $NonQuery
)
Begin {
    $ConnectionName = $ConnectionNames.Automatic
    "Using ConnectionName: $ConnectionName" | Write-Verbose
    $AggregatedSQL = @()
}
Process {
    $AggregatedSQL += $SQL
}
End {
    try {
        $OpenArguments = [hashtable]$PSBoundParameters
        $OpenArguments += @{ConnectionName = $ConnectionName}
        $OpenArguments.Remove('SQL')
        $OpenArguments.Remove('NonQuery')
        Open-SimplySQL @OpenArguments

        Get-SimplySQLResultset -SQL ($AggregatedSQL -join "`n") -NonQuery:$NonQuery -ConnectionName $ConnectionName
    } finally {
        Close-SimplySQL -ConnectionName $ConnectionName
    }
}
}
Function Read-Confluence {
<#
    .SYNOPSIS
    Reads confluence into PSCustomObjects processable by the Portfolio module
 
    .EXAMPLE
    # Reads a specific page from Confluence using it's ID
    Read-Confluence -Credential $Credential -CQL "id = 46564202"
 
    .EXAMPLE
    # Reads all pages from Confluence in space ARC (without loading the labels)
    Read-Confluence -Credential $Credential -SkipLabels -CQL "SPACE = ARC"
 
    .EXAMPLE
    # Reads all pages tagged with 'product' under the ARC space
    Read-Confluence -Credential $Credential -SkipLabels -CQL "SPACE = ARC and label = 'product'"
     
    .EXAMPLE
    # Prints a table of all 'actors' in the ARC space
    Read-Confluence -Credential $Credential -CQL "SPACE = ARC and title ~ 'actor'" | Select-Object ID,ArtifactType,ArtifactName,Title | Format-Table
#>

[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
    [Parameter(Mandatory)]
    [System.Management.Automation.Credential()]
    [System.Management.Automation.PSCredential]
    $Credential

    , [Parameter()]
        [switch]
        $SkipLabels

    , [Parameter(Mandatory=$false)]
        [string] $ConfluenceApiUri = 'https://eesc-cor.atlassian.net/wiki/rest/api'

    , [Parameter(Mandatory=$false)]
        [string] $CQL = "ancestor = 141754953"
        # [int] $PortfolioPageId = 141754953
)
Begin {
    # $Query = "ancestor = $PortfolioPageId and Id = 91750457"
    # $Query = "ancestor = $PortfolioPageId "
    $Query = $CQL
}
End {

    Get-ConfluencePage -Query $Query -ApiUri $ConfluenceApiUri -Credential $Credential | Foreach-Object {
        if( -not $SkipLabels ) {
            $Labels = Get-ConfluenceLabel -Id $_.ID -ApiUri $ConfluenceApiUri -Credential $Credential
        } else {
            $Labels = [ConfluencePS.ContentLabelSet]::new()
        }
        $_ | ConvertFrom-ConfluencePage -LabelSet $Labels | Expand-Artifact | Write-Output
    }
}
}
Function Test-Artifact {
<#
    .SYNOPSIS
    Test a Portfolio's PSCustomObject (e.g. a page) checking that it is a valid Portfolio Artifact
 
    .PARAMETER Artifact
    The Portfolio's PSCustomObject to test
 
    .EXAMPLE
    Get-ConfluencePage -Id 46564202 | ConvertFrom-ConfluencePage | Expand-Artifact | Test-Artifact
#>

[CmdletBinding()]
[OutputType([bool])]
param(
    [Parameter(Mandatory,ValueFromPipeline,Position=0)]
    [PSCustomObject]
    $Artifact
)
Process {
    $Artifact.ID -is [uint64]               -and
    $Artifact.ID -ge 1                      -and
    $null -ne $Artifact.Title               -and
    $Artifact.Title -ne [string]::Empty     -and
    $null -ne $Artifact.SpaceKey            -and
    $Artifact.SpaceKey -ne [string]::Empty  -and
    $null -ne $Artifact.Body                -and
    $Artifact.Body -ne [string]::Empty      -and
    $null -ne $Artifact.BodyXml
}
}
Function Write-Confluence {
<#
    .SYNOPSIS
    Write a portfolio PSCustomObject back into confluence
    The function is intended to save the object in it's original confluence page, as a new version, not to create a new page.
    In the event where a newer version of the page exists in confluence, the function will refuse to overwrite it unless the -Force parameter is set.
 
    .PARAMETER Artifact
    The Portfolio's PSCustomObject to write back into confluence
 
    .PARAMETER StoreUnparsedBody
    If set, Write the content of $_.Body (HTML string) instead of $_.BodyXml (XDocument object)
 
    .PARAMETER SkipLabels
    If set, don't write the labels of the page
 
    .PARAMETER Force
    If set, force the update of the page even if the version number doesn't match
 
    .PARAMETER Credential
    The credential to use to authenticate with Confluence
 
    .PARAMETER ConfluenceApiUri
    The URI of the Confluence API to use
 
    .EXAMPLE
    Read-Confluence -Credential $Credential -CQL "id = 46564202" | Write-Confluence -Credential $Credential -Force
#>

[CmdletBinding(SupportsShouldProcess = $true,ConfirmImpact="High")]
[OutputType([PSCustomObject])]
param(
    [Parameter(Mandatory=$true,ValueFromPipeline,Position=0)]
    [ValidateScript({$_ | Test-Artifact})]
    [pscustomobject] $Artifact

    , [Parameter()]
        [switch]
        $StoreUnparsedBody

    , [Parameter()]
        [switch]
        $SkipLabels

    , [Parameter()]
        [switch]
        $Force

    , [Parameter(Mandatory)]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]
        $Credential

    , [Parameter(Mandatory=$false)]
        [string]
        $ConfluenceApiUri = 'https://eesc-cor.atlassian.net/wiki/rest/api'
)
Process {
    $Page = Get-ConfluencePage -ID $Artifact.ID -ApiUri $ConfluenceApiUri -Credential $Credential
    if( -not $Page ) {
        throw "Couldn't find a Confluence page with Id: $($Artifact.ID)"
    }
    if ((-not ($Force)) -and ($Page.Version.Number) -ne $Artifact.VersionNumber ) {
        throw "Confluence has a different version of the page with Id: $($Artifact.ID) (Ours: $($Artifact.VersionNumber), Confluence's: $($Page.Version.Number))"
    }

    try {
        if($StoreUnparsedBody) {
            if($pscmdlet.ShouldProcess("Overwrite confluence page $($Artifact.ID) with our unparsed HTML body")) {
                Set-ConfluencePage -ID $Artifact.ID -ApiUri $ConfluenceApiUri -Credential $Credential -Title ($Artifact.Title) -Body ($Artifact.Body) -ErrorAction Stop
            }
        } else {
            if($pscmdlet.ShouldProcess("Overwrite confluence page $($Artifact.ID)")) {
                Set-ConfluencePage -ID $Artifact.ID -ApiUri $ConfluenceApiUri -Credential $Credential -Title ($Artifact.Title) -Body ($Artifact.BodyXml | ConvertTo-ConfluencePageBody) -ErrorAction Stop
            }
        }
        if( -not $SkipLabels ) {
            if( $null -eq $Artifact.Labels -or $Artifact.Labels.Count -eq 0 ) {
                if($pscmdlet.ShouldProcess("Remove all labels from confluence page $($Artifact.ID)")) {
                    Remove-ConfluenceLabel -ID $Artifact.ID -ApiUri $ConfluenceApiUri -Credential $Credential -ErrorAction Stop
                }
            } else {
                if($pscmdlet.ShouldProcess("Overwrite labels of confluence page $($Artifact.ID)")) {
                    Set-ConfluenceLabel -ID $Artifact.ID -ApiUri $ConfluenceApiUri -Credential $Credential -Label ($Artifact.Labels) -ErrorAction Stop
                }
            }
        }
    } catch {
        throw $_
    }
}
}