using System.Data; using System.Threading; using System.ComponentModel; using MySql.Data.MySqlClient; using Dapper; using Dapper.Transaction; using JetHerald.Options; using JetHerald.Contracts; namespace JetHerald.Services; public class Db { public Db(IOptionsMonitor cfg) { Config = cfg; } IOptionsMonitor Config { get; } MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection); public async Task GetContext( IsolationLevel lvl = IsolationLevel.RepeatableRead, CancellationToken token = default) { var conn = GetConnection(); if (conn.State != ConnectionState.Open) await conn.OpenAsync(); var tran = await conn.BeginTransactionAsync(lvl, token); return new DbContext(tran); } public async Task> ProcessHearts() { using var conn = GetConnection(); return await conn.QueryAsync("CALL process_hearts();"); } } public class DbContext : IDisposable { [EditorBrowsable(EditorBrowsableState.Never)] public DbContext(IDbTransaction tran) { Tran = tran; Conn = Tran.Connection; } IDbConnection Conn; IDbTransaction Tran; public void Commit() => Tran.Commit(); public void Dispose() { Tran.Dispose(); Conn.Dispose(); } public Task> GetTopicsForUser(uint userId) => Tran.QueryAsync( " SELECT * FROM topic WHERE CreatorId = @userId", new { userId }); public Task UpdatePerms(uint userId, uint planId, uint roleId) => Tran.ExecuteAsync(@" UPDATE user SET PlanId = @planId, RoleId = @roleId WHERE UserId = @userId", new { userId, planId, roleId }); public Task> GetPlans() => Tran.QueryAsync("SELECT * FROM plan"); public Task> GetRoles() => Tran.QueryAsync("SELECT * FROM role"); public Task> GetInvites() => Tran.QueryAsync(@" SELECT ui.*, u.Login as RedeemedByLogin FROM userinvite ui LEFT JOIN user u ON ui.RedeemedBy = u.UserId"); public Task> GetUsers() => Tran.QueryAsync(@" SELECT u.* FROM user u;"); public Task> GetHeartsForUser(uint userId) => Tran.QueryAsync( " SELECT h.* FROM heart h JOIN topic USING (TopicId) WHERE CreatorId = @userId", new { userId }); public Task CreateUserInvite(uint planId, uint roleId, string inviteCode) => Tran.ExecuteAsync(@" INSERT INTO userinvite ( PlanId, RoleId, InviteCode) VALUES (@planId, @roleId, @inviteCode)", new { planId, roleId, inviteCode }); public Task DeleteUserInvite(uint inviteId) => Tran.ExecuteAsync(@" DELETE FROM userinvite WHERE UserInviteId = @inviteId", new { inviteId }); public Task GetTopic(string name) => Tran.QuerySingleOrDefaultAsync( "SELECT * FROM topic WHERE Name = @name", new { name }); public Task DeleteTopic(string name, uint userId) => Tran.ExecuteAsync( " DELETE FROM topic WHERE Name = @name AND CreatorId = @userId", new { name, userId }); public Task GetTopicForSub(string token, NamespacedId sub) => Tran.QuerySingleOrDefaultAsync( " SELECT t.*, ts.Sub " + " FROM topic t " + " LEFT JOIN topic_sub ts ON t.TopicId = ts.TopicId AND ts.Sub = @sub " + " WHERE ReadToken = @token", new { token, sub }); public Task> GetHeartsForTopic(uint topicId) => Tran.QueryAsync( " SELECT * FROM heart WHERE TopicId = @topicId", new { topicId }); public Task GetUser(string login) => Tran.QuerySingleOrDefaultAsync(@" SELECT u.*, up.*, ur.* FROM user u JOIN plan up ON u.PlanId = up.PlanId JOIN role ur ON u.RoleId = ur.RoleId WHERE u.Login = @login;", new { login }); public async Task CreateTopic(uint user, string name, string descr) { var topicsCount = await Tran.QuerySingleAsync( " SELECT COUNT(*) " + " FROM user u " + " LEFT JOIN topic t ON t.CreatorId = u.UserId " + " WHERE u.UserId = @user", new { user } ); var planTopicsCount = await Tran.QuerySingleAsync( " SELECT p.MaxTopics " + " FROM user u " + " LEFT JOIN plan p ON p.PlanId = u.PlanId " + " WHERE u.UserId = @user", new { user } ); if (topicsCount >= planTopicsCount) return null; var topic = await Tran.QuerySingleOrDefaultAsync( " INSERT INTO topic " + " ( CreatorId, Name, Description, ReadToken, WriteToken) " + " VALUES " + " (@CreatorId, @Name, @Description, @ReadToken, @WriteToken); " + " SELECT * FROM topic WHERE TopicId = LAST_INSERT_ID(); ", new Topic { CreatorId = user, Name = name, Description = descr, ReadToken = TokenHelper.GetToken(), WriteToken = TokenHelper.GetToken() }); return topic; } public async Task RegisterUser(User user) { _ = await Tran.QuerySingleOrDefaultAsync(@" INSERT INTO user ( Login, Name, PasswordHash, PasswordSalt, HashType, PlanId, RoleId) VALUES (@Login, @Name, @PasswordHash, @PasswordSalt, @HashType, @PlanId, @RoleId);", param: user); return await GetUser(user.Login); } public Task RedeemInvite(uint inviteId, uint userId) => Tran.ExecuteAsync( @"UPDATE userinvite SET RedeemedBy = @userId WHERE UserInviteId = @inviteId", new { inviteId, userId }); public Task GetInviteByCode(string inviteCode) => Tran.QuerySingleOrDefaultAsync( " SELECT * FROM userinvite " + " WHERE InviteCode = @inviteCode " + " AND RedeemedBy IS NULL ", new { inviteCode }); public Task> GetSubsForTopic(uint topicId) => Tran.QueryAsync( " SELECT Sub " + " FROM topic_sub " + " WHERE TopicId = @topicid", new { topicId }); public Task> GetTopicsForSub(NamespacedId sub) => Tran.QueryAsync( " SELECT t.*" + " FROM topic_sub ts" + " JOIN topic t USING (TopicId)" + " WHERE ts.Sub = @sub", new { sub }); public Task CreateSubscription(uint topicId, NamespacedId sub) => Tran.ExecuteAsync( " INSERT INTO topic_sub" + " (TopicId, Sub)" + " VALUES" + " (@topicId, @sub)", new { topicId, sub }); public Task RemoveSubscription(string topicName, NamespacedId sub) => Tran.ExecuteAsync( " DELETE ts " + " FROM topic_sub ts" + " JOIN topic t USING (TopicId) " + " WHERE t.Name = @topicName AND ts.Sub = @sub;", new { topicName, sub }); public Task ReportHeartbeat(uint topicId, string heart, int timeoutSeconds) => Tran.QueryFirstAsync( @"CALL report_heartbeat(@topicId, @heart, @timeoutSeconds);", new { topicId, heart, timeoutSeconds }); public Task MarkHeartAttackReported(ulong id) => Tran.ExecuteAsync("UPDATE heartevent SET Status = 'reported' WHERE HeartEventId = @id", new { id }); #region TicketStore public Task RemoveSession(string sessionId) => Tran.ExecuteAsync("DELETE FROM usersession WHERE SessionId = @sessionId", new { sessionId }); public Task GetSession(string sessionId) => Tran.QuerySingleOrDefaultAsync( "SELECT * FROM usersession WHERE SessionId = @sessionId", new { sessionId }); public Task UpdateSession(string sessionId, byte[] data, DateTime expiryTs) => Tran.ExecuteAsync(@" UPDATE usersession SET SessionData = @data, ExpiryTs = @expiryTs WHERE SessionId = @sessionId;", new { sessionId, data, expiryTs }); public async Task CreateSession(string sessionId, byte[] data, DateTime expiryTs) { await Tran.ExecuteAsync(@" INSERT INTO usersession (SessionId, SessionData, ExpiryTs) VALUES (@sessionId, @data, @expiryTs);", new { sessionId, data, expiryTs }); return sessionId; } #endregion }