Functions/Invoke-SqlPackage.ps1

function Invoke-SqlPackage {
    <#
    .SYNOPSIS
        Ejecuta acciones de SqlPackage para despliegue declarativo de bases de datos SQL Server.
 
    .DESCRIPTION
        Cmdlet que envuelve sqlpackage.exe con configuración declarativa via sqlpackage.yaml.
        Soporta las 6 acciones principales de SqlPackage más un inicializador de plantillas.
 
        Las credenciales se leen del archivo .env (gitignored).
        Los parámetros de comportamiento se leen de sqlpackage.yaml (versionado).
 
        Debe ejecutarse desde el directorio del SQL Project (donde está el .sqlproj).
 
    .PARAMETER Init
        Genera los archivos de configuración (sqlpackage.yaml y .env) en el directorio actual.
        Requiere que exista un archivo .sqlproj.
 
    .PARAMETER Publish
        Despliega el .dacpac al servidor. Compila el proyecto, genera un DeployReport previo,
        solicita confirmación y ejecuta el publish.
 
    .PARAMETER DeployReport
        Genera un reporte XML con las diferencias entre el código y el servidor (dry-run).
        No modifica la base de datos.
 
    .PARAMETER Script
        Genera el archivo .sql exacto que Publish ejecutaría.
        No modifica la base de datos.
 
    .PARAMETER Extract
        Captura el esquema actual del servidor y genera un .dacpac snapshot.
        No modifica la base de datos.
 
    .PARAMETER Export
        Exporta esquema y datos del servidor a un archivo .bacpac.
        No modifica la base de datos.
 
    .PARAMETER Import
        Importa un archivo .bacpac (esquema + datos) al servidor.
        Modifica la base de datos.
 
    .EXAMPLE
        Invoke-SqlPackage -Init
 
        Genera sqlpackage.yaml y .env en el directorio actual del SQL Project.
 
    .EXAMPLE
        Invoke-SqlPackage -DeployReport
 
        Muestra qué cambios se aplicarían sin modificar la base de datos.
 
    .EXAMPLE
        Invoke-SqlPackage -Publish
 
        Compila, muestra cambios, pide confirmación y despliega.
 
    .EXAMPLE
        Invoke-SqlPackage -Script
 
        Genera el archivo .sql con el SQL exacto que se ejecutaría.
 
    .EXAMPLE
        Invoke-SqlPackage -Extract
 
        Captura el esquema actual del servidor como snapshot .dacpac.
 
    .NOTES
        Requiere:
        - sqlpackage.exe en el PATH
        - dotnet SDK (para compilar el SQL Project)
        - Archivos sqlpackage.yaml y .env configurados (usar -Init para generarlos)
 
        Referencia: https://learn.microsoft.com/sql/tools/sqlpackage
        Author: @ccisnedev
        Version: 1.0.1
    #>

    [CmdletBinding(DefaultParameterSetName = 'Publish')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Init',
            HelpMessage = "Genera archivos de configuración (sqlpackage.yaml y .env)")]
        [switch]$Init,

        [Parameter(Mandatory, ParameterSetName = 'Publish',
            HelpMessage = "Despliega el .dacpac al servidor (build → report → confirm → publish)")]
        [switch]$Publish,

        [Parameter(Mandatory, ParameterSetName = 'DeployReport',
            HelpMessage = "Genera reporte XML de diferencias (dry-run)")]
        [switch]$DeployReport,

        [Parameter(Mandatory, ParameterSetName = 'Script',
            HelpMessage = "Genera el script SQL que Publish ejecutaría")]
        [switch]$Script,

        [Parameter(Mandatory, ParameterSetName = 'Extract',
            HelpMessage = "Captura el esquema actual del servidor como .dacpac")]
        [switch]$Extract,

        [Parameter(Mandatory, ParameterSetName = 'Export',
            HelpMessage = "Exporta esquema + datos del servidor como .bacpac")]
        [switch]$Export,

        [Parameter(Mandatory, ParameterSetName = 'Import',
            HelpMessage = "Importa un archivo .bacpac al servidor")]
        [switch]$Import
    )

    begin {
        $ErrorActionPreference = 'Stop'
    }

    process {
        # Banner
        Write-Host ""
        Write-Host "╔══════════════════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║ SqlPackage — macss-devops ║" -ForegroundColor Cyan
        Write-Host "╚══════════════════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""

        switch ($PSCmdlet.ParameterSetName) {
            'Init' {
                # Validar que existe .sqlproj
                $sqlproj = Get-ChildItem -Path "." -Filter "*.sqlproj" -File | Select-Object -First 1
                if (-not $sqlproj) {
                    throw "No se encontró un archivo .sqlproj en el directorio actual. Ejecute este cmdlet desde un SQL Project."
                }

                Write-Host " Inicializando configuración para: $($sqlproj.Name)" -ForegroundColor Cyan
                Write-Host ""
                New-SqlPackageConfig
                Write-Host ""
                Write-Host " Configuración creada. Edite los archivos según su entorno:" -ForegroundColor Green
                Write-Host " sqlpackage.yaml → parámetros de SqlPackage (versionar)" -ForegroundColor DarkGray
                Write-Host " .env → credenciales del servidor (NO versionar)" -ForegroundColor DarkGray
                Write-Host ""
            }

            'Publish' {
                # Validar prerequisitos
                $sqlproj = Get-ChildItem -Path "." -Filter "*.sqlproj" -File | Select-Object -First 1
                if (-not $sqlproj) { throw "No se encontró .sqlproj en el directorio actual." }
                if (-not (Test-Path ".\sqlpackage.yaml")) { throw "No se encontró sqlpackage.yaml. Ejecute 'Invoke-SqlPackage -Init'." }
                if (-not (Test-Path ".\.env")) { throw "No se encontró .env. Ejecute 'Invoke-SqlPackage -Init'." }

                $config = Read-SqlPackageConfig
                $envConfig = Read-DotEnv -Path ".\.env"
                $envVars = $envConfig.Env

                try {
                    Write-Host " Servidor: $($envVars['DB_SERVER'])" -ForegroundColor Cyan
                    Write-Host " Base datos: $($envVars['DB_NAME'])" -ForegroundColor Cyan
                    Write-Host " Usuario: $($envVars['DB_USER'])" -ForegroundColor Cyan
                    Write-Host ""

                    # 1. Build
                    Write-Host " Compilando proyecto..." -ForegroundColor Cyan
                    $buildResult = dotnet build 2>&1
                    if ($LASTEXITCODE -ne 0) {
                        Write-Host " ERROR: La compilación falló" -ForegroundColor Red
                        $buildResult | Write-Host
                        throw "Build fallido"
                    }
                    Write-Host " Build exitoso" -ForegroundColor Green
                    Write-Host ""

                    # 2. DeployReport
                    Write-Host " Generando reporte de cambios..." -ForegroundColor Cyan
                    $dacpacPath = Find-DacpacPath
                    if (-not (Test-Path $dacpacPath)) { throw "No se encontró $dacpacPath" }

                    # Crear directorio de salida si está configurado y no existe
                    $outputDir = "."
                    if ($config.deployReport -and $config.deployReport.outputDir) {
                        $outputDir = $config.deployReport.outputDir
                    }
                    if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null }
                    $reportPath = Join-Path $outputDir "deploy_report_$(Get-Date -Format 'yyyyMMdd_HHmmss').xml"
                    $reportArgs = Build-SqlPackageArgs -Action 'DeployReport' -Config $config -EnvVars $envVars -DacpacPath $dacpacPath -OutputPath $reportPath

                    & sqlpackage @reportArgs 2>&1 | Tee-Object -Variable reportOutput
                    if ($LASTEXITCODE -ne 0) {
                        throw "No se pudo generar el reporte. Verifique la conexión y permisos."
                    }

                    # 3. Mostrar cambios
                    $changes = Show-DeployReport -ReportPath $reportPath
                    if ($null -eq $changes) {
                        Remove-Item $reportPath -ErrorAction SilentlyContinue
                        return
                    }

                    # 4. Confirmación
                    $confirm = Read-Host " ¿Aplicar estos cambios? (S/N)"
                    if ($confirm -notmatch '^[Ss]$') {
                        Write-Host " Despliegue cancelado." -ForegroundColor Yellow
                        Remove-Item $reportPath -ErrorAction SilentlyContinue
                        return
                    }

                    # 5. Publish
                    Write-Host ""
                    Write-Host " Iniciando despliegue..." -ForegroundColor Cyan
                    $publishArgs = Build-SqlPackageArgs -Action 'Publish' -Config $config -EnvVars $envVars -DacpacPath $dacpacPath

                    & sqlpackage @publishArgs 2>&1 | Tee-Object -Variable publishOutput
                    if ($LASTEXITCODE -eq 0) {
                        Write-Host ""
                        Write-Host " Despliegue completado exitosamente" -ForegroundColor Green
                        Remove-Item $reportPath -ErrorAction SilentlyContinue
                    }
                    else {
                        throw "El despliegue falló con código de salida: $LASTEXITCODE"
                    }
                }
                finally {
                    # Limpiar contraseña de memoria
                    if ($envVars) { $envVars['DB_PASSWORD'] = $null }
                    [System.GC]::Collect()
                }
            }

            'DeployReport' {
                # Validar prerequisitos
                if (-not (Test-Path ".\sqlpackage.yaml")) { throw "No se encontró sqlpackage.yaml. Ejecute 'Invoke-SqlPackage -Init'." }
                if (-not (Test-Path ".\.env")) { throw "No se encontró .env. Ejecute 'Invoke-SqlPackage -Init'." }

                $config = Read-SqlPackageConfig
                $envConfig = Read-DotEnv -Path ".\.env"
                $envVars = $envConfig.Env
                $outputDir = "."
                if ($config.deployReport -and $config.deployReport.outputDir) {
                    $outputDir = $config.deployReport.outputDir
                }
                if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null }

                try {
                    Write-Host " Servidor: $($envVars['DB_SERVER'])" -ForegroundColor Cyan
                    Write-Host " Base datos: $($envVars['DB_NAME'])" -ForegroundColor Cyan
                    Write-Host " Modo: SOLO REPORTE" -ForegroundColor Cyan
                    Write-Host ""

                    # Build
                    Write-Host " Compilando proyecto..." -ForegroundColor Cyan
                    $buildResult = dotnet build 2>&1
                    if ($LASTEXITCODE -ne 0) {
                        Write-Host " ERROR: La compilación falló" -ForegroundColor Red
                        $buildResult | Write-Host
                        throw "Build fallido"
                    }
                    Write-Host " Build exitoso" -ForegroundColor Green
                    Write-Host ""

                    $dacpacPath = Find-DacpacPath
                    if (-not (Test-Path $dacpacPath)) { throw "No se encontró $dacpacPath" }

                    $reportPath = Join-Path $outputDir "deploy_report_$(Get-Date -Format 'yyyyMMdd_HHmmss').xml"

                    Write-Host " Generando reporte de cambios..." -ForegroundColor Cyan
                    $reportArgs = Build-SqlPackageArgs -Action 'DeployReport' -Config $config -EnvVars $envVars -DacpacPath $dacpacPath -OutputPath $reportPath

                    & sqlpackage @reportArgs 2>&1 | Tee-Object -Variable reportOutput
                    if ($LASTEXITCODE -ne 0) {
                        throw "No se pudo generar el reporte. Verifique la conexión y permisos."
                    }

                    $changes = Show-DeployReport -ReportPath $reportPath
                    Write-Host " Reporte guardado en: $reportPath" -ForegroundColor DarkGray
                    Write-Host ""
                }
                finally {
                    # Limpiar contraseña de memoria
                    if ($envVars) { $envVars['DB_PASSWORD'] = $null }
                    [System.GC]::Collect()
                }
            }

            'Script' {
                if (-not (Test-Path ".\sqlpackage.yaml")) { throw "No se encontró sqlpackage.yaml. Ejecute 'Invoke-SqlPackage -Init'." }
                if (-not (Test-Path ".\.env")) { throw "No se encontró .env. Ejecute 'Invoke-SqlPackage -Init'." }

                $config = Read-SqlPackageConfig
                $envConfig = Read-DotEnv -Path ".\.env"
                $envVars = $envConfig.Env
                $outputDir = "."
                if ($config.script -and $config.script.outputDir) {
                    $outputDir = $config.script.outputDir
                }
                if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null }

                try {
                    Write-Host " Servidor: $($envVars['DB_SERVER'])" -ForegroundColor Cyan
                    Write-Host " Base datos: $($envVars['DB_NAME'])" -ForegroundColor Cyan
                    Write-Host " Modo: GENERAR SCRIPT SQL" -ForegroundColor Cyan
                    Write-Host ""

                    # Build
                    Write-Host " Compilando proyecto..." -ForegroundColor Cyan
                    $buildResult = dotnet build 2>&1
                    if ($LASTEXITCODE -ne 0) {
                        Write-Host " ERROR: La compilación falló" -ForegroundColor Red
                        $buildResult | Write-Host
                        throw "Build fallido"
                    }
                    Write-Host " Build exitoso" -ForegroundColor Green
                    Write-Host ""

                    $dacpacPath = Find-DacpacPath
                    if (-not (Test-Path $dacpacPath)) { throw "No se encontró $dacpacPath" }

                    $scriptPath = Join-Path $outputDir "deploy_script_$(Get-Date -Format 'yyyyMMdd_HHmmss').sql"

                    Write-Host " Generando script SQL..." -ForegroundColor Cyan
                    $scriptArgs = Build-SqlPackageArgs -Action 'Script' -Config $config -EnvVars $envVars -DacpacPath $dacpacPath -OutputPath $scriptPath

                    & sqlpackage @scriptArgs 2>&1 | Tee-Object -Variable scriptOutput
                    if ($LASTEXITCODE -ne 0) {
                        throw "No se pudo generar el script. Verifique la conexión y permisos."
                    }

                    Write-Host ""
                    Write-Host " Script generado en: $scriptPath" -ForegroundColor Green
                    Write-Host ""
                }
                finally {
                    # Limpiar contraseña de memoria
                    if ($envVars) { $envVars['DB_PASSWORD'] = $null }
                    [System.GC]::Collect()
                }
            }

            'Extract' {
                if (-not (Test-Path ".\.env")) { throw "No se encontró .env. Ejecute 'Invoke-SqlPackage -Init'." }
                if (-not (Test-Path ".\sqlpackage.yaml")) { throw "No se encontró sqlpackage.yaml. Ejecute 'Invoke-SqlPackage -Init'." }

                $config = Read-SqlPackageConfig
                $envConfig = Read-DotEnv -Path ".\.env"
                $envVars = $envConfig.Env

                try {
                    Write-Host " Servidor: $($envVars['DB_SERVER'])" -ForegroundColor Cyan
                    Write-Host " Base datos: $($envVars['DB_NAME'])" -ForegroundColor Cyan
                    Write-Host " Modo: EXTRACT (snapshot)" -ForegroundColor Cyan
                    Write-Host ""

                    $outputDir = "./snapshots"
                    if ($config.extract -and $config.extract.outputDir) {
                        $outputDir = $config.extract.outputDir
                    }
                    if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null }

                    $dbName = $envVars['DB_NAME']
                    $extractPath = Join-Path $outputDir "$($dbName)_$(Get-Date -Format 'yyyyMMdd_HHmmss').dacpac"

                    Write-Host " Extrayendo esquema del servidor..." -ForegroundColor Cyan
                    $extractArgs = Build-SqlPackageArgs -Action 'Extract' -Config $config -EnvVars $envVars -OutputPath $extractPath

                    & sqlpackage @extractArgs 2>&1 | Tee-Object -Variable extractOutput
                    if ($LASTEXITCODE -ne 0) {
                        throw "No se pudo extraer el esquema. Verifique la conexión y permisos."
                    }

                    Write-Host ""
                    Write-Host " Snapshot guardado en: $extractPath" -ForegroundColor Green
                    Write-Host ""
                }
                finally {
                    # Limpiar contraseña de memoria
                    if ($envVars) { $envVars['DB_PASSWORD'] = $null }
                    [System.GC]::Collect()
                }
            }

            'Export' {
                if (-not (Test-Path ".\.env")) { throw "No se encontró .env. Ejecute 'Invoke-SqlPackage -Init'." }
                if (-not (Test-Path ".\sqlpackage.yaml")) { throw "No se encontró sqlpackage.yaml. Ejecute 'Invoke-SqlPackage -Init'." }

                $config = Read-SqlPackageConfig
                $envConfig = Read-DotEnv -Path ".\.env"
                $envVars = $envConfig.Env

                try {
                    Write-Host " Servidor: $($envVars['DB_SERVER'])" -ForegroundColor Cyan
                    Write-Host " Base datos: $($envVars['DB_NAME'])" -ForegroundColor Cyan
                    Write-Host " Modo: EXPORT (esquema + datos)" -ForegroundColor Cyan
                    Write-Host ""

                    $outputDir = "./exports"
                    if ($config.export -and $config.export.outputDir) {
                        $outputDir = $config.export.outputDir
                    }
                    if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null }

                    $dbName = $envVars['DB_NAME']
                    $exportPath = Join-Path $outputDir "$($dbName)_$(Get-Date -Format 'yyyyMMdd_HHmmss').bacpac"

                    Write-Host " Exportando esquema y datos..." -ForegroundColor Cyan
                    $exportArgs = Build-SqlPackageArgs -Action 'Export' -Config $config -EnvVars $envVars -OutputPath $exportPath

                    & sqlpackage @exportArgs 2>&1 | Tee-Object -Variable exportOutput
                    if ($LASTEXITCODE -ne 0) {
                        throw "No se pudo exportar. Verifique la conexión y permisos."
                    }

                    Write-Host ""
                    Write-Host " Exportación guardada en: $exportPath" -ForegroundColor Green
                    Write-Host ""
                }
                finally {
                    # Limpiar contraseña de memoria
                    if ($envVars) { $envVars['DB_PASSWORD'] = $null }
                    [System.GC]::Collect()
                }
            }

            'Import' {
                if (-not (Test-Path ".\.env")) { throw "No se encontró .env. Ejecute 'Invoke-SqlPackage -Init'." }
                if (-not (Test-Path ".\sqlpackage.yaml")) { throw "No se encontró sqlpackage.yaml. Ejecute 'Invoke-SqlPackage -Init'." }

                $config = Read-SqlPackageConfig
                $envConfig = Read-DotEnv -Path ".\.env"
                $envVars = $envConfig.Env

                # Obtener ruta del .bacpac
                $sourcePath = $null
                if ($config.import -and $config.import.sourcePath) {
                    $sourcePath = $config.import.sourcePath
                }
                if (-not $sourcePath -or -not (Test-Path $sourcePath)) {
                    throw "Configure 'import.sourcePath' en sqlpackage.yaml con la ruta al archivo .bacpac"
                }

                try {
                    Write-Host " Servidor: $($envVars['DB_SERVER'])" -ForegroundColor Cyan
                    Write-Host " Base datos: $($envVars['DB_NAME'])" -ForegroundColor Cyan
                    Write-Host " Fuente: $sourcePath" -ForegroundColor Cyan
                    Write-Host " Modo: IMPORT (.bacpac → servidor)" -ForegroundColor Yellow
                    Write-Host ""
                    Write-Host " ADVERTENCIA: Esta acción reemplazará la base de datos completa." -ForegroundColor Red
                    Write-Host ""

                    $confirm = Read-Host " ¿Continuar con la importación? (S/N)"
                    if ($confirm -notmatch '^[Ss]$') {
                        Write-Host " Importación cancelada." -ForegroundColor Yellow
                        return
                    }

                    Write-Host ""
                    Write-Host " Importando .bacpac..." -ForegroundColor Cyan
                    $importArgs = Build-SqlPackageArgs -Action 'Import' -Config $config -EnvVars $envVars -SourcePath $sourcePath

                    & sqlpackage @importArgs 2>&1 | Tee-Object -Variable importOutput
                    if ($LASTEXITCODE -eq 0) {
                        Write-Host ""
                        Write-Host " Importación completada exitosamente" -ForegroundColor Green
                    }
                    else {
                        throw "La importación falló con código de salida: $LASTEXITCODE"
                    }
                }
                finally {
                    # Limpiar contraseña de memoria
                    if ($envVars) { $envVars['DB_PASSWORD'] = $null }
                    [System.GC]::Collect()
                }
            }
        }
    }
}