using SurrealDb.Net; using TarninaHub.Application.Abstractions; using TarninaHub.Application.Dtos; namespace TarninaHub.Infrastructure.Persistence; public sealed class InnovationService : IInnovationService { private const string SelectFields = "type::string(id) AS id, title, summary, description, type::string(challenge) AS challenge, " + "stage, cost, contact, media, published, updated_at"; private readonly ISurrealDbClient _client; public InnovationService(ISurrealDbClient client) { _client = client; } public async Task> ListAsync( string? challengeId, string? stage, string? query, bool onlyPublished, CancellationToken cancellationToken) { var where = BuildFilter(challengeId, stage, query, onlyPublished, out var parameters); var response = await _client.RawQuery( $"SELECT {SelectFields} FROM innovation{where} ORDER BY updated_at DESC;", parameters, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(0) ?? []; return rows.Select(Map).ToList(); } public async Task GetAsync(string innovationId, CancellationToken cancellationToken) { var (table, id) = SurrealRecordId.Split(innovationId); var response = await _client.RawQuery( $"SELECT {SelectFields} FROM type::record($table, $id) LIMIT 1;", new Dictionary { ["table"] = table, ["id"] = id }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(0) ?? []; return rows.Count > 0 ? Map(rows[0]) : null; } public async Task CreateAsync( InnovationRequest request, CancellationToken cancellationToken) { var id = Guid.NewGuid().ToString("N"); var (challengeTable, challengeId) = SurrealRecordId.Split(request.ChallengeId); var parameters = new Dictionary { ["id"] = id, ["title"] = request.Title, ["summary"] = request.Summary ?? string.Empty, ["description"] = request.Description ?? string.Empty, ["challengeTable"] = challengeTable, ["challengeId"] = challengeId, ["stage"] = request.Stage, ["media"] = request.Media ?? [], ["published"] = request.Published, }; var assignments = new List { "title = $title", "summary = $summary", "description = $description", "challenge = type::record($challengeTable, $challengeId)", "stage = $stage", "media = $media", "published = $published", "updated_at = time::now()", }; AppendOptional(assignments, parameters, "cost", request.Cost); AppendOptionalObject(assignments, parameters, "contact", request.Contact); var response = await _client.RawQuery( "CREATE type::record('innovation', $id) SET " + string.Join(", ", assignments) + "; " + $"SELECT {SelectFields} FROM type::record('innovation', $id) LIMIT 1;", parameters, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(1) ?? []; return Map(rows[0]); } public async Task UpdateAsync( string innovationId, InnovationRequest request, CancellationToken cancellationToken) { var (table, id) = SurrealRecordId.Split(innovationId); var (challengeTable, challengeId) = SurrealRecordId.Split(request.ChallengeId); var parameters = new Dictionary { ["table"] = table, ["id"] = id, ["title"] = request.Title, ["summary"] = request.Summary ?? string.Empty, ["description"] = request.Description ?? string.Empty, ["challengeTable"] = challengeTable, ["challengeId"] = challengeId, ["stage"] = request.Stage, ["media"] = request.Media ?? [], ["published"] = request.Published, }; var assignments = new List { "title = $title", "summary = $summary", "description = $description", "challenge = type::record($challengeTable, $challengeId)", "stage = $stage", "media = $media", "published = $published", "updated_at = time::now()", }; AppendOptional(assignments, parameters, "cost", request.Cost, clearWhenMissing: true); AppendOptionalObject(assignments, parameters, "contact", request.Contact, clearWhenMissing: true); var response = await _client.RawQuery( $"UPDATE type::record($table, $id) SET {string.Join(", ", assignments)}; " + $"SELECT {SelectFields} FROM type::record($table, $id) LIMIT 1;", parameters, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(1) ?? []; return rows.Count > 0 ? Map(rows[0]) : null; } private static string BuildFilter( string? challengeId, string? stage, string? query, bool onlyPublished, out Dictionary parameters) { parameters = new Dictionary(); var clauses = new List(); if (!string.IsNullOrWhiteSpace(challengeId)) { var (table, id) = SurrealRecordId.Split(challengeId); clauses.Add("challenge = type::record($challengeTable, $challengeId)"); parameters["challengeTable"] = table; parameters["challengeId"] = id; } if (!string.IsNullOrWhiteSpace(stage)) { clauses.Add("stage = $stage"); parameters["stage"] = stage; } if (!string.IsNullOrWhiteSpace(query)) { clauses.Add( "(string::contains(string::lowercase(title), string::lowercase($query)) " + "OR string::contains(string::lowercase(summary), string::lowercase($query)))"); parameters["query"] = query; } if (onlyPublished) { clauses.Add("published = true"); } return clauses.Count > 0 ? " WHERE " + string.Join(" AND ", clauses) : string.Empty; } private static void AppendOptional( List assignments, Dictionary parameters, string field, string? value, bool clearWhenMissing = false) { if (!string.IsNullOrWhiteSpace(value)) { assignments.Add($"{field} = $field_{field}"); parameters[$"field_{field}"] = value; } else if (clearWhenMissing) { assignments.Add($"{field} = NONE"); } } private static void AppendOptionalObject( List assignments, Dictionary parameters, string field, IReadOnlyDictionary? value, bool clearWhenMissing = false) { if (value is not null) { assignments.Add($"{field} = $field_{field}"); parameters[$"field_{field}"] = value; } else if (clearWhenMissing) { assignments.Add($"{field} = NONE"); } } private static InnovationResponse Map(InnovationRow row) => new( row.Id, row.Title, row.Summary, row.Description, row.Challenge, row.Stage, row.Cost, row.Contact is null ? null : row.Contact, row.Media.Select(item => (IReadOnlyDictionary)item).ToList(), row.Published, row.UpdatedAt); }