using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Npgsql; namespace SaasMediaWorker.Services; /// /// PostgreSQL veritabanı servisi — RenderJob ve Project durumlarını günceller. /// NestJS Prisma schema'sıyla uyumlu SQL sorguları kullanır. /// /// Neden doğrudan SQL (ORM yerine)? /// - C# Worker minimum footprint olmalı (16GB RPi). /// - Sadece UPDATE sorguları yapılıyor — ORM gereksiz overhead. /// - Npgsql ARM64'te native çalışır. /// public class DatabaseService { private readonly ILogger _logger; private readonly string _connectionString; public DatabaseService( ILogger logger, IConfiguration configuration) { _logger = logger; _connectionString = configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("DefaultConnection bağlantı dizesi bulunamadı"); } /// /// RenderJob tablosunun durumunu günceller. /// public async Task UpdateRenderJobStatus( string renderJobId, string status, int progress, string? currentStage, string? errorMessage = null, string? errorStack = null, long? processingTimeMs = null, string? workerVersion = null, string? workerHostname = null) { await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); var sql = @" UPDATE ""RenderJob"" SET ""status"" = @status::""RenderJobStatus"", ""progress"" = @progress, ""currentStage"" = CASE WHEN @stage IS NOT NULL THEN @stage::""RenderStage"" ELSE ""currentStage"" END, ""errorMessage"" = COALESCE(@errorMessage, ""errorMessage""), ""errorStack"" = COALESCE(@errorStack, ""errorStack""), ""processingTimeMs"" = COALESCE(@processingTimeMs, ""processingTimeMs""), ""workerVersion"" = COALESCE(@workerVersion, ""workerVersion""), ""workerHostname"" = COALESCE(@workerHostname, ""workerHostname""), ""startedAt"" = CASE WHEN @status = 'PROCESSING' AND ""startedAt"" IS NULL THEN NOW() ELSE ""startedAt"" END, ""completedAt"" = CASE WHEN @status IN ('COMPLETED', 'FAILED') THEN NOW() ELSE ""completedAt"" END, ""lastErrorAt"" = CASE WHEN @status = 'FAILED' THEN NOW() ELSE ""lastErrorAt"" END, ""updatedAt"" = NOW() WHERE ""id"" = @id"; await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("id", renderJobId); cmd.Parameters.AddWithValue("status", status); cmd.Parameters.AddWithValue("progress", progress); cmd.Parameters.AddWithValue("stage", (object?)currentStage ?? DBNull.Value); cmd.Parameters.AddWithValue("errorMessage", (object?)errorMessage ?? DBNull.Value); cmd.Parameters.AddWithValue("errorStack", (object?)errorStack ?? DBNull.Value); cmd.Parameters.AddWithValue("processingTimeMs", (object?)processingTimeMs ?? DBNull.Value); cmd.Parameters.AddWithValue("workerVersion", (object?)workerVersion ?? DBNull.Value); cmd.Parameters.AddWithValue("workerHostname", (object?)workerHostname ?? DBNull.Value); var affected = await cmd.ExecuteNonQueryAsync(); _logger.LogDebug("RenderJob güncellendi: {Id} → {Status} ({Progress}%)", renderJobId, status, progress); } /// /// Project tablosunun durumunu günceller. /// public async Task UpdateProjectStatus( string projectId, string status, int progress, string? finalVideoUrl = null, string? errorMessage = null) { await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); var sql = @" UPDATE ""Project"" SET ""status"" = @status::""ProjectStatus"", ""progress"" = @progress, ""finalVideoUrl"" = COALESCE(@finalVideoUrl, ""finalVideoUrl""), ""errorMessage"" = @errorMessage, ""completedAt"" = CASE WHEN @status = 'COMPLETED' THEN NOW() ELSE ""completedAt"" END, ""updatedAt"" = NOW() WHERE ""id"" = @id"; await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("id", projectId); cmd.Parameters.AddWithValue("status", status); cmd.Parameters.AddWithValue("progress", progress); cmd.Parameters.AddWithValue("finalVideoUrl", (object?)finalVideoUrl ?? DBNull.Value); cmd.Parameters.AddWithValue("errorMessage", (object?)errorMessage ?? DBNull.Value); await cmd.ExecuteNonQueryAsync(); _logger.LogDebug("Project güncellendi: {Id} → {Status} ({Progress}%)", projectId, status, progress); } /// /// Render log kaydı ekler. /// public async Task AddRenderLog( string renderJobId, string stage, string message, string level = "info", int? durationMs = null, string? metadata = null) { await using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); var sql = @" INSERT INTO ""RenderLog"" (""id"", ""renderJobId"", ""stage"", ""message"", ""level"", ""durationMs"", ""metadata"", ""createdAt"") VALUES (gen_random_uuid(), @renderJobId, @stage::""RenderStage"", @message, @level, @durationMs, @metadata::jsonb, NOW())"; await using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("renderJobId", renderJobId); cmd.Parameters.AddWithValue("stage", stage); cmd.Parameters.AddWithValue("message", message); cmd.Parameters.AddWithValue("level", level); cmd.Parameters.AddWithValue("durationMs", (object?)durationMs ?? DBNull.Value); cmd.Parameters.AddWithValue("metadata", (object?)metadata ?? DBNull.Value); await cmd.ExecuteNonQueryAsync(); } }