Files
Hy2026/backend/Infrastructure/Persistence/IdeaService.cs
T
Rafał Tyński fa1199539b feat(backend): add communication, tester, admin and middleman modules
Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
2026-10-04 03:49:45 +02:00

232 lines
8.1 KiB
C#

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<IdeaResponse> 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<string, object?>
{
["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<string, object?>
{
["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<IdeaResponse?> GetAsync(string ideaId, CancellationToken cancellationToken) =>
await LoadAsync(ideaId, cancellationToken);
public async Task<IReadOnlyList<IdeaResponse>> 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<string, object?>
{
["authorTable"] = authorTable,
["authorId"] = authorIdPart,
},
cancellationToken);
response.EnsureAllOks();
var rows = response.GetValue<List<IdeaRow>>(0) ?? [];
var result = new List<IdeaResponse>(rows.Count);
foreach (var row in rows)
{
result.Add(Map(row, await LoadCardAsync(row.Id, cancellationToken)));
}
return result;
}
public async Task<IReadOnlyList<string>> ListDistinctAuthorIdsAsync(CancellationToken cancellationToken)
{
var response = await _client.RawQuery(
"SELECT type::string(author) AS id FROM idea;",
new Dictionary<string, object?>(),
cancellationToken);
response.EnsureAllOks();
var rows = response.GetValue<List<IdRow>>(0) ?? [];
return rows.Select(row => row.Id).Distinct(StringComparer.Ordinal).ToList();
}
public async Task<IdeaResponse?> UpdateAsync(
string ideaId,
UpdateIdeaRequest request,
CancellationToken cancellationToken)
{
var (table, id) = SurrealRecordId.Split(ideaId);
var parameters = new Dictionary<string, object?>
{
["table"] = table,
["id"] = id,
["title"] = request.Title,
["description"] = request.Description,
};
var assignments = new List<string>
{
"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<List<IdeaRow>>(1) ?? [];
if (rows.Count == 0)
{
return null;
}
return Map(rows[0], await LoadCardAsync(ideaId, cancellationToken));
}
public async Task<IdeaCardResponse?> GetCardAsync(string ideaId, CancellationToken cancellationToken) =>
await LoadCardAsync(ideaId, cancellationToken);
public async Task<IdeaCardResponse?> 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<string, object?>
{
["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<List<IdeaCardRow>>(1) ?? [];
return rows.Count > 0 ? MapCard(rows[0]) : null;
}
private async Task<IdeaResponse?> 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<string, object?> { ["table"] = table, ["id"] = id },
cancellationToken);
response.EnsureAllOks();
var rows = response.GetValue<List<IdeaRow>>(0) ?? [];
if (rows.Count == 0)
{
return null;
}
return Map(rows[0], await LoadCardAsync(ideaId, cancellationToken));
}
private async Task<IdeaCardResponse?> 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<string, object?> { ["table"] = table, ["id"] = id },
cancellationToken);
response.EnsureAllOks();
var rows = response.GetValue<List<IdeaCardRow>>(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);
}