Private/PgSchemaHelpers.ps1

# PgSchemaHelpers.ps1
# Funciones helper para Invoke-PgSchema

<#
.SYNOPSIS
    Lee y parsea el archivo pgschema.yaml del directorio actual.
 
.DESCRIPTION
    Valida que exista la sección 'schemas' como lista de schemas.
    Cada entrada debe tener: name, file, plan.
 
.PARAMETER Path
    Ruta al archivo pgschema.yaml. Por defecto: ./pgschema.yaml
 
.OUTPUTS
    Hashtable con la configuración parseada.
#>

function Read-PgSchemaConfig {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Path = ".\pgschema.yaml"
    )

    if (Get-Command -Name Ensure-YamlModule -ErrorAction SilentlyContinue) {
        Ensure-YamlModule
    }
    elseif (-not (Get-Command -Name ConvertFrom-Yaml -ErrorAction SilentlyContinue)) {
        try {
            Import-Module powershell-yaml -ErrorAction Stop | Out-Null
        }
        catch {
            throw "No se encontró el módulo 'powershell-yaml'. Instale con: Install-Module powershell-yaml -Scope CurrentUser -Force"
        }
    }

    if (-not (Test-Path $Path)) {
        throw "No se encontró '$Path'. Ejecute 'Invoke-PgSchema -Init' para generarlo."
    }

    $raw = Get-Content $Path -Raw
    $config = ConvertFrom-Yaml $raw

    if (-not $config.schemas -or $config.schemas.Count -eq 0) {
        throw "El archivo '$Path' no tiene la sección 'schemas' requerida (lista de schemas)."
    }

    # Validar cada entrada
    foreach ($s in $config.schemas) {
        if (-not $s.name) { throw "Cada schema en '$Path' debe tener 'name'." }
        if (-not $s.file) { throw "El schema '$($s.name)' en '$Path' no tiene 'file' (ruta al main.sql)." }
        if (-not $s.plan) { throw "El schema '$($s.name)' en '$Path' no tiene 'plan' (ruta al plan.json)." }
    }

    return $config
}

<#
.SYNOPSIS
    Construye el array de argumentos para pgschema CLI vía WSL.
 
.PARAMETER Action
    La acción de pgschema (dump, plan, apply).
 
.PARAMETER Config
    Hashtable de configuración leída de pgschema.yaml.
 
.PARAMETER EnvVars
    Hashtable de variables de entorno leídas de .env.
 
.PARAMETER ExtraArgs
    Argumentos adicionales (output paths, flags, etc.).
 
.OUTPUTS
    String[] — Array de argumentos para pgschema.
#>

function Build-PgSchemaArgs {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('dump', 'plan', 'apply')]
        [string]$Action,

        [Parameter(Mandatory)]
        [hashtable]$SchemaEntry,

        [Parameter(Mandatory)]
        [hashtable]$EnvVars,

        [Parameter()]
        [string[]]$ExtraArgs
    )

    $pgArgs = @($Action)

    # Conexión al servidor (credenciales desde .env, variables PG* estándar)
    $host_    = $EnvVars['PGHOST']
    $port     = $EnvVars['PGPORT']
    $database = $EnvVars['PGDATABASE']
    $user     = $EnvVars['PGUSER']
    $sslmode  = $EnvVars['PGSSLMODE']

    if (-not $host_)    { throw "Falta PGHOST en .env" }
    if (-not $database) { throw "Falta PGDATABASE en .env" }
    if (-not $user)     { throw "Falta PGUSER en .env" }

    $pgArgs += '--host', $host_
    if ($port) { $pgArgs += '--port', $port }
    $pgArgs += '--db', $database
    $pgArgs += '--user', $user

    # Schema desde la entrada individual
    $pgArgs += '--schema', $SchemaEntry.name

    # SSL mode
    if ($sslmode) {
        $pgArgs += '--sslmode', $sslmode
    }

    # Acción-específicos
    switch ($Action) {
        'dump' {
            # dump no necesita --file (output a stdout por defecto)
        }
        'plan' {
            $pgArgs += '--file', $SchemaEntry.file
        }
        'apply' {
            # apply acepta --file o --plan
        }
    }

    # Argumentos extra
    if ($ExtraArgs) {
        $pgArgs += $ExtraArgs
    }

    return $pgArgs
}

<#
.SYNOPSIS
    Genera los archivos de configuración pgschema.yaml y .env para un proyecto PostgreSQL.
 
