using SurrealDb.Net; using TarninaHub.Application.Abstractions; using TarninaHub.Application.Dtos; using TarninaHub.Application.Mappings; namespace TarninaHub.Infrastructure.Persistence; public sealed class IdeaService : IIdeaService { private const string SelectIdeaFields = "type::string(id) AS id, title, description, type::string(author) AS author, status, updated_at"; private const string SelectCardFields = "type::string(id) AS id, type::string(idea) AS idea, essence, audience, stage"; private readonly ISurrealDbClient _client; public IdeaService(ISurrealDbClient client) { _client = client; } public async Task CreateAsync( string authorId, CreateIdeaRequest request, CancellationToken cancellationToken) { var ideaId = Guid.NewGuid().ToString("N"); var cardId = Guid.NewGuid().ToString("N"); var (authorTable, authorIdPart) = SurrealRecordId.Split(authorId); var card = request.Card ?? new IdeaCardRequest(null, null, null); var ideaResponse = await _client.RawQuery( "CREATE type::record('idea', $id) SET title = $title, description = $description, " + "author = type::record($authorTable, $authorId), status = 'draft';", new Dictionary { ["id"] = ideaId, ["title"] = request.Title, ["description"] = request.Description, ["authorTable"] = authorTable, ["authorId"] = authorIdPart, }, cancellationToken); ideaResponse.EnsureAllOks(); var cardResponse = await _client.RawQuery( "CREATE type::record('idea_card', $id) SET idea = type::record('idea', $ideaId), " + "essence = $essence, audience = $audience, stage = $stage;", new Dictionary { ["id"] = cardId, ["ideaId"] = ideaId, ["essence"] = card.Essence ?? string.Empty, ["audience"] = card.Audience ?? string.Empty, ["stage"] = string.IsNullOrWhiteSpace(card.Stage) ? "idea" : card.Stage, }, cancellationToken); cardResponse.EnsureAllOks(); return await LoadAsync($"idea:{ideaId}", cancellationToken) ?? throw new InvalidOperationException("Idea could not be loaded after creation."); } public async Task GetAsync(string ideaId, CancellationToken cancellationToken) => await LoadAsync(ideaId, cancellationToken); public async Task> ListForAuthorAsync( string authorId, CancellationToken cancellationToken) { var (authorTable, authorIdPart) = SurrealRecordId.Split(authorId); var response = await _client.RawQuery( $"SELECT {SelectIdeaFields} FROM idea WHERE author = type::record($authorTable, $authorId) " + "ORDER BY updated_at DESC;", new Dictionary { ["authorTable"] = authorTable, ["authorId"] = authorIdPart, }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(0) ?? []; var result = new List(rows.Count); foreach (var row in rows) { result.Add(Map(row, await LoadCardAsync(row.Id, cancellationToken))); } return result; } public async Task UpdateAsync( string ideaId, UpdateIdeaRequest request, CancellationToken cancellationToken) { var (table, id) = SurrealRecordId.Split(ideaId); var parameters = new Dictionary { ["table"] = table, ["id"] = id, ["title"] = request.Title, ["description"] = request.Description, }; var assignments = new List { "title = $title", "description = $description", "updated_at = time::now()", }; if (!string.IsNullOrWhiteSpace(request.Status) && IdeaStatusWire.Parse(request.Status) is { } status) { assignments.Add("status = $status"); parameters["status"] = status.ToWire(); } var response = await _client.RawQuery( $"UPDATE type::record($table, $id) SET {string.Join(", ", assignments)}; " + $"SELECT {SelectIdeaFields} FROM type::record($table, $id) LIMIT 1;", parameters, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(1) ?? []; if (rows.Count == 0) { return null; } return Map(rows[0], await LoadCardAsync(ideaId, cancellationToken)); } public async Task GetCardAsync(string ideaId, CancellationToken cancellationToken) => await LoadCardAsync(ideaId, cancellationToken); public async Task UpdateCardAsync( string ideaId, IdeaCardRequest request, CancellationToken cancellationToken) { var (table, id) = SurrealRecordId.Split(ideaId); var response = await _client.RawQuery( "UPDATE idea_card SET essence = $essence, audience = $audience, stage = $stage " + "WHERE idea = type::record($table, $id); " + $"SELECT {SelectCardFields} FROM idea_card WHERE idea = type::record($table, $id) LIMIT 1;", new Dictionary { ["table"] = table, ["id"] = id, ["essence"] = request.Essence ?? string.Empty, ["audience"] = request.Audience ?? string.Empty, ["stage"] = string.IsNullOrWhiteSpace(request.Stage) ? "idea" : request.Stage, }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(1) ?? []; return rows.Count > 0 ? MapCard(rows[0]) : null; } private async Task LoadAsync(string ideaId, CancellationToken cancellationToken) { var (table, id) = SurrealRecordId.Split(ideaId); var response = await _client.RawQuery( $"SELECT {SelectIdeaFields} FROM type::record($table, $id) LIMIT 1;", new Dictionary { ["table"] = table, ["id"] = id }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(0) ?? []; if (rows.Count == 0) { return null; } return Map(rows[0], await LoadCardAsync(ideaId, cancellationToken)); } private async Task LoadCardAsync(string ideaId, CancellationToken cancellationToken) { var (table, id) = SurrealRecordId.Split(ideaId); var response = await _client.RawQuery( $"SELECT {SelectCardFields} FROM idea_card WHERE idea = type::record($table, $id) LIMIT 1;", new Dictionary { ["table"] = table, ["id"] = id }, cancellationToken); response.EnsureAllOks(); var rows = response.GetValue>(0) ?? []; return rows.Count > 0 ? MapCard(rows[0]) : null; } private static IdeaResponse Map(IdeaRow row, IdeaCardResponse? card) => new( row.Id, row.Title, row.Description, row.Author, row.Status, card, row.UpdatedAt); private static IdeaCardResponse MapCard(IdeaCardRow row) => new( row.Id, row.Idea, row.Essence, row.Audience, row.Stage); }