Private/Clients/NAVGitClientHelpers.ps1

function Get-NAVGitClientContext {
    <#
    .SYNOPSIS
        Common start of the client commands: configuration and saved container state, without elevation or docker.
        Returns [pscustomobject]@{ Config; State }.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path
    )

    $config = Get-NAVGitConfig -Path $Path
    $state = Get-NAVGitContainerState -ContainerName $config.containerName
    if (-not $state) {
        throw "No container information for '$($config.containerName)'. Build the container with NAVGit (New-NAVGitContainer), or run Update-NAVGitContainerState in an elevated session if it was created another way."
    }
    if (-not $state.clientFolder -or -not (Test-Path -LiteralPath $state.clientFolder -PathType Container)) {
        throw "The Windows client for '$($config.containerName)' was not found on this machine (container built without includeCSide?). Rebuild the container or run Update-NAVGitContainerState."
    }
    if (-not (Test-NAVGitContainerReachable -HostName $config.containerName -Port $state.clientServicesPort)) {
        Write-NAVGitLog "Container '$($config.containerName)' does not answer on port $($state.clientServicesPort). Is it running? (docker start $($config.containerName))" -Level Warning
    }

    return [pscustomobject]@{ Config = $config; State = $state }
}

function Test-NAVGitContainerReachable {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [string] $HostName,

        [Parameter(Mandatory)]
        [int] $Port,

        [int] $TimeoutMilliseconds = 2000
    )

    $client = New-Object System.Net.Sockets.TcpClient
    try {
        $connect = $client.BeginConnect($HostName, $Port, $null, $null)
        if (-not $connect.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)) {
            return $false
        }
        $client.EndConnect($connect)
        return $true
    }
    catch {
        return $false
    }
    finally {
        $client.Close()
    }
}

function Get-NAVGitFinSqlArguments {
    <#
    .SYNOPSIS
        finsql.exe arguments for the container, following BcContainerHelper's C/SIDE shortcut, plus the NAV server
        settings so C/SIDE can synchronize table schema when a table is saved.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [hashtable] $State
    )

    $ntAuthentication = '0'
    if ($State.auth -eq 'Windows') { $ntAuthentication = '1' }

    $arguments = @(
        "servername=`"$($State.hostDatabaseServer)`""
        "database=`"$($State.databaseName)`""
        "ntauthentication=$ntAuthentication"
        "ID=`"$($State.containerName)`""
        "navservername=`"$($State.containerName)`""
        "navserverinstance=`"$($State.serverInstance)`""
        "navservermanagementport=$($State.managementServicesPort)"
    )
    $major = 0
    if ($State.navVersion) { $major = ([version]$State.navVersion).Major }
    if ($State.enableSymbolLoading -and $major -ge 11) {
        $arguments += 'generatesymbolreference=1'
    }
    return ($arguments -join ',')
}

function Get-NAVGitClientArguments {
    <#
    .SYNOPSIS
        Microsoft.Dynamics.Nav.Client.exe arguments: the ClientUserSettings.config prepared by BcContainerHelper and
        optionally a profile.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory)]
        [hashtable] $State,

        [string] $ProfileId
    )

    $arguments = @("-settings:`"$(Join-Path $State.clientFolder 'ClientUserSettings.config')`"")
    if ($ProfileId) {
        $arguments += "-profile:`"$ProfileId`""
    }
    return $arguments
}

function Invoke-NAVGitSql {
    <#
    .SYNOPSIS
        Runs a query from the host against the container database (reachable by container name thanks to updateHosts).
        Uses integrated security for Windows containers and 'sa' with the container password for NavUserPassword.
        No elevation or docker access needed. Returns rows as PSCustomObjects.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable] $State,

        [Parameter(Mandatory)]
        [string] $Query,

        [pscredential] $Credential,

        [int] $TimeoutSeconds = 15
    )

    $builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
    $builder['Data Source'] = $State.hostDatabaseServer
    $builder['Initial Catalog'] = $State.databaseName
    $builder['Connect Timeout'] = $TimeoutSeconds
    if ($State.auth -eq 'Windows') {
        $builder['Integrated Security'] = $true
    }
    else {
        if (-not $Credential) {
            throw 'A credential is required for NavUserPassword containers.'
        }
        $builder['User ID'] = 'sa'
        $builder['Password'] = $Credential.GetNetworkCredential().Password
    }

    $connection = New-Object System.Data.SqlClient.SqlConnection($builder.ConnectionString)
    try {
        $connection.Open()
        $command = $connection.CreateCommand()
        $command.CommandText = $Query
        $command.CommandTimeout = $TimeoutSeconds
        $table = New-Object System.Data.DataTable
        $null = $table.Load($command.ExecuteReader())

        return @(foreach ($row in $table.Rows) {
                $item = [ordered]@{}
                foreach ($column in $table.Columns) {
                    $value = $row[$column]
                    if ($value -is [System.DBNull]) { $value = $null }
                    $item[$column.ColumnName] = $value
                }
                [pscustomobject]$item
            })
    }
    catch {
        throw "SQL query against $($State.hostDatabaseServer)/$($State.databaseName) failed: $($_.Exception.Message)"
    }
    finally {
        $connection.Dispose()
    }
}