.DESCRIPTION
    Crea plantillas de configuración en el directorio actual.
    pgschema.yaml contiene parámetros de esquema (versionado).
    .env contiene credenciales de conexión (NO versionado).
#>

function New-PgSchemaConfig {
    [CmdletBinding()]
    param()

    # pgschema.yaml
    if (-not (Test-Path ".\pgschema.yaml")) {
        $yamlContent = @"
# pgschema.yaml — Configuración declarativa para pgschema
# Ejecutar desde: db/ (directorio raíz de la base de datos)
# Referencia: https://www.pgschema.com
#
# pgschema trabaja schema-por-schema. Cada entrada define:
# name: nombre del schema PostgreSQL
# file: ruta al main.sql (estado deseado)
# plan: ruta al plan.json generado
 
schemas:
  - name: auth
    file: auth/main.sql
    plan: auth/plan.json
  # - name: votacion
  # file: votacion/main.sql
  # plan: votacion/plan.json
 
# Opciones compartidas de plan
plan_options:
  output_human: stdout
  # output_sql: plan.sql # Descomenta para generar script SQL
 
# Opciones compartidas de apply
apply_options:
  auto_approve: false
  # lock_timeout: 30s
 
# Opciones compartidas de dump
dump_options:
  # multi_file: false
  # no_comments: false
"@

        $yamlContent | Out-File -FilePath ".\pgschema.yaml" -Encoding utf8 -NoNewline
        Write-Host " Creado: pgschema.yaml" -ForegroundColor Green
    }
    else {
        Write-Host " Ya existe: pgschema.yaml" -ForegroundColor Yellow
    }

    # .env
    if (-not (Test-Path ".\.env")) {
        $envContent = @"
# .env — Credenciales PostgreSQL (NO versionar)
# Variables estándar PG* usadas por pgschema
 
PGHOST=localhost
PGPORT=5432
PGDATABASE=socia
PGUSER=postgres
PGPASSWORD=tu_password_aqui
# PGSSLMODE=prefer
"@

        $envContent | Out-File -FilePath ".\.env" -Encoding utf8 -NoNewline
        Write-Host " Creado: .env" -ForegroundColor Green
    }
    else {
        Write-Host " Ya existe: .env" -ForegroundColor Yellow
    }
}

<#
.SYNOPSIS
    Ejecuta pgschema dentro de WSL desde PowerShell en Windows.
 
.DESCRIPTION
    Construye el comando pgschema con los argumentos dados y lo ejecuta en WSL,
    pasando PGPASSWORD como variable de entorno (no como argumento CLI).
 
.PARAMETER Arguments
    Array de argumentos para pgschema.
 
.PARAMETER Password
    Contraseña de PostgreSQL (se pasa como PGPASSWORD env var).
 
.PARAMETER WorkingDir
    Directorio de trabajo en Windows (se convierte a ruta WSL).
 
.PARAMETER WSLDistro
    Distribución WSL a usar.
 
.OUTPUTS
    El output de pgschema.
#>

function Invoke-PgSchemaWSL {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Arguments,

        [Parameter()]
        [string]$Password,

        [Parameter()]
        [string]$WorkingDir = (Get-Location).Path,

        [Parameter()]
        [string]$WSLDistro = "Ubuntu"
    )

    # Convertir directorio de trabajo a ruta WSL
    $wslPath = ConvertTo-WSLPath -winPath $WorkingDir -WSLDistro $WSLDistro

    # Construir comando: cd al directorio + PGPASSWORD + pgschema args
    $argsString = ($Arguments | ForEach-Object {
        # Escapar comillas simples dentro de cada argumento
        $escaped = $_ -replace "'", "'\''"
        "'$escaped'"
    }) -join ' '

    $envPrefix = ""
    if ($Password) {
        # Escapar comillas simples en la contraseña
        $escapedPwd = $Password -replace "'", "'\''"
        $envPrefix = "PGPASSWORD='$escapedPwd' "
    }

    $bashCmd = "cd $wslPath && ${envPrefix}pgschema $argsString"

    Write-Verbose "WSL cmd: $bashCmd"

    # Ejecutar en WSL
    $output = & wsl.exe -d $WSLDistro -- bash -c $bashCmd 2>&1
    $exitCode = $LASTEXITCODE

    # Mostrar output
    $output | ForEach-Object { Write-Host $_ }

    if ($exitCode -ne 0) {
        throw "pgschema falló con código de salida: $exitCode"
    }

    return $output
}