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();
}
}