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).
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using SurrealDb.Net;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Infrastructure.Persistence;
|
||||
|
||||
public sealed class PartnershipService : IPartnershipService
|
||||
{
|
||||
private const string SelectFields =
|
||||
"type::string(id) AS id, type::string(initiator) AS initiator, subject, " +
|
||||
"array::map(partners, |$p| type::string($p)) AS partners, status";
|
||||
|
||||
private readonly ISurrealDbClient _client;
|
||||
|
||||
public PartnershipService(ISurrealDbClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PartnershipResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
bool isStaff,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var parameters = new Dictionary<string, object?>();
|
||||
var where = string.Empty;
|
||||
|
||||
if (!isStaff)
|
||||
{
|
||||
var (table, id) = SurrealRecordId.Split(userId);
|
||||
where = " WHERE initiator = type::record($table, $id)";
|
||||
parameters["table"] = table;
|
||||
parameters["id"] = id;
|
||||
}
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
$"SELECT {SelectFields} FROM partnership{where} ORDER BY subject ASC;",
|
||||
parameters,
|
||||
cancellationToken);
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<PartnershipRow>>(0) ?? [];
|
||||
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<PartnershipResponse> CreateAsync(
|
||||
string initiatorId,
|
||||
CreatePartnershipRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var partnershipId = Guid.NewGuid().ToString("N");
|
||||
var (initiatorTable, initiatorIdPart) = SurrealRecordId.Split(initiatorId);
|
||||
var partners = request.Partners ?? [];
|
||||
var parameters = new Dictionary<string, object?>
|
||||
{
|
||||
["id"] = partnershipId,
|
||||
["subject"] = request.Subject,
|
||||
["initiatorTable"] = initiatorTable,
|
||||
["initiatorId"] = initiatorIdPart,
|
||||
};
|
||||
|
||||
var partnerExpressions = new List<string>(partners.Count);
|
||||
|
||||
for (var i = 0; i < partners.Count; i++)
|
||||
{
|
||||
var (table, id) = SurrealRecordId.Split(partners[i]);
|
||||
parameters[$"table{i}"] = table;
|
||||
parameters[$"id{i}"] = id;
|
||||
partnerExpressions.Add($"type::record($table{i}, $id{i})");
|
||||
}
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
"CREATE type::record('partnership', $id) SET " +
|
||||
"initiator = type::record($initiatorTable, $initiatorId), subject = $subject, " +
|
||||
$"partners = [{string.Join(", ", partnerExpressions)}], status = 'proposed'; " +
|
||||
$"SELECT {SelectFields} FROM type::record('partnership', $id) LIMIT 1;",
|
||||
parameters,
|
||||
cancellationToken);
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<PartnershipRow>>(1) ?? [];
|
||||
|
||||
return rows.Count > 0
|
||||
? Map(rows[0])
|
||||
: throw new InvalidOperationException("Partnership could not be loaded after creation.");
|
||||
}
|
||||
|
||||
private static PartnershipResponse Map(PartnershipRow row) => new(
|
||||
row.Id,
|
||||
row.Initiator,
|
||||
row.Subject,
|
||||
row.Partners,
|
||||
row.Status);
|
||||
}
|
||||
Reference in New Issue
Block a user