using System; using System.Threading; using System.Threading.Tasks; using Dapper; using Npgsql; namespace GmRelay.Shared.Features.Sessions.CreateSession.Wizard; public sealed class WizardDraftRepository(NpgsqlDataSource dataSource) : IWizardDraftRepository { // NOTE: NativeAOT — Dapper.AOT 1.0.48 only generates interceptors for the // (sql, param) extension overloads, NOT for the (CommandDefinition) overload. // Passing a CommandDefinition here would skip the interceptor and fall back to // Dapper.SqlMapper.CreateParamInfoGenerator, which uses Reflection.Emit and // throws PlatformNotSupportedException on AOT (issue: wizard silently dropped // every Telegram update in v3.9.0). All four methods therefore use the plain // (sql, param) overload, matching the pattern in JoinSessionHandler. public async Task GetActiveAsync(string platform, string ownerId, CancellationToken ct) { const string sql = """ SELECT id AS Id, chat_id AS ChatId, message_thread_id AS MessageThreadId, owner_id AS OwnerId, platform AS Platform, step AS Step, payload::text AS PayloadJson, draft_message_id AS DraftMessageId, created_at AS CreatedAt, updated_at AS UpdatedAt, expires_at AS ExpiresAt FROM wizard_drafts WHERE platform = @Platform AND owner_id = @OwnerId AND expires_at > NOW() ORDER BY updated_at DESC LIMIT 1 """; await using var connection = await dataSource.OpenConnectionAsync(ct); return await connection.QuerySingleOrDefaultAsync( sql, new { Platform = platform, OwnerId = ownerId }); } public async Task UpsertAsync(WizardDraft draft, CancellationToken ct) { const string sql = """ INSERT INTO wizard_drafts (id, chat_id, message_thread_id, owner_id, platform, step, payload, draft_message_id, created_at, updated_at, expires_at) VALUES (@Id, @ChatId, @MessageThreadId, @OwnerId, @Platform, @Step, @PayloadJson::jsonb, @DraftMessageId, @CreatedAt, @UpdatedAt, @ExpiresAt) ON CONFLICT (id) DO UPDATE SET step = EXCLUDED.step, payload = EXCLUDED.payload, draft_message_id = EXCLUDED.draft_message_id, updated_at = EXCLUDED.updated_at, expires_at = EXCLUDED.expires_at; """; await using var connection = await dataSource.OpenConnectionAsync(ct); await connection.ExecuteAsync(sql, draft); } public async Task DeleteAsync(Guid id, CancellationToken ct) { const string sql = "DELETE FROM wizard_drafts WHERE id = @Id"; await using var connection = await dataSource.OpenConnectionAsync(ct); await connection.ExecuteAsync(sql, new { Id = id }); } public async Task DeleteExpiredAsync(CancellationToken ct) { const string sql = "DELETE FROM wizard_drafts WHERE expires_at <= NOW()"; await using var connection = await dataSource.OpenConnectionAsync(ct); return await connection.ExecuteAsync(sql); } }