From cd9b72f39e02ede240cfd6fed16153f995b56fd1 Mon Sep 17 00:00:00 2001 From: Diogenes Polanco Martinez Date: Mon, 21 Oct 2019 15:12:24 -0400 Subject: [PATCH 1/3] adding support for PostgreSQL --- .../Coderr.Server.PostgreSQL.csproj | 29 ++ .../Core/Accounts/AccountMapper.cs | 25 ++ .../Core/Accounts/AccountRepository.cs | 230 +++++++++++++ .../GetAccountEmailByIdHandler.cs | 26 ++ .../QueryHandlers/ListAccountsHandler.cs | 26 ++ .../Core/ApiKeys/ApiKeyRepository.cs | 194 +++++++++++ .../ApiKeys/Commands/CreateApiKeyHandler.cs | 55 +++ .../ApiKeys/Commands/DeleteApiKeyHandler.cs | 40 +++ .../ApiKeys/Commands/EditApiKeyHandler.cs | 52 +++ .../Core/ApiKeys/Mappings/ApiKeyMapper.cs | 16 + .../Core/ApiKeys/Mappings/IntMapper.cs | 21 ++ .../Core/ApiKeys/Queries/GetApiKeyHandler.cs | 66 ++++ .../ApiKeys/Queries/ListApiKeysHandler.cs | 30 ++ .../Core/Applications/ApplicationMapper.cs | 15 + .../Applications/ApplicationRepository.cs | 205 +++++++++++ .../GetApplicationByAppKeyHandler.cs | 34 ++ .../Queries/GetApplicationIdByKeyHandler.cs | 32 ++ .../Queries/GetApplicationOverviewHandler.cs | 305 ++++++++++++++++ .../Environments/GetEnvironmentsHandler.cs | 54 +++ .../GetEnvironmentsResultItemMapper.cs | 13 + .../Environments/ResetEnvironmentHandler.cs | 38 ++ .../CheckForFeedbackNotificationsToSend.cs | 78 +++++ .../Core/Feedback/FeedbackEntityMapper.cs | 15 + .../Core/Feedback/FeedbackRepository.cs | 61 ++++ .../Core/Feedback/SubmitFeedbackHandler.cs | 110 ++++++ .../Core/Incidents/IncidentMapper.cs | 38 ++ .../Core/Incidents/IncidentRepository.cs | 159 +++++++++ .../Core/Incidents/IncidentSummaryMapper.cs | 29 ++ .../Queries/FindIncidentResultItemMapper.cs | 39 +++ .../Incidents/Queries/FindIncidentsHandler.cs | 241 +++++++++++++ .../Incidents/Queries/GetCollectionHandler.cs | 86 +++++ .../Queries/GetIncidentForClosePage.cs | 33 ++ .../GetIncidentForClosePageResultMapper.cs | 9 + .../Incidents/Queries/GetIncidentHandler.cs | 318 +++++++++++++++++ .../Queries/GetIncidentResultMapper.cs | 43 +++ .../Queries/GetIncidentStatisticsHandler.cs | 108 ++++++ .../Incidents/Queries/GetReportListHandler.cs | 52 +++ .../Queries/GetReportListResultItemMapper.cs | 13 + .../Core/Incidents/ReportDayMapper.cs | 9 + .../Invitations/GetInvitationByKeyHandler.cs | 28 ++ .../Core/Invitations/InvitationMapping.cs | 20 ++ .../Core/Invitations/InvitationRepository.cs | 51 +++ .../Notifications/NotificationRepository.cs | 84 +++++ .../UserNotificationSettingsMap.cs | 54 +++ .../Core/Reports/ErrorReportDtoMapper.cs | 28 ++ .../Core/Reports/ErrorReportRepository.cs | 149 ++++++++ .../Core/Reports/ReportMappingMapper.cs | 19 + .../Core/Users/ApplicationTeamMemberMapper.cs | 26 ++ .../Core/Users/UserMapper.cs | 13 + .../Core/Users/UserRepository.cs | 48 +++ .../DateTimeExtensions.cs | 12 + .../Coderr.Server.PostgreSQL/DbConverters.cs | 47 +++ .../Migrations/MigrationRunner.cs | 181 ++++++++++ .../Migrations/MigrationScripts.cs | 99 ++++++ .../Modules/Geolocation/ErrorOrginListItem.cs | 23 ++ .../Geolocation/ErrorOriginRepository.cs | 105 ++++++ .../GetOriginsForIncidentHandler.cs | 65 ++++ .../History/GetIncidentsForStatesHandler.cs | 65 ++++ .../Modules/History/HistoryEntryMapper.cs | 21 ++ .../Modules/History/HistoryRepository.cs | 31 ++ .../ReportSpikes/ErrorReportSpikeMapper.cs | 24 ++ .../ReportSpikes/ReportSpikesRepository.cs | 108 ++++++ .../ContextCollectionPropertyDbEntity.cs | 21 ++ .../ContextCollectionPropertyValueDbEntity.cs | 26 ++ .../Mappers/SimilarityCollectionMapper.cs | 22 ++ .../Similarities/Mappers/SimilarityMapper.cs | 22 ++ .../Mappers/SimilarityValueMapper.cs | 14 + .../Queries/GetSimilaritiesHandler.cs | 54 +++ .../Similarities/SimilarityRepository.cs | 113 ++++++ .../Modules/Tagging/DoInitialRun.cs | 96 ++++++ .../Modules/Tagging/TagsRepository.cs | 110 ++++++ .../Triggers/CollectionMetadataMapper.cs | 18 + .../Modules/Triggers/DeleteTriggerHandler.cs | 29 ++ .../GetContextCollectionMetadataHandler.cs | 39 +++ .../GetTriggersForApplicationHandler.cs | 32 ++ .../Modules/Triggers/TriggerDtoMapper.cs | 17 + .../Modules/Triggers/TriggerMapper.cs | 36 ++ .../Modules/Triggers/TriggerRepository.cs | 142 ++++++++ .../Versions/ApplicationVersionConfig.cs | 9 + .../ApplicationVersionConfigMapper.cs | 13 + .../Versions/ApplicationVersionMapper.cs | 19 + .../Versions/ApplicationVersionMonthMapper.cs | 15 + .../Queries/GetApplicationVersionsHandler.cs | 59 ++++ .../Queries/GetVersionHistoryHandler.cs | 85 +++++ .../Modules/Versions/Queries/VersionRange.cs | 40 +++ .../Modules/Versions/Queries/Versions.cs | 75 ++++ .../Modules/Versions/VersionRepository.cs | 137 ++++++++ src/Server/Coderr.Server.PostgreSQL/ReadMe.md | 6 + .../ReportAnalyzer/AnalyticsRepository.cs | 268 +++++++++++++++ .../ReportAnalyzer/ErrorReportEntityMapper.cs | 34 ++ .../ReportAnalyzer/ErrorReportRepository.cs | 185 ++++++++++ .../Feedback/LookupReportsForFeedback.cs | 120 +++++++ .../ProcessInboundCollectionsHandler.cs | 90 +++++ .../IncidentBeingAnalyzedMapper.cs | 40 +++ .../ErrorReportContextCollectionProperty.cs | 19 + ...orReportContextCollectionPropertyMapper.cs | 12 + .../ReportAnalyzer/Jobs/Importer.cs | 87 +++++ .../ReportAnalyzer/Jobs/InboundCollection.cs | 9 + .../Jobs/InboundCollectionMapper.cs | 14 + .../Schema/Coderr.v01.sql | 325 ++++++++++++++++++ .../Schema/Coderr.v02.sql | 18 + .../Schema/Coderr.v03.sql | 41 +++ .../Schema/Coderr.v04.sql | 5 + .../Schema/Coderr.v05.sql | 3 + .../Schema/Coderr.v06.sql | 39 +++ .../Schema/Coderr.v07.sql | 12 + .../Schema/Coderr.v08.sql | 34 ++ .../Schema/Coderr.v09.sql | 64 ++++ .../Schema/Coderr.v10.sql | 18 + .../Schema/Coderr.v11.sql | 1 + .../Schema/Coderr.v12.sql | 1 + .../Schema/Coderr.v13.sql | 8 + .../Schema/Coderr.v14.sql | 1 + .../Schema/Coderr.v15.sql | 10 + .../Schema/Coderr.v16.sql | 1 + .../Schema/Coderr.v17.sql | 12 + .../Schema/Coderr.v18.sql | 11 + .../Schema/Coderr.v19.sql | 12 + .../Schema/Coderr.v20.sql | 14 + .../Schema/CoderrMigrationPointer.cs | 10 + .../SqlServerTools.cs | 76 ++++ .../Tools/ConnectionStringHelper.cs | 23 ++ .../Tools/DbCommandExtensions.cs | 23 ++ .../Tools/EntitySerializer.cs | 47 +++ .../UnitOfWorkWithTransaction.cs | 73 ++++ .../Queries/GetApplicationFeedbackHandler.cs | 43 +++ .../GetApplicationFeedbackResultItemMapper.cs | 26 ++ .../GetIncidentFeedbackItemsHandler.cs | 60 ++++ .../Queries/GetOverviewFeedbackHandler.cs | 67 ++++ .../GetOverviewFeedbackResultItemMapper.cs | 9 + .../Web/Overview/GetOverviewHandler.cs | 294 ++++++++++++++++ src/Server/Coderr.Server.Web/appsettings.json | 4 +- .../Coderr.Server.Web/npm-shrinkwrap.json | 44 ++- src/Server/Coderr.Server.sln | 6 + 134 files changed, 7796 insertions(+), 14 deletions(-) create mode 100644 src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/ListAccountsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/ApiKeyRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/CreateApiKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/EditApiKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/ApiKeyMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/IntMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/GetApiKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/ListApiKeysHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Applications/GetApplicationByAppKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationOverviewHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsResultItemMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Environments/ResetEnvironmentHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Feedback/CheckForFeedbackNotificationsToSend.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackEntityMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Feedback/SubmitFeedbackHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentSummaryMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentResultItemMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetCollectionHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePage.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentResultMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListResultItemMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Incidents/ReportDayMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Invitations/GetInvitationByKeyHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationMapping.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Notifications/NotificationRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Notifications/UserNotificationSettingsMap.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportDtoMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Reports/ReportMappingMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Users/ApplicationTeamMemberMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Users/UserMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Core/Users/UserRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/DateTimeExtensions.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/DbConverters.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationScripts.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOrginListItem.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOriginRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/GetOriginsForIncidentHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/History/GetIncidentsForStatesHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryEntryMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ErrorReportSpikeMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ReportSpikesRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityValueMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Queries/GetSimilaritiesHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/SimilarityRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/DoInitialRun.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/TagsRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/CollectionMetadataMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/DeleteTriggerHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetContextCollectionMetadataHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetTriggersForApplicationHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerDtoMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfig.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfigMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMonthMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetApplicationVersionsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetVersionHistoryHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/VersionRange.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/Versions.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Modules/Versions/VersionRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReadMe.md create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/AnalyticsRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportEntityMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportRepository.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/Importer.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollection.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollectionMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v11.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v12.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql create mode 100644 src/Server/Coderr.Server.PostgreSQL/Schema/CoderrMigrationPointer.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Tools/ConnectionStringHelper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Tools/DbCommandExtensions.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Tools/EntitySerializer.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs create mode 100644 src/Server/Coderr.Server.PostgreSQL/Web/Overview/GetOverviewHandler.cs diff --git a/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj b/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj new file mode 100644 index 00000000..281acec9 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj @@ -0,0 +1,29 @@ + + + netstandard2.0 + Coderr.Server.PostgreSQL + Coderr.Server.PostgreSQL + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountMapper.cs new file mode 100644 index 00000000..c2aae02e --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountMapper.cs @@ -0,0 +1,25 @@ +using System; +using Coderr.Server.Domain.Core.Account; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Accounts +{ + public class AccountMapper : CrudEntityMapper + { + public AccountMapper() : base("Accounts") + { + Property(x => x.Id) + .PrimaryKey(true); + + Property(x => x.AccountState) + .ToPropertyValue(o => (AccountState) Enum.Parse(typeof(AccountState), (string) o, true)) + .ToColumnValue(o => o.ToString()); + + Property(x => x.UpdatedAtUtc) + .ToColumnValue(DbConverters.ToSqlNull); + + Property(x => x.LastLoginAtUtc) + .ToColumnValue(DbConverters.ToSqlNull); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountRepository.cs new file mode 100644 index 00000000..63c12b51 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/AccountRepository.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; +using log4net; + +namespace Coderr.Server.PostgreSQL.Core.Accounts +{ + [ContainerService] + public class AccountRepository : IAccountRepository + { + private readonly IAdoNetUnitOfWork _uow; + private ILog _logger = LogManager.GetLogger(typeof(AccountRepository)); + + public AccountRepository(IAdoNetUnitOfWork uow) + { + _uow = uow ?? throw new ArgumentNullException(nameof(uow)); + LogManager.GetLogger(typeof(AccountRepository)).Info("UOW hash: " + _uow.GetHashCode()); + } + + /// + public Task CountAsync() + { + return Task.FromResult((int)_uow.ExecuteScalar("SELECT CAST(count(*) as int) FROM Accounts;")); + } + + + /// + public async Task CreateAsync(Account account) + { + await _uow.InsertAsync(account); + } + + /// + public async Task FindByActivationKeyAsync(string activationKey) + { + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = "SELECT * FROM Accounts WHERE ActivationKey=@key;"; + cmd.AddParameter("key", activationKey); + var accounts= await cmd.ToListAsync(new AccountMapper()); + if (accounts.Count == 0) + return null; + + _logger.Error($"Found {accounts.Count} accounts, expected one."); + return accounts.First(); + } + } + + /// + public async Task UpdateAsync(Account account) + { + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = + "UPDATE Accounts SET " + + " Username = @Username, " + + " HashedPassword = @HashedPassword, " + + " Salt = @Salt, " + + " CreatedAtUtc = @CreatedAtUtc, " + + " AccountState = @AccountState, " + + " Email = @Email, " + + " UpdatedAtUtc = @UpdatedAtUtc, " + + " ActivationKey = @ActivationKey, " + + " LoginAttempts = @LoginAttempts, " + + " LastLoginAtUtc = @LastLoginAtUtc " + + "WHERE Id = @Id;"; + cmd.AddParameter("@Id", account.Id); + cmd.AddParameter("@Username", account.UserName); + cmd.AddParameter("@HashedPassword", account.HashedPassword); + cmd.AddParameter("@Salt", account.Salt); + cmd.AddParameter("@CreatedAtUtc", account.CreatedAtUtc); + cmd.AddParameter("@AccountState", account.AccountState.ToString()); + cmd.AddParameter("@Email", account.Email); + cmd.AddParameter("@UpdatedAtUtc", + account.UpdatedAtUtc == DateTime.MinValue ? (object) null : account.UpdatedAtUtc); + cmd.AddParameter("@ActivationKey", account.ActivationKey); + cmd.AddParameter("@LoginAttempts", account.LoginAttempts); + cmd.AddParameter("@LastLoginAtUtc", + account.LastLoginAtUtc == DateTime.MinValue ? (object) null : account.LastLoginAtUtc); + await cmd.ExecuteNonQueryAsync(); + } + } + + /// + public async Task FindByUserNameAsync(string userName) + { + if (userName == null) throw new ArgumentNullException(nameof(userName)); + + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = "SELECT TOP 1 * FROM Accounts WHERE UserName=@uname;"; + cmd.AddParameter("uname", userName); + return await cmd.FirstOrDefaultAsync(new AccountMapper()); + } + } + + public async Task GetByIdAsync(int id) + { + if (id <= 0) throw new ArgumentNullException(nameof(id)); + + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = "SELECT * FROM Accounts WHERE Id=@id;"; + cmd.AddParameter("id", id); + return await cmd.FirstAsync(); + } + } + + /// + public async Task FindByEmailAsync(string emailAddress) + { + if (emailAddress == null) throw new ArgumentNullException(nameof(emailAddress)); + + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = "SELECT * FROM Accounts WHERE Email=@email;"; + cmd.AddParameter("email", emailAddress); + return await cmd.FirstOrDefaultAsync(new AccountMapper()); + } + } + + /// + public async Task> GetByIdAsync(int[] ids) + { + if (ids == null) throw new ArgumentNullException(nameof(ids)); + + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + var idStr = string.Join(",", ids.Select(x => "'" + x + "'")); + cmd.CommandText = $"SELECT * FROM Accounts WHERE Id IN ({idStr});"; + return await cmd.ToListAsync(); + } + } + + + /// + public async Task IsEmailAddressTakenAsync(string email) + { + if (email == null) throw new ArgumentNullException(nameof(email)); + + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = "SELECT TOP 1 Email FROM Accounts WHERE Email = @Email;"; + cmd.AddParameter("Email", email); + var result = await cmd.ExecuteScalarAsync(); + return result != null && result != DBNull.Value; + } + } + + + /// + public async Task IsUserNameTakenAsync(string userName) + { + if (userName == null) throw new ArgumentNullException(nameof(userName)); + + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = "SELECT TOP 1 UserName FROM Accounts WHERE UserName = @userName;"; + cmd.AddParameter("userName", userName); + var result = await cmd.ExecuteScalarAsync(); + return result != null && result != DBNull.Value; + } + } + + public void Create(Account account) + { + if (account == null) throw new ArgumentNullException(nameof(account)); + using (var cmd = _uow.CreateCommand()) + { + //for systems where ID must be specified + if (account.Id > 0) + { + cmd.CommandText = + "INSERT INTO Accounts (Id, Username, HashedPassword, Salt, CreatedAtUtc, AccountState, Email, UpdatedAtUtc, ActivationKey, LoginAttempts, LastLoginAtUtc) " + + " VALUES(@Id, @Username, @HashedPassword, @Salt, @CreatedAtUtc, @AccountState, @Email, @UpdatedAtUtc, @ActivationKey, @LoginAttempts, @LastLoginAtUtc); RETURNING Id;"; + cmd.AddParameter("id", account.Id); + + } + else + { + cmd.CommandText = + "INSERT INTO Accounts (Username, HashedPassword, Salt, CreatedAtUtc, AccountState, Email, UpdatedAtUtc, ActivationKey, LoginAttempts, LastLoginAtUtc) " + + " VALUES(@Username, @HashedPassword, @Salt, @CreatedAtUtc, @AccountState, @Email, @UpdatedAtUtc, @ActivationKey, @LoginAttempts, @LastLoginAtUtc); RETURNING Id;"; + } + cmd.AddParameter("@Username", account.UserName); + cmd.AddParameter("@HashedPassword", account.HashedPassword); + cmd.AddParameter("@Salt", account.Salt); + cmd.AddParameter("@CreatedAtUtc", account.CreatedAtUtc); + cmd.AddParameter("@AccountState", account.AccountState.ToString()); + cmd.AddParameter("@Email", account.Email); + cmd.AddParameter("@UpdatedAtUtc", + account.UpdatedAtUtc == DateTime.MinValue ? (object) null : account.UpdatedAtUtc); + cmd.AddParameter("@ActivationKey", account.ActivationKey); + cmd.AddParameter("@LoginAttempts", account.LoginAttempts); + cmd.AddParameter("@LastLoginAtUtc", + account.LastLoginAtUtc == DateTime.MinValue ? (object) null : account.LastLoginAtUtc); + + if (account.Id > 0) + { + cmd.ExecuteNonQuery(); + } + else + { + var value = (int) cmd.ExecuteScalar(); + account.GetType().GetProperty("Id").SetValue(account, value); + } + } + } + + public async Task GetByUserNameAsync(string userName) + { + if (userName == null) throw new ArgumentNullException(nameof(userName)); + + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = "SELECT * FROM Accounts WHERE UserName=@userName;"; + cmd.AddParameter("userName", userName); + return await cmd.FirstAsync(new AccountMapper()); + } + } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs new file mode 100644 index 00000000..b056e26c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Queries; +using Coderr.Server.Domain.Core.Account; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; + +namespace Coderr.Server.PostgreSQL.Core.Accounts.QueryHandlers +{ + public class GetAccountEmailByIdHandler : IQueryHandler + { + private readonly IAccountRepository _accountRepository; + + public GetAccountEmailByIdHandler(IAccountRepository accountRepository) + { + if (accountRepository == null) throw new ArgumentNullException(nameof(accountRepository)); + _accountRepository = accountRepository; + } + + public async Task HandleAsync(IMessageContext context, GetAccountEmailById query) + { + var usr = await _accountRepository.GetByIdAsync((int) query.AccountId); + return usr.Email; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/ListAccountsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/ListAccountsHandler.cs new file mode 100644 index 00000000..8a696937 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Accounts/QueryHandlers/ListAccountsHandler.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Queries; +using DotNetCqs; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Accounts.QueryHandlers +{ + public class ListAccountsHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + private static readonly IEntityMapper _mapper = new MirrorMapper(); + + public ListAccountsHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, ListAccounts query) + { + var sql = "SELECT Id AccountId, UserName, CreatedAtUtc, Email FROM Accounts;"; + var users = await _unitOfWork.ToListAsync(_mapper, sql); + return new ListAccountsResult() { Accounts = users.ToArray() }; + } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/ApiKeyRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/ApiKeyRepository.cs new file mode 100644 index 00000000..9051b004 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/ApiKeyRepository.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.App.Core.ApiKeys; +using Coderr.Server.PostgreSQL.Core.ApiKeys.Mappings; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys +{ + /// + /// SQL Server implementation of . + /// + [ContainerService] + public class ApiKeyRepository : IApiKeyRepository + { + private readonly IAdoNetUnitOfWork _uow; + + /// + /// Creates a new instance of . + /// + /// Active unit of work + public ApiKeyRepository(IAdoNetUnitOfWork uow) + { + if (uow == null) throw new ArgumentNullException(nameof(uow)); + + _uow = uow; + } + + /// + /// Delete all mappings that are for a specific application + /// + /// id for the ApiKey that the application is associated with + /// Application to remove mapping for + /// + public Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId) + { + _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId AND ApplicationId = @appId;", + new { appId = applicationId, keyId = apiKeyId }); + return Task.FromResult(null); + } + + /// + /// Delete a specific ApiKey. + /// + /// + /// + public Task DeleteAsync(int keyId) + { + _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId;", new { keyId }); + _uow.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @keyId;", new { keyId }); + return Task.FromResult(null); + } + + /// + /// Get an key by using the generated string. + /// + /// key + /// key + /// Given key was not found. + public async Task GetByKeyAsync(string apiKey) + { + if (apiKey == null) throw new ArgumentNullException(nameof(apiKey)); + + var key = await _uow.FirstAsync("GeneratedKey=@1", apiKey); + var sql = "SELECT [ApplicationId] FROM [ApiKeyApplications] WHERE [ApiKeyId] = @1;"; + var apps = await _uow.ToListAsync(new IntMapper(), sql, key.Id); + + // apikeys without application restrictions should have access to all applications. + if (apps.Count == 0) + { + sql = "SELECT [Id] FROM [Applications];"; + apps = await _uow.ToListAsync(new IntMapper(), sql); + + } + + foreach (var app in apps) + { + key.Add(app); + } + + + return key; + } + + /// + /// Get all ApiKeys that maps to a specific application + /// + /// application id + /// list + public async Task> GetForApplicationAsync(int applicationId) + { + var apiKeyIds = new List(); + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = "SELECT ApiKeyId FROM ApiKeyApplications WHERE ApplicationId = @id;"; + cmd.AddParameter("id", applicationId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + apiKeyIds.Add(reader.GetInt32(0)); + } + } + } + + var keys = new List(); + foreach (var id in apiKeyIds) + { + var key = await GetByKeyIdAsync(id); + keys.Add(key); + } + return keys; + } + + /// + /// Create a new key + /// + /// key to create + /// task + public async Task CreateAsync(ApiKey key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + + await _uow.InsertAsync(key); + foreach (var claim in key.Claims.Where(x => x.Type == CoderrClaims.Application)) + { + AddApplication(key.Id, int.Parse(claim.Value)); + } + } + + /// + /// Get an key by using the generated string. + /// + /// PK + /// key + /// Given key was not found. + public async Task GetByKeyIdAsync(int id) + { + var key = await _uow.FirstAsync("id=@1", id); + var sql = "SELECT [ApplicationId] FROM [ApiKeyApplications] WHERE [ApiKeyId] = @1;"; + var apps = await _uow.ToListAsync(new IntMapper(), sql, key.Id); + foreach (var app in apps) + { + key.Add(app); + } + return key; + } + + /// + /// Update an existing key + /// + /// key + /// task + public async Task UpdateAsync(ApiKey key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + + await _uow.InsertAsync(key); + + var existingMappings = + await _uow.ToListAsync("SELECT ApplicationId FROM ApiKeyApplications WHERE ApiKeyId=@1;", + key); + + var apps = key.Claims + .Select(x => int.Parse(x.Value)) + .ToList(); + var removed = existingMappings.Except(apps); + foreach (var applicationId in removed) + { + _uow.Execute("DELETE FROM ApiKeyApplications WHERE ApiKeyId = @1 AND ApplicationId = @2;", + new[] { key.Id, applicationId }); + } + + var added = apps.Except(existingMappings); + foreach (var id in added) + { + AddApplication(key.Id, id); + } + } + + private void AddApplication(int apiKeyId, int applicationId) + { + _uow.Execute("INSERT INTO [ApiKeyApplications] (ApiKeyId, ApplicationId) VALUES(@api, @app);", new + { + api = apiKeyId, + app = applicationId + }); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/CreateApiKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/CreateApiKeyHandler.cs new file mode 100644 index 00000000..7869b15d --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/CreateApiKeyHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.ApiKeys.Commands; +using Coderr.Server.Api.Core.ApiKeys.Events; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Commands +{ + public class CreateApiKeyHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + private readonly IMessageBus _messageBus; + + public CreateApiKeyHandler(IAdoNetUnitOfWork unitOfWork, IMessageBus messageBus) + { + _unitOfWork = unitOfWork; + _messageBus = messageBus; + } + + public async Task HandleAsync(IMessageContext context, CreateApiKey command) + { + int id; + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "INSERT INTO ApiKeys (ApplicationName, GeneratedKey, SharedSecret, CreatedById, CreatedAtUtc) VALUES(@appName, @key, @secret, @by, @when); RETURNING id;"; + cmd.AddParameter("appName", command.ApplicationName); + cmd.AddParameter("key", command.ApiKey); + cmd.AddParameter("secret", command.SharedSecret); + cmd.AddParameter("by", command.AccountId); + cmd.AddParameter("when", DateTime.UtcNow); + id = (int) await cmd.ExecuteScalarAsync(); + } + + + foreach (var applicationId in command.ApplicationIds) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "INSERT INTO ApiKeyApplications (ApiKeyId, ApplicationId) VALUES(@key, @app);"; + cmd.AddParameter("app", applicationId); + cmd.AddParameter("key", id); + await cmd.ExecuteNonQueryAsync(); + } + } + + var evt = new ApiKeyCreated(command.ApplicationName, command.ApiKey, command.SharedSecret, + command.ApplicationIds, command.AccountId); + await context.SendAsync(evt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs new file mode 100644 index 00000000..af425f8b --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.ApiKeys.Commands; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Commands +{ + public class DeleteApiKeyHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public DeleteApiKeyHandler(IAdoNetUnitOfWork unitOfWork) + { + if (unitOfWork == null) throw new ArgumentNullException(nameof(unitOfWork)); + _unitOfWork = unitOfWork; + } + + public Task HandleAsync(IMessageContext context, DeleteApiKey command) + { + int id; + if (!string.IsNullOrEmpty(command.ApiKey)) + { + id = + (int) + _unitOfWork.ExecuteScalar("SELECT Id FROM ApiKeys WHERE GeneratedKey = @key;", + new {key = command.ApiKey}); + } + else + { + id = command.Id; + } + + _unitOfWork.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @id;", new {id}); + _unitOfWork.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @id;", new {id}); + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/EditApiKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/EditApiKeyHandler.cs new file mode 100644 index 00000000..af12f377 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Commands/EditApiKeyHandler.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.ApiKeys.Commands; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Commands +{ + public class EditApiKeyHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + private readonly IMessageBus _messageBus; + + public EditApiKeyHandler(IAdoNetUnitOfWork unitOfWork, IMessageBus messageBus) + { + _unitOfWork = unitOfWork; + _messageBus = messageBus; + } + + public async Task HandleAsync(IMessageContext context, EditApiKey command) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "UPDATE ApiKeys SET ApplicationName=@appName WHERE Id = @id;"; + cmd.AddParameter("appName", command.ApplicationName); + cmd.AddParameter("id", command.Id); + await cmd.ExecuteNonQueryAsync(); + } + + + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = "DELETE FROM ApiKeyApplications WHERE ApiKeyId = @id;"; + cmd.AddParameter("id", command.Id); + await cmd.ExecuteNonQueryAsync(); + } + + foreach (var applicationId in command.ApplicationIds) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "INSERT INTO ApiKeyApplications (ApiKeyId, ApplicationId) VALUES(@key, @app);"; + cmd.AddParameter("app", applicationId); + cmd.AddParameter("key", command.Id); + await cmd.ExecuteNonQueryAsync(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/ApiKeyMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/ApiKeyMapper.cs new file mode 100644 index 00000000..85c6873e --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/ApiKeyMapper.cs @@ -0,0 +1,16 @@ +using Coderr.Server.App.Core.ApiKeys; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Mappings +{ + public class ApiKeyMapper : CrudEntityMapper + { + public ApiKeyMapper() : base("ApiKeys") + { + Property(x => x.Id) + .PrimaryKey(true); + Property(x => x.Claims) + .Ignore(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/IntMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/IntMapper.cs new file mode 100644 index 00000000..a5464256 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Mappings/IntMapper.cs @@ -0,0 +1,21 @@ +using System.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Mappings +{ + public class IntMapper : IEntityMapper + { + public object Create(IDataRecord record) + { + return record[0]; + } + + public void Map(IDataRecord source, object destination) + { + } + + public void Map(IDataRecord source, int destination) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/GetApiKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/GetApiKeyHandler.cs new file mode 100644 index 00000000..6834e61c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/GetApiKeyHandler.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.ApiKeys.Queries; +using Coderr.Server.App.Core.ApiKeys; +using Coderr.Server.PostgreSQL.Core.ApiKeys.Mappings; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Queries +{ + /// + /// Handler for . + /// + public class GetApiKeyHandler : IQueryHandler + { + private static readonly MirrorMapper _appMapping = + new MirrorMapper(); + + private readonly IAdoNetUnitOfWork _uow; + + /// + /// Creates a new instance of . + /// + /// valid uow + public GetApiKeyHandler(IAdoNetUnitOfWork uow) + { + if (uow == null) throw new ArgumentNullException(nameof(uow)); + + _uow = uow; + } + + /// Method used to execute the query + /// Query to execute. + /// Task which will contain the result once completed. + public async Task HandleAsync(IMessageContext context, GetApiKey query) + { + if (query == null) throw new ArgumentNullException(nameof(query)); + + ApiKey key; + if (!string.IsNullOrEmpty(query.ApiKey)) + key = await _uow.FirstAsync("GeneratedKey=@1", query.ApiKey); + else + key = await _uow.FirstAsync("Id=@1", query.Id); + + var result = new GetApiKeyResult + { + ApplicationName = key.ApplicationName, + CreatedAtUtc = key.CreatedAtUtc, + CreatedById = key.CreatedById, + GeneratedKey = key.GeneratedKey, + Id = key.Id, + SharedSecret = key.SharedSecret + }; + + var sql = @"SELECT Id as ApplicationId, Name as ApplicationName +FROM Applications +JOIN ApiKeyApplications ON (Id = ApplicationId) +WHERE ApiKeyId = @1;"; + var apps = await _uow.ToListAsync(_appMapping, sql, key.Id); + result.AllowedApplications = apps.ToArray(); + return result; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/ListApiKeysHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/ListApiKeysHandler.cs new file mode 100644 index 00000000..f046438b --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/ApiKeys/Queries/ListApiKeysHandler.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.ApiKeys.Queries; +using Coderr.Server.PostgreSQL.Core.ApiKeys.Mappings; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.ApiKeys.Queries +{ + public class ListApiKeysHandler : IQueryHandler + { + private readonly MirrorMapper _mapper = new MirrorMapper(); + private readonly IAdoNetUnitOfWork _unitOfWork; + + public ListApiKeysHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, ListApiKeys query) + { + var keys = + await + _unitOfWork.ToListAsync(_mapper, + "SELECT ID, GeneratedKey ApiKey, ApplicationName FROM ApiKeys ORDER BY ApplicationName;"); + return new ListApiKeysResult {Keys = keys.ToArray()}; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationMapper.cs new file mode 100644 index 00000000..ed6bb065 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationMapper.cs @@ -0,0 +1,15 @@ +using System; +using Coderr.Server.Domain.Core.Applications; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Applications +{ + public class ApplicationMapper : CrudEntityMapper + { + public ApplicationMapper() : base("Applications") + { + Property(x => x.ApplicationType) + .ToPropertyValue(o => (TypeOfApplication) Enum.Parse(typeof(TypeOfApplication), (string) o)); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationRepository.cs new file mode 100644 index 00000000..332cf192 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/ApplicationRepository.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.Applications; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Applications +{ + [ContainerService] + public class ApplicationRepository : IApplicationRepository + { + private readonly IAdoNetUnitOfWork _uow; + + public ApplicationRepository(IAdoNetUnitOfWork uow) + { + if (uow == null) throw new ArgumentNullException("uow"); + _uow = uow; + } + + public async Task CreateAsync(ApplicationTeamMember member) + { + await _uow.InsertAsync(member); + } + + public async Task GetForUserAsync(int accountId) + { + if (accountId <= 0) throw new ArgumentOutOfRangeException(nameof(accountId)); + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = @"SELECT a.Id ApplicationId, a.Name ApplicationName, ApplicationMembers.Roles, a.NumberOfFtes NumberOfDevelopers + FROM Applications a + JOIN ApplicationMembers ON (ApplicationMembers.ApplicationId = a.Id) + WHERE ApplicationMembers.AccountId = @userId + ORDER BY Name;"; + cmd.AddParameter("userId", accountId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var apps = new List(); + while (await reader.ReadAsync()) + { + var numberOfDevelopers = reader.GetValue(3); + var a = new UserApplication + { + IsAdmin = reader.GetString(2).Contains("Admin"), + ApplicationName = reader.GetString(1), + ApplicationId = reader.GetInt32(0), + NumberOfDevelopers = numberOfDevelopers is DBNull ? null : (decimal?)numberOfDevelopers + }; + apps.Add(a); + } + + return apps.ToArray(); + } + } + } + + public async Task RemoveTeamMemberAsync(int applicationId, string invitedEmailAddress) + { + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = "DELETE FROM ApplicationMembers WHERE ApplicationId=@appId AND EmailAddress = @email;"; + cmd.AddParameter("appId", applicationId); + cmd.AddParameter("email", invitedEmailAddress); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task UpdateAsync(ApplicationTeamMember member) + { + await _uow.UpdateAsync(member); + } + + public async Task> GetTeamMembersAsync(int applicationId) + { + return await _uow.ToListAsync(@"SELECT Users.UserName, ApplicationMembers.* + FROM ApplicationMembers + LEFT JOIN Users ON (Users.AccountId = ApplicationMembers.AccountId) + WHERE ApplicationId = @1;", applicationId); + } + + + public async Task GetByKeyAsync(string appKey) + { + if (appKey == null) throw new ArgumentNullException("appKey"); + + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = + "SELECT * FROM Applications WHERE AppKey = @id;"; + + cmd.AddParameter("id", appKey); + var item = await cmd.FirstOrDefaultAsync(new ApplicationMapper()); + if (item == null) + throw new EntityNotFoundException(appKey, cmd); + return item; + } + } + + /*Id uniqueidentifier not null primary key, + Title nvarchar(50) not null, + AppKey uniqueidentifier not null, + OrganizationId uniqueidentifier not null, + CreatedById uniqueidentifier not null, + CreatedAtUtc datetime2 not null, + ApplicationType varchar(40) not null, + SharedSecret uniqueidentifier not null,*/ + + public async Task GetByIdAsync(int id) + { + if (id == 0) + throw new ArgumentNullException("id"); + + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = + "SELECT * FROM Applications WHERE Id = @id;"; + + cmd.AddParameter("id", id); + var item = await cmd.FirstOrDefaultAsync(); + if (item == null) + throw new EntityNotFoundException("Failed to find application with id " + id, cmd); + + return item; + } + } + + public async Task CreateAsync(Application application) + { + if (application == null) throw new ArgumentNullException("application"); + + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = + @"INSERT INTO Applications (Name, AppKey, CreatedById, CreatedAtUtc, ApplicationType, SharedSecret, EstimatedNumberOfErrors, NumberOfFtes) + VALUES(@Name, @AppKey, @CreatedById, @CreatedAtUtc, @ApplicationType, @SharedSecret, @EstimatedNumberOfErrors, @NumberOfFtes);RETURNING Id;"; + cmd.AddParameter("Name", application.Name); + cmd.AddParameter("AppKey", application.AppKey); + cmd.AddParameter("CreatedById", application.CreatedById); + cmd.AddParameter("CreatedAtUtc", application.CreatedAtUtc); + cmd.AddParameter("ApplicationType", application.ApplicationType.ToString()); + cmd.AddParameter("SharedSecret", application.SharedSecret); + cmd.AddParameter("EstimatedNumberOfErrors", application.EstimatedNumberOfErrors); + cmd.AddParameter("NumberOfFtes", application.NumberOfFtes); + var item = (decimal) await cmd.ExecuteScalarAsync(); + application.GetType().GetProperty("Id").SetValue(application, (int) item); + } + } + + + public async Task DeleteAsync(int applicationId) + { + if (applicationId == 0) throw new ArgumentNullException("applicationId"); + + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = + "DELETE FROM Applications WHERE Id = @id;"; + + //TODO: Delete reports?? + // or save them for future analysis? + + cmd.AddParameter("id", applicationId); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task GetAllAsync() + { + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = "SELECT * FROM Applications ORDER BY Name;"; + + //cmd.AddParameter("ids", string.Join(", ", appIds.Select(x => "'" + x + "'"))); + var result = await cmd.ToListAsync(); + return result.ToArray(); + } + } + + public async Task UpdateAsync(Application entity) + { + await _uow.UpdateAsync(entity); + } + + public async Task RemoveTeamMemberAsync(int applicationId, int userId) + { + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = "DELETE FROM ApplicationMembers WHERE ApplicationId=@appId AND AccountId = @userId;"; + cmd.AddParameter("appId", applicationId); + cmd.AddParameter("userId", userId); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task DeleteAsync(Application application) + { + if (application == null) throw new ArgumentNullException("application"); + await DeleteAsync(application.Id); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Applications/GetApplicationByAppKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/GetApplicationByAppKeyHandler.cs new file mode 100644 index 00000000..7f6fceb7 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/GetApplicationByAppKeyHandler.cs @@ -0,0 +1,34 @@ +//using System; +//using System.Threading.Tasks; +//using Coderr.Server.ReportAnalyzer.Abstractions; +//using Coderr.Core.Api.Applications; +// + +//namespace Coderr.Data.Applications +//{ +// public class GetApplicationByAppKeyHandler : IQueryHandler +// { +// private readonly IAdoNetUnitOfWork _uow; + +// public GetApplicationByAppKeyHandler(SomeUow uow) +// { +// if (uow == null) throw new ArgumentNullException("uow"); +// _uow = uow; +// } + + +// public async Task ExecuteAsync(IMessageContext context, GetApplicationByAppKey query) +// { +// using (var cmd = _uow.CreateCommand()) +// { +// cmd.CommandText = +// "SELECT * FROM Applications WHERE AppKey = @id"; + +// cmd.AddParameter("id", query.AppKey); +// var result = await cmd.FirstOrDefaultAsync(new ApplicationMapper()); +// return new ApplicationResult(result); +// } +// } +// } +//} + diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs new file mode 100644 index 00000000..f23c7618 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.Applications.Queries +{ + public class GetApplicationIdByKeyHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _uow; + + public GetApplicationIdByKeyHandler(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public async Task HandleAsync(IMessageContext context, GetApplicationIdByKey query) + { + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = "SELECT Id FROM Applications WHERE AppKey = @appKey;"; + cmd.AddParameter("appKey", query.ApplicationKey); + var result = await cmd.ExecuteScalarAsync(); + if (result == null) + return null; + + return new GetApplicationIdByKeyResult {Id = (int)result }; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationOverviewHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationOverviewHandler.cs new file mode 100644 index 00000000..16bfe929 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Applications/Queries/GetApplicationOverviewHandler.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Queries; +using Coderr.Server.Domain.Core.Incidents; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.Applications.Queries +{ + internal class GetApplicationOverviewHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetApplicationOverviewHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetApplicationOverview query) + { + if (query.NumberOfDays == 0) + query.NumberOfDays = 30; + + if (query.NumberOfDays == 1) + return await GetTodaysOverviewAsync(query); + + var curDate = DateTime.Today.AddDays(-query.NumberOfDays); + var errorReports = new Dictionary(); + var incidents = new Dictionary(); + while (curDate <= DateTime.Today) + { + errorReports[curDate] = 0; + incidents[curDate] = 0; + curDate = curDate.AddDays(1); + } + + var result = new GetApplicationOverviewResult(); + using (var cmd = _unitOfWork.CreateDbCommand()) + { + string filter1; + string filter2; + if (query.Version != null) + { + var id = _unitOfWork.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version = @version", + new { version = query.Version }); + filter1 = @"JOIN IncidentVersions On (Incidents.Id = IncidentVersions.IncidentId) + WHERE IncidentVersions.VersionId = @versionId AND "; + filter2 = @"JOIN IncidentVersions On (IncidentReports.IncidentId = IncidentVersions.IncidentId) + WHERE IncidentVersions.VersionId = @versionId AND "; + cmd.AddParameter("versionId", id); + } + else + { + filter1 = "WHERE "; + filter2 = "WHERE "; + } + var sql = @"select cast(Incidents.CreatedAtUtc as date), count(Incidents.Id) +from Incidents +{2} Incidents.CreatedAtUtc >= @minDate +AND Incidents.CreatedAtUtc <= GetUtcDate() +{0} +group by cast(Incidents.CreatedAtUtc as date); +select cast(IncidentReports.ReceivedAtUtc as date), count(IncidentReports.Id) +from IncidentReports +join Incidents isa ON (isa.Id = IncidentReports.IncidentId) +{3} IncidentReports.ReceivedAtUtc >= @minDate +AND IncidentReports.ReceivedAtUtc <= GetUtcDate() +{1} +group by cast(IncidentReports.ReceivedAtUtc as date);"; + + if (query.ApplicationId > 0) + { + cmd.CommandText = string.Format(sql, + " AND Incidents.ApplicationId = @appId", + " AND isa.ApplicationId = @appId", + filter1, filter2); + cmd.AddParameter("appId", query.ApplicationId); + } + else + { + cmd.CommandText = string.Format(sql, "", "", filter1, filter2); + } + + cmd.AddParameter("minDate", DateTime.Today.AddDays(-query.NumberOfDays)); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + incidents[(DateTime)reader[0]] = (int)reader[1]; + } + await reader.NextResultAsync(); + while (await reader.ReadAsync()) + { + errorReports[(DateTime)reader[0]] = (int)reader[1]; + } + + result.ErrorReports = errorReports.Select(x => x.Value).ToArray(); + result.Incidents = incidents.Select(x => x.Value).ToArray(); + result.TimeAxisLabels = incidents.Select(x => x.Key.ToString("yyyy-MM-dd")).ToArray(); + } + } + + await GetStatSummary(query, result); + + + return result; + } + + private async Task GetStatSummary(GetApplicationOverview query, GetApplicationOverviewResult result) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + if (!string.IsNullOrEmpty(query.Version)) + { + var versionId = + _unitOfWork.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version=@version", + new {version = query.Version}); + cmd.CommandText = $@"select count(id) +from incidents +JOIN IncidentVersions ON (Incidents.Id = IncidentVersions.IncidentId) +WHERE IncidentVersions.VersionId = @versionId +AND CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId = @appId +AND Incidents.State <> {(int)IncidentState.Ignored} +AND Incidents.State <> {(int)IncidentState.Closed}; + +SELECT count(id) from IncidentReports +JOIN IncidentVersions ON (IncidentReports.IncidentId = IncidentVersions.IncidentId) +WHERE IncidentVersions.VersionId = @versionId +AND ReceivedAtUtc >= @minDate +AND ReceivedAtUtc <= GetUtcDate() +AND ApplicationId = @appId; + +SELECT count(distinct emailaddress) +from IncidentFeedback +JOIN IncidentVersions ON (IncidentFeedback.IncidentId = IncidentVersions.IncidentId) +WHERE IncidentVersions.VersionId = @versionId +AND CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId = @appId +AND emailaddress is not null +AND DATALENGTH(emailaddress) > 0; + +select count(*) +from IncidentFeedback +JOIN IncidentVersions ON (IncidentFeedback.IncidentId = IncidentVersions.IncidentId) +WHERE IncidentVersions.VersionId = @versionId +AND CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId = @appId +AND Description is not null +AND DATALENGTH(Description) > 0;"; + cmd.AddParameter("versionId", versionId); + } + else + { + cmd.CommandText = $@"select count(id) from incidents +where CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId = @appId +AND Incidents.State <> {(int)IncidentState.Ignored} +AND Incidents.State <> {(int)IncidentState.Closed}; + +SELECT count(IncidentReports.id) +FROM IncidentReports +JOIN Incidents ON (Incidents.Id = IncidentReports.IncidentId) +WHERE ReceivedAtUtc >= @minDate +AND ReceivedAtUtc <= GetUtcDate() +AND ApplicationId = @appId; + +select count(distinct emailaddress) +from IncidentFeedback +where CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId = @appId +AND emailaddress is not null +AND DATALENGTH(emailaddress) > 0; + +select count(*) +from IncidentFeedback +where CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId = @appId +AND Description is not null +AND DATALENGTH(Description) > 0;"; + + } + cmd.AddParameter("appId", query.ApplicationId); + var minDate = query.NumberOfDays == 1 + ? DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23) + : DateTime.Today.AddDays(-query.NumberOfDays); + cmd.AddParameter("minDate", minDate); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (!await reader.ReadAsync()) + { + throw new InvalidOperationException("Expected to be able to read."); + } + + var data = new OverviewStatSummary {Incidents = reader.GetInt32(0)}; + await reader.NextResultAsync(); + await reader.ReadAsync(); + data.Reports = reader.GetInt32(0); + await reader.NextResultAsync(); + await reader.ReadAsync(); + data.Followers = reader.GetInt32(0); + await reader.NextResultAsync(); + await reader.ReadAsync(); + data.UserFeedback = reader.GetInt32(0); + result.StatSummary = data; + } + } + } + + private async Task GetTodaysOverviewAsync(GetApplicationOverview query) + { + var result = new GetApplicationOverviewResult + { + TimeAxisLabels = new string[24] + }; + var incidentValues = new Dictionary(); + var reportValues = new Dictionary(); + + var startDate = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23); + for (var i = 0; i < 24; i++) + { + result.TimeAxisLabels[i] = startDate.AddHours(i).ToString("HH:mm"); + incidentValues[startDate.AddHours(i)] = 0; + reportValues[startDate.AddHours(i)] = 0; + } + var filter1 = ""; + var filter2 = ""; + using (var cmd = _unitOfWork.CreateDbCommand()) + { + if (query.Version != null) + { + var id = _unitOfWork.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version = @version", + new { version = query.Version }); + filter1 = @"JOIN IncidentVersions On (Incidents.Id = IncidentVersions.IncidentId) + WHERE IncidentVersions.VersionId = @versionId AND "; + filter2 = @"JOIN IncidentVersions On (IncidentReports.IncidentId = IncidentVersions.IncidentId) + WHERE IncidentVersions.VersionId = @versionId AND "; + cmd.AddParameter("versionId", id); + } + else + { + filter1 = "WHERE "; + filter2 = "WHERE "; + } + + var sql = @"SELECT DATEPART(HOUR, Incidents.CreatedAtUtc), cast(count(Id) as int) + from Incidents + {0} Incidents.CreatedAtUtc >= @minDate + AND Incidents.CreatedAtUtc <= GetUtcDate() + AND Incidents.ApplicationId = @appId + group by DATEPART(HOUR, Incidents.CreatedAtUtc); + select DATEPART(HOUR, IncidentReports.ReceivedAtUtc), cast(count(Id) as int) + from IncidentReports + JOIN Incidents ice ON (ice.Id = IncidentId) + {1} IncidentReports.ReceivedAtUtc >= @minDate + AND IncidentReports.ReceivedAtUtc <= GetUtcDate() + AND ice.ApplicationId = @appId + group by DATEPART(HOUR, IncidentReports.ReceivedAtUtc);"; + + cmd.CommandText = string.Format(sql, filter1, filter2); + cmd.AddParameter("appId", query.ApplicationId); + cmd.AddParameter("minDate", startDate); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var todayWithHour = DateTime.Today.AddHours(DateTime.Now.Hour); + while (await reader.ReadAsync()) + { + var hour = reader.GetInt32(0); + var date = hour < todayWithHour.Hour + ? DateTime.Today.AddHours(hour) + : DateTime.Today.AddDays(-1).AddHours(hour); + incidentValues[date] = reader.GetInt32(1); + } + await reader.NextResultAsync(); + while (await reader.ReadAsync()) + { + var hour = reader.GetInt32(0); + var date = hour < todayWithHour.Hour + ? DateTime.Today.AddHours(hour) + : DateTime.Today.AddDays(-1).AddHours(hour); + reportValues[date] = reader.GetInt32(1); + } + } + } + + result.ErrorReports = reportValues.Values.ToArray(); + result.Incidents = incidentValues.Values.ToArray(); + + //a bit weird, but required since the method + await GetStatSummary(query, result); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsHandler.cs new file mode 100644 index 00000000..c963e572 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsHandler.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Environments.Queries; +using Coderr.Server.PostgreSQL.Core.ApiKeys.Mappings; +using DotNetCqs; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Environments +{ + public class GetEnvironmentsHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetEnvironmentsHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetEnvironments query) + { + var result = new GetEnvironmentsResult(); + + string sql; + if (query.ApplicationId == null) + { + sql = @"select Id, Name from Environments ORDER BY Name;"; + } + else + { + sql = @"WITH EnvironmentIds + AS + ( + select distinct EnvironmentId + FROM IncidentEnvironments + JOIN Incidents ON (IncidentId = Incidents.Id) + WHERE ApplicationId = @applicationId + ) + SELECT Id, Name + FROM Environments + JOIN EnvironmentIds ON (EnvironmentId=Environments.Id);"; + } + + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("applicationId", query.ApplicationId); + var items = await cmd.ToListAsync(); + result.Items = items.ToArray(); + return result; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsResultItemMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsResultItemMapper.cs new file mode 100644 index 00000000..bcca0b3a --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Environments/GetEnvironmentsResultItemMapper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Coderr.Server.Api.Core.Environments.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Environments +{ + class GetEnvironmentsResultItemMapper : EntityMapper + { + + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Environments/ResetEnvironmentHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Environments/ResetEnvironmentHandler.cs new file mode 100644 index 00000000..5653e73d --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Environments/ResetEnvironmentHandler.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Environments.Commands; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.Environments +{ + internal class ResetEnvironmentHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public ResetEnvironmentHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public Task HandleAsync(IMessageContext context, ResetEnvironment message) + { + var sql = @"WITH JustOurIncidents (IncidentId) AS + ( + select ie.IncidentId + from IncidentEnvironments ie + join Incidents i ON (i.Id = ie.IncidentId) + join Environments e ON (ie.EnvironmentId = e.Id) + where i.ApplicationId = @applicationId + group by ie.IncidentId + having count(e.Id) = 1 + ) + DELETE IncidentEnvironments + FROM IncidentEnvironments + JOIN JustOurIncidents ON (JustOurIncidents.IncidentId = IncidentEnvironments.IncidentId) + WHERE IncidentEnvironments.EnvironmentId = @environmentId;"; + + _unitOfWork.ExecuteNonQuery(sql, new {message.ApplicationId, message.EnvironmentId}); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/CheckForFeedbackNotificationsToSend.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/CheckForFeedbackNotificationsToSend.cs new file mode 100644 index 00000000..80028797 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/CheckForFeedbackNotificationsToSend.cs @@ -0,0 +1,78 @@ +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Api.Core.Accounts.Queries; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Modules.UserNotifications; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.ReportAnalyzer.Abstractions.Feedback; +using Coderr.Server.ReportAnalyzer.UserNotifications.Handlers; +using DotNetCqs; + +namespace Coderr.Server.PostgreSQL.Core.Feedback +{ + /// + /// Responsible of sending notifications when a new report have been analyzed. + /// + // MUST be here so that it's used from both queues. + public class CheckForFeedbackNotificationsToSend : + IMessageHandler + { + private readonly IUserNotificationsRepository _notificationsRepository; + private ConfigurationStore _configStore; + private IIncidentRepository _incidentRepository; + + /// + /// Creates a new instance of . + /// + /// To load notification configuration + public CheckForFeedbackNotificationsToSend(IUserNotificationsRepository notificationsRepository, ConfigurationStore configStore, IIncidentRepository incidentRepository) + { + _notificationsRepository = notificationsRepository; + _configStore = configStore; + _incidentRepository = incidentRepository; + } + + /// + public async Task HandleAsync(IMessageContext context, FeedbackAttachedToIncident e) + { + var settings = await _notificationsRepository.GetAllAsync(-1); + var incident = await _incidentRepository.GetAsync(e.IncidentId); + foreach (var setting in settings) + { + if (setting.UserFeedback == NotificationState.Disabled) + continue; + + var notificationEmail = await context.QueryAsync(new GetAccountEmailById(setting.AccountId)); + var config = _configStore.Load(); + + var shortName = incident.Description.Length > 40 + ? incident.Description.Substring(0, 40) + "..." + : incident.Description; + + if (string.IsNullOrEmpty(e.UserEmailAddress)) + e.UserEmailAddress = "unknown"; + + var incidentUrl = string.Format("{0}/discover/{1}/incident/{2}", + config.BaseUrl.ToString().TrimEnd('/'), + incident.ApplicationId, + incident.Id); + + //TODO: Add more information + var msg = new EmailMessage(notificationEmail); + msg.Subject = "New feedback: " + shortName; + msg.TextBody = string.Format(@"Incident: {0} +Feedback: {0}/feedback +From: {1} + +{2} +", incidentUrl, e.UserEmailAddress, e.Message); + + + var emailCmd = new SendEmail(msg); + await context.SendAsync(emailCmd); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackEntityMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackEntityMapper.cs new file mode 100644 index 00000000..f9b66e9e --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackEntityMapper.cs @@ -0,0 +1,15 @@ +using Coderr.Server.Domain.Core.Feedback; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Feedback +{ + public class FeedbackEntityMapper : CrudEntityMapper + { + public FeedbackEntityMapper() : base("IncidentFeedback") + { + Property(x => x.ErrorId).ColumnName("ErrorReportId"); + Property(x => x.CanRemove).Ignore(); + Property(x => x.CanUpdate).Ignore(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackRepository.cs new file mode 100644 index 00000000..9ccd6135 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/FeedbackRepository.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.Feedback; +using Coderr.Server.ReportAnalyzer.Feedback; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Feedback +{ + [ContainerService] + public class FeedbackRepository : IFeedbackRepository, IUserFeedbackRepository + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public FeedbackRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task FindPendingAsync(string reportId) + { + return await _unitOfWork.FirstOrDefaultAsync(new { ErrorId = reportId }); + } + + public async Task UpdateAsync(UserFeedback feedback) + { + await _unitOfWork.UpdateAsync(feedback); + } + + public async Task> GetEmailAddressesAsync(int incidentId) + { + var emailAddresses = new List(); + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "SELECT distinct EmailAddress FROM IncidentFeedback WHERE IncidentId = @id AND EmailAddress IS NOT NULL;"; + cmd.AddParameter("id", incidentId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var email = reader.GetString(0); + if (!emailAddresses.Any(x => x.Equals(email, StringComparison.OrdinalIgnoreCase))) + emailAddresses.Add(email); + } + } + } + + return emailAddresses; + } + + public Task CreateAsync(NewFeedback feedback) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/SubmitFeedbackHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/SubmitFeedbackHandler.cs new file mode 100644 index 00000000..9daa8ceb --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Feedback/SubmitFeedbackHandler.cs @@ -0,0 +1,110 @@ +using System; +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Feedback.Commands; +using Coderr.Server.Domain.Core.ErrorReports; +using Coderr.Server.ReportAnalyzer.Abstractions.Feedback; +using DotNetCqs; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.PostgreSQL.Core.Feedback +{ + public class SubmitFeedbackHandler : IMessageHandler + { + private readonly ILog _logger = LogManager.GetLogger(typeof(SubmitFeedbackHandler)); + private readonly IReportsRepository _reportsRepository; + private readonly IAdoNetUnitOfWork _unitOfWork; + + public SubmitFeedbackHandler(IAdoNetUnitOfWork unitOfWork, IReportsRepository reportsRepository) + { + _unitOfWork = unitOfWork; + _reportsRepository = reportsRepository; + } + + public async Task HandleAsync(IMessageContext context, SubmitFeedback command) + { + if (string.IsNullOrEmpty(command.Email)) + { + if (string.IsNullOrEmpty(command.Feedback)) + return; + if (command.Feedback.Length < 3) + return; + } + + ReportMapping report2; + int? reportId = null; + if (command.ReportId > 0) + { + var report = await _reportsRepository.GetAsync(command.ReportId); + report2 = new ReportMapping + { + ApplicationId = report.ApplicationId, + ErrorId = report.ClientReportId, + IncidentId = report.IncidentId, + ReceivedAtUtc = report.CreatedAtUtc + }; + reportId = report.Id; + } + else + report2 = await _reportsRepository.FindByErrorIdAsync(command.ErrorId); + + // storing it without connections as the report might not have been uploaded yet. + if (report2 == null) + { + _logger.InfoFormat( + "Failed to find report. Let's enqueue it instead for report {0}/{1}. Email: {2}, Feedback: {3}", + command.ReportId, command.ErrorId, command.Email, command.Feedback); + try + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = "INSERT INTO IncidentFeedback (ErrorReportId, RemoteAddress, Description, EmailAddress, CreatedAtUtc, Conversation, ConversationLength) " + + + "VALUES (@ErrorReportId, @RemoteAddress, @Description, @EmailAddress, @CreatedAtUtc, '', 0);"; + cmd.AddParameter("ErrorReportId", command.ErrorId); + cmd.AddParameter("RemoteAddress", command.RemoteAddress); + cmd.AddParameter("Description", command.Feedback); + cmd.AddParameter("EmailAddress", command.Email); + cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); + cmd.ExecuteNonQuery(); + } + } + catch (Exception exception) + { + _logger.Error( + $"{command.ErrorId}: Failed to store '{command.Email}' '{command.Feedback}'", exception); + //hide errors. + } + + return; + } + + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = "INSERT INTO IncidentFeedback (ErrorReportId, ApplicationId, ReportId, IncidentId, RemoteAddress, Description, EmailAddress, CreatedAtUtc, Conversation, ConversationLength) " + + + "VALUES (@ErrorReportId, @ApplicationId, @ReportId, @IncidentId, @RemoteAddress, @Description, @EmailAddress, @CreatedAtUtc, @Conversation, 0);"; + cmd.AddParameter("ErrorReportId", command.ErrorId); + cmd.AddParameter("ApplicationId", report2.ApplicationId); + cmd.AddParameter("ReportId", reportId); + cmd.AddParameter("IncidentId", report2.IncidentId); + cmd.AddParameter("RemoteAddress", command.RemoteAddress); + cmd.AddParameter("Description", command.Feedback); + cmd.AddParameter("EmailAddress", command.Email); + cmd.AddParameter("Conversation", ""); + cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); + + var evt = new FeedbackAttachedToIncident + { + Message = command.Feedback, + UserEmailAddress = command.Email, + IncidentId = report2.IncidentId + }; + await context.SendAsync(evt); + + await cmd.ExecuteNonQueryAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentMapper.cs new file mode 100644 index 00000000..394054d0 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentMapper.cs @@ -0,0 +1,38 @@ +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents +{ + public class IncidentMapper : CrudEntityMapper + { + public IncidentMapper() : base("Incidents") + { + Property(x => x.SolvedAtUtc) + .ToColumnValue(DbConverters.ToNullableSqlDate) + .ToPropertyValue(DbConverters.ToEntityDate); + + Property(x => x.LastSolutionAtUtc) + .ToColumnValue(DbConverters.ToNullableSqlDate) + .ToPropertyValue(DbConverters.ToEntityDate); + + Property(x => x.IgnoringReportsSinceUtc) + .ToColumnValue(DbConverters.ToNullableSqlDate) + .ToPropertyValue(DbConverters.ToEntityDate); + + Property(x => x.ReopenedAtUtc) + .ToColumnValue(DbConverters.ToNullableSqlDate) + .ToPropertyValue(DbConverters.ToEntityDate); + + Property(x => x.Solution) + .ToColumnValue(DbConverters.SerializeEntity) + .ToPropertyValue(EntitySerializer.Deserialize); + + Property(x => x.State); + + + Property(x => x.IsSolutionShared) + .ToPropertyValue(DbConverters.BoolFromByteArray); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentRepository.cs new file mode 100644 index 00000000..fe218558 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentRepository.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents +{ + [ContainerService] + public class IncidentRepository : IIncidentRepository + { + private readonly IAdoNetUnitOfWork _uow; + + public IncidentRepository(IAdoNetUnitOfWork uow) + { + if (uow == null) throw new ArgumentNullException("uow"); + + _uow = uow; + } + + public async Task UpdateAsync(Incident incident) + { + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + @"UPDATE Incidents SET + ApplicationId = @ApplicationId, + UpdatedAtUtc = @UpdatedAtUtc, + Description = @Description, + Solution = @Solution, + SolvedAtUtc = @solvedAt, + IsSolutionShared = @IsSolutionShared, + AssignedToId = @AssignedTo, + AssignedAtUtc = @AssignedAtUtc, + State = @state, + IgnoringReportsSinceUtc = @IgnoringReportsSinceUtc, + IgnoringRequestedBy = @IgnoringRequestedBy + WHERE Id = @id;"; + cmd.AddParameter("Id", incident.Id); + cmd.AddParameter("ApplicationId", incident.ApplicationId); + cmd.AddParameter("UpdatedAtUtc", incident.UpdatedAtUtc); + cmd.AddParameter("Description", incident.Description); + cmd.AddParameter("State", (int)incident.State); + cmd.AddParameter("AssignedTo", incident.AssignedToId); + cmd.AddParameter("AssignedAtUtc", (object)incident.AssignedAtUtc ?? DBNull.Value); + cmd.AddParameter("solvedAt", incident.SolvedAtUtc.ToDbNullable()); + cmd.AddParameter("IgnoringReportsSinceUtc", incident.IgnoringReportsSinceUtc.ToDbNullable()); + cmd.AddParameter("IgnoringRequestedBy", incident.IgnoringRequestedBy); + cmd.AddParameter("Solution", + incident.Solution == null ? null : EntitySerializer.Serialize(incident.Solution)); + cmd.AddParameter("IsSolutionShared", incident.IsSolutionShared); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task MapCorrelationId(int incidentId, string correlationId) + { + var sql = @"declare @id int; + select @id = Id FROM CorrelationIds WHERE Value = @value; + if (@id is NULL) + BEGIN + INSERT INTO CorrelationIds(Value) VALUES(@value); + set @id = scope_identity(); + END; + BEGIN TRY + INSERT INTO IncidentCorrelations (CorrelationId, IncidentId) VALUES (@id, @incidentId); + END TRY + BEGIN CATCH + IF ERROR_NUMBER() NOT IN (2601, 2627) + THROW; + END CATCH;"; + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("value", correlationId); + cmd.AddParameter("incidentId", incidentId); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task GetTotalCountForAppInfoAsync(int applicationId) + { + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + @"SELECT CAST(count(*) as int) FROM Incidents WHERE ApplicationId = @ApplicationId;"; + cmd.AddParameter("ApplicationId", applicationId); + var result = (int)await cmd.ExecuteScalarAsync(); + return result; + } + } + + public Task> GetManyAsync(IEnumerable incidentIds) + { + if (incidentIds == null) throw new ArgumentNullException(nameof(incidentIds)); + var ids = string.Join(",", incidentIds); + if (ids == "") + throw new ArgumentException("No incident IDs were specified.", nameof(incidentIds)); + + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + $"SELECT * FROM Incidents WHERE Id IN ({ids});"; + return cmd.ToListAsync(new IncidentMapper()); + } + } + + public async Task Delete(int incidentId) + { + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + @"DELETE FROM Incidents WHERE Id = @id;"; + cmd.AddParameter("Id", incidentId); + await cmd.ExecuteNonQueryAsync(); + } + } + + public Task GetAsync(int id) + { + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + "SELECT TOP 1 * FROM Incidents WHERE Id = @id;"; + + cmd.AddParameter("id", id); + return cmd.FirstAsync(new IncidentMapper()); + } + } + + public Incident Find(int id) + { + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = + "SELECT TOP 1 * FROM Incidents WHERE Id = @id;"; + + cmd.AddParameter("id", id); + return cmd.FirstOrDefault(new IncidentMapper()); + } + } + + public Incident Get(int id) + { + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = + "SELECT TOP 3 * FROM Incidents WHERE Id = @id;"; + + cmd.AddParameter("id", id); + return cmd.First(new IncidentMapper()); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentSummaryMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentSummaryMapper.cs new file mode 100644 index 00000000..f75b164c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/IncidentSummaryMapper.cs @@ -0,0 +1,29 @@ +using System; +using System.Data; +using Coderr.Server.Api.Core.Incidents; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents +{ + public class IncidentSummaryMapper : IEntityMapper + { + public object Create(IDataRecord record) + { + return new IncidentSummaryDTO((int) record["Id"], (string) record["Description"]); + } + + public void Map(IDataRecord source, object destination) + { + Map(source, (IncidentSummaryDTO) destination); + } + + public void Map(IDataRecord source, IncidentSummaryDTO destination) + { + destination.ApplicationId = (int) source["ApplicationId"]; + destination.ApplicationName = (string) source["ApplicationName"]; + destination.ReportCount = (int) source["Count"]; + destination.LastUpdateAtUtc = (DateTime) source["UpdatedAtUtc"]; + destination.CreatedAtUtc = (DateTime) source["CreatedAtUtc"]; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentResultItemMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentResultItemMapper.cs new file mode 100644 index 00000000..c1b0f72e --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentResultItemMapper.cs @@ -0,0 +1,39 @@ +using System; +using System.Data; +using Coderr.Server.Api.Core.Incidents.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + public class FindIncidentResultItemMapper : IEntityMapper + { + public object Create(IDataRecord record) + { + return new FindIncidentsResultItem((int) record["Id"], (string) record["Description"]); + } + + public void Map(IDataRecord source, object destination) + { + Map(source, (FindIncidentsResultItem) destination); + } + + public void Map(IDataRecord source, FindIncidentsResultItem destination) + { + destination.ApplicationName = (string) source["ApplicationName"]; + destination.ApplicationId = (int)source["ApplicationId"]; + destination.IsReOpened = source["IsReopened"].Equals(1); + destination.ReportCount = (int) source["ReportCount"]; + destination.CreatedAtUtc = (DateTime)source["CreatedAtUtc"]; + + var value = source["UpdatedAtUtc"]; + if (!(value is DBNull)) + destination.LastUpdateAtUtc = (DateTime) value; + + value = source["LastReportAtUtc"]; + destination.LastReportReceivedAtUtc = (DateTime) (value is DBNull ? destination.LastUpdateAtUtc : value); + + value = source["AssignedAtUtc"]; + destination.AssignedAtUtc = (DateTime?)(value is DBNull ? null : value); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentsHandler.cs new file mode 100644 index 00000000..4d7a9cbd --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/FindIncidentsHandler.cs @@ -0,0 +1,241 @@ +using System; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Incidents; +using Coderr.Server.Api.Core.Incidents.Queries; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + public class FindIncidentsHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _uow; + + public FindIncidentsHandler(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public async Task HandleAsync(IMessageContext context, FindIncidents query) + { + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + var sqlQuery = @"SELECT {0} + FROM Incidents + JOIN Applications ON (Applications.Id = Incidents.ApplicationId)"; + + var startWord = " WHERE "; + if (!string.IsNullOrEmpty(query.Version)) + { + var versionId = + _uow.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version = @version", + new { version = query.Version }); + + sqlQuery += " JOIN IncidentVersions ON (Incidents.Id = IncidentVersions.IncidentId)" + + " WHERE IncidentVersions.VersionId = @versionId"; + cmd.AddParameter("versionId", versionId); + startWord = " AND "; + } + if (query.Tags != null && query.Tags.Length > 0) + { + var ourSql = @" join IncidentTags on (Incidents.Id=IncidentTags.IncidentId AND IncidentTags.Id IN ( + SELECT MAX(IncidentTags.Id) + FROM IncidentTags + WHERE TagName IN ({0}) + AND IncidentTags.IncidentId=Incidents.Id + GROUP BY IncidentId + HAVING Count(IncidentTags.Id) = {1} + )) +"; + var ps = ""; + for (int i = 0; i < query.Tags.Length; i++) + { + ps += $"@tag{i}, "; + cmd.AddParameter($"@tag{i}", query.Tags[i]); + } + + sqlQuery += string.Format(ourSql, ps.Remove(ps.Length - 2, 2), query.Tags.Length); + } + + if (query.EnvironmentIds != null && query.EnvironmentIds.Length > 0) + { + sqlQuery += " JOIN IncidentEnvironments ON (Incidents.Id = IncidentEnvironments.IncidentId)" + + $" WHERE IncidentEnvironments.EnvironmentId IN ({string.Join(", ", query.EnvironmentIds)})"; + startWord = " AND "; + } + + if (!string.IsNullOrEmpty(query.ContextCollectionPropertyValue) + || !string.IsNullOrEmpty(query.ContextCollectionName) + || !string.IsNullOrEmpty(query.ContextCollectionPropertyName)) + { + var where = AddContextProperty(cmd, "", "Name", "ContextName", query.ContextCollectionName); + where += AddContextProperty(cmd, where, "PropertyName", "ContextPropertyName", query.ContextCollectionPropertyName); + where += AddContextProperty(cmd, where, "Value", "ContextPropertyValue", query.ContextCollectionPropertyValue); + if (where.EndsWith(" AND ")) + where = where.Remove(where.Length - 5, 5); + var ourSql = + $@"with ContextSearch (IncidentId) + as ( + select distinct(IncidentId) + from ErrorReports + join ErrorReportCollectionProperties ON (ErrorReports.Id = ErrorReportCollectionProperties.ReportId) + WHERE {where} + ) +"; + sqlQuery = ourSql + sqlQuery + " join ContextSearch ON (Incidents.Id = ContextSearch.IncidentId)\r\n"; + } + + if (query.ApplicationIds != null && query.ApplicationIds.Length > 0) + { + foreach (var applicationId in query.ApplicationIds) + { + if (!context.Principal.IsApplicationMember(applicationId) + && !context.Principal.IsSysAdmin() + && !context.Principal.IsApplicationAdmin(applicationId)) + throw new UnauthorizedAccessException( + "You are not a member of application " + applicationId); + } + + var ids = string.Join(",", query.ApplicationIds); + sqlQuery += $" {startWord} Applications.Id IN ({ids})"; + } + else if (!context.Principal.IsSysAdmin()) + { + var appIds = context.Principal.Claims + .Where(x => x.Type == CoderrClaims.Application) + .Select(x => x.Value) + .ToArray(); + if (!appIds.Any()) + { + return new FindIncidentsResult { Items = new FindIncidentsResultItem[0] }; + } + + sqlQuery += $" {startWord} Applications.Id IN({string.Join(",", appIds)})"; + } + + if (!string.IsNullOrWhiteSpace(query.FreeText)) + { + sqlQuery += @" AND ( + Incidents.Id IN + ( + SELECT Distinct IncidentId + FROM ErrorReports + WHERE StackTrace LIKE @FreeText + OR ErrorReports.Title LIKE @FreeText + OR ErrorReports.ErrorId LIKE @FreeText + OR Incidents.Description LIKE @FreeText) + )"; + cmd.AddParameter("FreeText", $"%{query.FreeText}%"); + } + + + + sqlQuery += " AND ("; + if (query.IsIgnored) + sqlQuery += $"State = {(int)IncidentState.Ignored} OR "; + if (query.IsNew) + sqlQuery += $"State = {(int)IncidentState.New} OR "; + if (query.IsClosed) + sqlQuery += $"State = {(int)IncidentState.Closed} OR "; + if (query.IsAssigned) + sqlQuery += $"State = {(int)IncidentState.Active} OR "; + if (query.ReOpened) + sqlQuery += "IsReOpened = 1 OR "; + + + if (sqlQuery.EndsWith("OR ")) + sqlQuery = sqlQuery.Remove(sqlQuery.Length - 4) + ") "; + else + sqlQuery = sqlQuery.Remove(sqlQuery.Length - 5); + + if (query.MinDate > DateTime.MinValue) + { + sqlQuery += " AND Incidents.LastReportAtUtc >= @minDate"; + cmd.AddParameter("minDate", query.MinDate); + } + if (query.MaxDate < DateTime.MaxValue) + { + sqlQuery += " AND Incidents.LastReportAtUtc <= @maxDate"; + cmd.AddParameter("maxDate", query.MaxDate); + } + + if (query.AssignedToId > 0) + { + sqlQuery += "AND AssignedToId = @assignedTo"; + cmd.AddParameter("assignedTo", query.AssignedToId); + } + + + + //count first; + cmd.CommandText = string.Format(sqlQuery, "count(Incidents.Id)"); + var count = await cmd.ExecuteScalarAsync(); + + + // then items + if (query.SortType == IncidentOrder.Newest) + { + if (query.SortAscending) + sqlQuery += " ORDER BY CreatedAtUtc"; + else + sqlQuery += " ORDER BY CreatedAtUtc DESC"; + } + else if (query.SortType == IncidentOrder.LatestReport) + { + if (query.SortAscending) + sqlQuery += " ORDER BY LastReportAtUtc"; + else + sqlQuery += " ORDER BY LastReportAtUtc DESC"; + } + else if (query.SortType == IncidentOrder.MostReports) + { + if (query.SortAscending) + sqlQuery += " ORDER BY ReportCount"; + else + sqlQuery += " ORDER BY ReportCount DESC"; + } + cmd.CommandText = string.Format(sqlQuery, + "Incidents.*, Applications.Id as ApplicationId, Applications.Name as ApplicationName"); + if (query.PageNumber > 0) + { + var offset = (query.PageNumber - 1) * query.ItemsPerPage; + cmd.CommandText += $@" OFFSET {offset} ROWS FETCH NEXT {query.ItemsPerPage} ROWS ONLY"; + } + var items = await cmd.ToListAsync(); + + return new FindIncidentsResult + { + Items = items.ToArray(), + PageNumber = query.PageNumber, + PageSize = query.ItemsPerPage, + TotalCount = (int)count + }; + } + } + + protected string AddContextProperty(DbCommand cmd, string sql, string sqlColumnName, string sqlParameterName, string value) + { + if (string.IsNullOrEmpty(value)) + return ""; + + if (value.Contains("*")) + { + sql += $" {sqlColumnName} LIKE @{sqlParameterName}"; + cmd.AddParameter(sqlParameterName, value.Replace("*", "%")); + } + else + { + sql += $" {sqlColumnName} = @{sqlParameterName}"; + cmd.AddParameter(sqlParameterName, value); + } + return sql + " AND "; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetCollectionHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetCollectionHandler.cs new file mode 100644 index 00000000..ac28c0e4 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetCollectionHandler.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Queries; +using DotNetCqs; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + internal class GetCollectionHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + private ILog _logger = LogManager.GetLogger(typeof(GetCollectionHandler)); + + public GetCollectionHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetCollection query) + { + if (query.MaxNumberOfCollections == 0) + query.MaxNumberOfCollections = 1; + + var sql = @"WITH ReportsWithCollection (ErrorReportId) + AS + ( + select distinct TOP(10) ErrorReports.Id + FROM ErrorReports + JOIN ErrorReportCollectionProperties ep ON (ep.ReportId = ErrorReports.Id) + WHERE ep.Name = @collectionName + AND ErrorReports.IncidentId=@incidentId + ) + + select erp.PropertyName, erp.Value, ErrorReports.CreatedAtUtc, ErrorReports.Id ReportId + from ErrorReportCollectionProperties erp + join ReportsWithCollection rc on (erp.ReportId = rc.ErrorReportId) + join ErrorReports on (ErrorReports.ID = rc.ErrorReportId) + WHERE erp.Name = @collectionName;"; + + var items = new List(); + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("incidentId", query.IncidentId); + cmd.AddParameter("collectionName", query.CollectionName); + using (var reader = await cmd.ExecuteReaderAsync()) + { + GetCollectionResultItem item = null; + var lastReportId = 0; + while (reader.Read()) + { + var reportId = (int)reader["ReportId"]; + if (reportId != lastReportId || item == null) + { + item = new GetCollectionResultItem + { + ReportId = (int)reader["ReportId"], + ReportDate = (DateTime)reader["CreatedAtUtc"], + Properties = new Dictionary() + }; + items.Add(item); + } + + lastReportId = reportId; + var key = (string)reader["PropertyName"]; + var value = (string)reader["Value"]; + if (item.Properties.ContainsKey(key)) + { + _logger.Info( + $"Report {reportId} have value for key {key} current: {item.Properties[key]} new: {value}."); + } + else + item.Properties.Add(key, value); + } + } + } + + return new GetCollectionResult + { + Items = items.ToArray() + }; + } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePage.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePage.cs new file mode 100644 index 00000000..15465e62 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePage.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + internal class GetIncidentForClosePageHandler : + IQueryHandler + { + private readonly IAdoNetUnitOfWork _uow; + + public GetIncidentForClosePageHandler(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public async Task HandleAsync(IMessageContext context, GetIncidentForClosePage query) + { + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = @"select Incidents.Description, +(select count(*) from IncidentFeedback WHERE IncidentFeedback.IncidentId = Incidents.Id AND IncidentFeedback.EmailAddress is not null AND IncidentFeedback.EmailAddress <> '') as SubscriberCount +FROM Incidents +WHERE Incidents.Id = @incidentId;"; + cmd.AddParameter("incidentId", query.IncidentId); + return await cmd.FirstAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs new file mode 100644 index 00000000..d9d98ce5 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs @@ -0,0 +1,9 @@ +using Coderr.Server.Api.Core.Incidents.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + public class GetIncidentForClosePageResultMapper : EntityMapper + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentHandler.cs new file mode 100644 index 00000000..e305700a --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentHandler.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Incidents; +using Coderr.Server.Api.Core.Incidents.Queries; +using DotNetCqs; +using Griffin.Data; +using Griffin.Data.Mapper; +using log4net; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + public class GetIncidentHandler : IQueryHandler + { + private readonly IEnumerable _quickfactProviders; + private readonly IEnumerable _highlightedContextDataProviders; + private readonly IEnumerable _solutionProviders; + private readonly IAdoNetUnitOfWork _unitOfWork; + private ILog _logger = LogManager.GetLogger(typeof(GetIncidentHandler)); + + public GetIncidentHandler(IAdoNetUnitOfWork unitOfWork, + IEnumerable quickfactProviders, + IEnumerable highlightedContextDataProviders, + IEnumerable solutionProviders + ) + { + _unitOfWork = unitOfWork; + _quickfactProviders = quickfactProviders; + _highlightedContextDataProviders = highlightedContextDataProviders; + _solutionProviders = solutionProviders; + } + + public async Task HandleAsync(IMessageContext context, GetIncident query) + { + _logger.Info("GetIncident step 1"); + var sql = + "SELECT Incidents.*, Users.Username as AssignedTo " + + " FROM Incidents WITH(READUNCOMMITTED)" + + " LEFT JOIN Users WITH(READUNCOMMITTED) ON (AssignedToId = Users.AccountId) " + + " WHERE Incidents.Id = @id;"; + + var result = await _unitOfWork.FirstAsync(sql, new { Id = query.IncidentId }); + _logger.Info("GetIncident step 2"); + + result.Tags = GetTags(query.IncidentId); + _logger.Info("GetIncident step 3"); + + var facts = new List + { + new QuickFact + { + Title = "Created", + Description = "When we received the first error report", + Value = result.CreatedAtUtc.ToShortDateString() + }, + new QuickFact + { + Title = "Last report", + Description = "When we received the most recent error report", + Value = result.LastReportReceivedAtUtc.ToShortDateString() + }, + new QuickFact + { + Title = "Report Count", + Description = "Number of reports since this incident was discovered", + Value = result.ReportCount.ToString() + } + }; + + var environments = GetEnvironments(query.IncidentId); + if (environments.Any()) + { + facts.Add(new QuickFact + { + Title = "Environments", + Value = string.Join(", ", environments) + }); + } + + _logger.Info("GetIncident step 4"); + await GetContextCollectionNames(result); + _logger.Info("GetIncident step 5"); + await GetReportStatistics(result); + _logger.Info("GetIncident step 6"); + await GetStatSummary(query, facts); + _logger.Info("GetIncident step 7"); + + var solutions = new List(); + var suggestedSolutionContext = new SolutionProviderContext(solutions) + { + ApplicationId = result.ApplicationId, + Description = result.Description, + FullName = result.FullName, + IncidentId = result.Id, + StackTrace = result.StackTrace, + Tags = result.Tags + }; + + var contextData = new List(); + var highlightedContext = new HighlightedContextDataProviderContext(contextData) + { + ApplicationId = result.ApplicationId, + Description = result.Description, + FullName = result.FullName, + IncidentId = result.Id, + StackTrace = result.StackTrace, + Tags = result.Tags + }; + var quickFactContext = new QuickFactContext(result.ApplicationId, query.IncidentId, facts); + foreach (var provider in _quickfactProviders) + { + await provider.CollectAsync(quickFactContext); + } + foreach (var provider in _highlightedContextDataProviders) + { + await provider.CollectAsync(highlightedContext); + } + foreach (var provider in _solutionProviders) + { + await provider.SuggestSolutionAsync(suggestedSolutionContext); + } + _logger.Info("GetIncident step 8"); + + result.RelatedIncidents = await GetRelatedIncidents(query.IncidentId); + result.Facts = facts.ToArray(); + result.SuggestedSolutions = solutions.ToArray(); + result.HighlightedContextData = contextData.ToArray(); + return result; + } + + //TODO : Do not mess with the similarity tables directly + private async Task GetContextCollectionNames(GetIncidentResult result) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"select distinct Name +from [IncidentContextCollections] WITH(READUNCOMMITTED) +where IncidentId=@incidentId"; + cmd.AddParameter("incidentId", result.Id); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var names = new List(); + while (await reader.ReadAsync()) + { + names.Add(reader.GetString(0)); + } + result.ContextCollections = names.ToArray(); + } + } + } + + private async Task GetRelatedIncidents(int incidentId) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"with cte (IncidentId) + AS + ( + select distinct ic1.IncidentId + FROM IncidentCorrelations ic1 + JOIN IncidentCorrelations ic2 ON (ic1.CorrelationId = ic2.CorrelationId) + WHERE ic2.IncidentId = @id and ic1.IncidentId <> @id + ) + + select i.Id IncidentId, i.Description Title, i.CreatedAtUtc, i.ApplicationId, a.Name ApplicationName + FROM Incidents i + JOIN Applications a ON (a.Id = i.ApplicationId) + JOIN CTE ON (IncidentId = i.Id)"; + cmd.AddParameter("id", incidentId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var result = new List(); + while (await reader.ReadAsync()) + { + var item = new RelatedIncident + { + IncidentId = reader.GetInt32(0), + Title = reader.GetString(1), + CreatedAtUtc = reader.GetDateTime(2), + ApplicationId = reader.GetInt32(3), + ApplicationName = reader.GetString(4) + }; + result.Add(item); + } + + return result.ToArray(); + } + } + } + + private async Task GetReportStatistics(GetIncidentResult result) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"select cast(ReceivedAtUtc as date) as Date, count(*) as Count +from IncidentReports WITH(READUNCOMMITTED) +where incidentid=@incidentId +AND ReceivedAtUtc > @date +group by cast(ReceivedAtUtc as date)"; + var startDate = DateTime.Today.AddDays(-29); + cmd.AddParameter("date", startDate); + cmd.AddParameter("incidentId", result.Id); + var specifiedDays = await cmd.ToListAsync(); + var curDate = startDate; + var values = new ReportDay[30]; + var valuesIndexer = 0; + var specifiedDaysIndexer = 0; + while (curDate <= DateTime.Today) + { + if (specifiedDays.Count > specifiedDaysIndexer && + specifiedDays[specifiedDaysIndexer].Date == curDate) + values[valuesIndexer++] = specifiedDays[specifiedDaysIndexer++]; + else + values[valuesIndexer++] = new ReportDay { Date = curDate }; + curDate = curDate.AddDays(1); + } + result.DayStatistics = values; + } + } + + private async Task GetStatSummary(GetIncident query, ICollection facts) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @" +select count(distinct emailaddress) +from IncidentFeedback +where @minDate < CreatedAtUtc +AND emailaddress is not null +AND DATALENGTH(emailaddress) > 0 +AND IncidentId = @incidentId; + +select count(*) +from IncidentFeedback +where @minDate < CreatedAtUtc +AND Description is not null +AND DATALENGTH(Description) > 0 +AND IncidentId = @incidentId;"; + cmd.AddParameter("incidentId", query.IncidentId); + cmd.AddParameter("minDate", DateTime.Today.AddDays(-90)); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (!await reader.ReadAsync()) + throw new InvalidOperationException("Expected to be able to read result 1."); + + facts.Add(new QuickFact + { + Title = "Subscribed users", + Description = + "Number of users that are waiting on a notification when the incident have been solved.", + Value = reader.GetInt32(0).ToString() + }); + + await reader.NextResultAsync(); + if (!await reader.ReadAsync()) + throw new InvalidOperationException("Expected to be able to read result 2."); + + facts.Add(new QuickFact + { + Title = "Bug reports", + Description = "Number of bug reports written by users.", + Value = reader.GetInt32(0).ToString() + }); + } + } + } + + private string[] GetTags(int incidentId) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"Declare @Tags AS Nvarchar(MAX); + SELECT @Tags = COALESCE(@Tags + ';', '') + TagName + FROM IncidentTags WITH(READUNCOMMITTED) + WHERE IncidentId=@id + ORDER BY OrderNumber, TagName + SELECT @Tags"; + cmd.AddParameter("id", incidentId); + using (var reader = cmd.ExecuteReader()) + { + if (!reader.Read()) + return new string[0]; + + var value = reader[0]; + return value is DBNull + ? new string[0] + : ((string)value).Split(';'); + } + } + } + private string[] GetEnvironments(int incidentId) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"Declare @Names AS Nvarchar(MAX); + SELECT @Names = COALESCE(@Names + ';', '') + Name + FROM IncidentEnvironments ie WITH(READUNCOMMITTED) + JOIN Environments ae WITH(READUNCOMMITTED) ON (ae.Id = ie.EnvironmentId) + WHERE IncidentId=@id + ORDER BY Name + SELECT @Names"; + cmd.AddParameter("id", incidentId); + using (var reader = cmd.ExecuteReader()) + { + if (!reader.Read()) + return new string[0]; + + var value = reader[0]; + return value is DBNull + ? new string[0] + : ((string)value).Split(';'); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentResultMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentResultMapper.cs new file mode 100644 index 00000000..3dea2573 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentResultMapper.cs @@ -0,0 +1,43 @@ +using System; +using Coderr.Server.Api.Core.Incidents.Queries; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + public class GetIncidentResultMapper : CrudEntityMapper + { + public GetIncidentResultMapper() + : base("Incidents") + { + Property(x => x.DayStatistics).Ignore(); + Property(x => x.SuggestedSolutions).Ignore(); + Property(x => x.Facts).Ignore(); + Property(x => x.HighlightedContextData).Ignore(); + Property(x => x.Tags).Ignore(); + Property(x => x.ContextCollections).Ignore(); + Property(x => x.IncidentState) + .ColumnName("State"); + Property(x => x.AssignedToId) + .ToPropertyValue(x => x is DBNull ? (int?)null : (int)x); + + Property(x => x.Solution) + .ToPropertyValue(x => EntitySerializer.Deserialize(x)?.Description); + + Property(x => x.LastReportReceivedAtUtc) + .ToPropertyValue(DbConverters.ToEntityDate) + .ColumnName("LastReportAtUtc"); + + Property(x => x.IsSolved).Ignore(); + Property(x => x.IsIgnored).Ignore(); + + Property(x => x.IsSolutionShared) + .ToPropertyValue(DbConverters.BoolFromByteArray); + + Property(x => x.RelatedIncidents) + .Ignore(); + } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs new file mode 100644 index 00000000..dfa26c80 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + internal class GetIncidentStatisticsHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetIncidentStatisticsHandler(IAdoNetUnitOfWork unitOfWork) + { + if (unitOfWork == null) throw new ArgumentNullException("unitOfWork"); + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetIncidentStatistics query) + { + if (query.NumberOfDays == 1) + return await GetTodaysOverviewAsync(query); + + var result = new GetIncidentStatisticsResult + { + Labels = new string[query.NumberOfDays], + Values = new int[query.NumberOfDays] + }; + + var startDate = DateTime.Today.AddDays(-query.NumberOfDays + 1); + for (var i = 0; i < query.NumberOfDays; i++) + { + result.Values[i] = 0; + result.Labels[i] = startDate.AddDays(i).ToShortDateString(); + } + + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"select cast(ReceivedAtUtc as date) as Date, count(*) as Count +from IncidentReports +where incidentid=@id +AND ReceivedAtUtc > @date +group by cast(ReceivedAtUtc as date);"; + cmd.AddParameter("id", query.IncidentId); + cmd.AddParameter("date", DateTime.Today.AddDays(0 - query.NumberOfDays)); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var index = reader.GetDateTime(0).Subtract(startDate).Days; + result.Values[index] = reader.GetInt32(1); + } + } + } + + return result; + } + + private async Task GetTodaysOverviewAsync(GetIncidentStatistics query) + { + var result = new GetIncidentStatisticsResult + { + Labels = new string[24], + Values = new int[24] + }; + var values = new Dictionary(); + var startDate = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23); + for (var i = 0; i < 24; i++) + { + result.Values[i] = 0; + result.Labels[i] = startDate.AddHours(i).ToString("HH:mm"); + values[startDate.AddHours(i)] = 0; + } + + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"SELECT DATEPART(HOUR, IncidentReports.ReceivedAtUtc), cast(count(Id) as int) +FROM IncidentReports +WHERE IncidentReports.ReceivedAtUtc > @minDate +AND IncidentId = @incidentId +GROUP BY DATEPART(HOUR, IncidentReports.ReceivedAtUtc);"; + + + cmd.AddParameter("incidentId", query.IncidentId); + cmd.AddParameter("minDate", startDate); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var todayWithHour = DateTime.Today.AddHours(DateTime.Now.Hour); + var hour = reader.GetInt32(0); + var date = hour < todayWithHour.Hour + ? DateTime.Today.AddHours(hour) + : DateTime.Today.AddDays(-1).AddHours(hour); + values[date] = reader.GetInt32(1); + } + } + } + + result.Values = values.Values.ToArray(); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListHandler.cs new file mode 100644 index 00000000..25fe3309 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListHandler.cs @@ -0,0 +1,52 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Reports.Queries; +using Coderr.Server.PostgreSQL.Tools; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + internal class GetReportListHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetReportListHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetReportList query) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + var totalCount = 0; + cmd.AddParameter("incidentId", query.IncidentId); + if (query.PageNumber > 0) + { + cmd.CommandText = "SELECT cast(count(Id) as int) FROM ErrorReports WHERE IncidentId = @incidentId;"; + totalCount = (int) cmd.ExecuteScalar(); + + cmd.CommandText = + "SELECT Id, Title, CreatedAtUtc, RemoteAddress, Exception FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY Id DESC;"; + + cmd.Paging(query.PageNumber, query.PageSize); + } + else + { + cmd.CommandText = + "SELECT Id, Title, CreatedAtUtc, RemoteAddress, Exception FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY Id DESC;"; + } + var items = await cmd.ToListAsync(); + return new GetReportListResult(items.ToArray()) + { + PageNumber = query.PageNumber, + PageSize = query.PageSize, + TotalCount = totalCount + }; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListResultItemMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListResultItemMapper.cs new file mode 100644 index 00000000..81e0f59c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/Queries/GetReportListResultItemMapper.cs @@ -0,0 +1,13 @@ +using Coderr.Server.Api.Core.Reports.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents.Queries +{ + public class GetReportListResultItemMapper : EntityMapper + { + public GetReportListResultItemMapper() + { + Property(x => x.Message).ColumnName("Title"); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/ReportDayMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/ReportDayMapper.cs new file mode 100644 index 00000000..353806be --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Incidents/ReportDayMapper.cs @@ -0,0 +1,9 @@ +using Coderr.Server.Api.Core.Incidents.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Incidents +{ + internal class ReportDayMapper : EntityMapper + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/GetInvitationByKeyHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/GetInvitationByKeyHandler.cs new file mode 100644 index 00000000..aeba79c2 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/GetInvitationByKeyHandler.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Invitations.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Core.Invitations +{ + internal class GetInvitationByKeyHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetInvitationByKeyHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetInvitationByKey query) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = "SELECT email FROM Invitations WHERE InvitationKey = @id;"; + cmd.AddParameter("id", query.InvitationKey); + return new GetInvitationByKeyResult {EmailAddress = (string) await cmd.ExecuteScalarAsync()}; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationMapping.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationMapping.cs new file mode 100644 index 00000000..b6171513 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationMapping.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Coderr.Server.App.Core.Invitations; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Invitations +{ + public class InvitationMapping : CrudEntityMapper + { + public InvitationMapping() + : base("Invitations") + { + Property(x => x.Id).PrimaryKey(true); + Property(x => x.EmailToInvitedUser).ColumnName("Email"); + Property(x => x.Invitations) + .ToColumnValue(EntitySerializer.Serialize) + .ToPropertyValue(x => EntitySerializer.Deserialize>((string) x)); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationRepository.cs new file mode 100644 index 00000000..aacac607 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Invitations/InvitationRepository.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.App.Core.Invitations; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Invitations +{ + [ContainerService] + internal class InvitationRepository : IInvitationRepository + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public InvitationRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task DeleteAsync(string invitationKey) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = "DELETE FROM Invitations WHERE InvitationKey = @key;"; + cmd.AddParameter("key", invitationKey); + await cmd.ExecuteNonQueryAsync(); + } + } + + + public async Task FindByEmailAsync(string email) + { + return await _unitOfWork.FirstOrDefaultAsync(new {EmailToInvitedUser = email}); + } + + public async Task CreateAsync(Invitation invitation) + { + await _unitOfWork.InsertAsync(invitation); + } + + public async Task UpdateAsync(Invitation invitation) + { + await _unitOfWork.UpdateAsync(invitation); + } + + public async Task GetByInvitationKeyAsync(string invitationKey) + { + return await _unitOfWork.FirstOrDefaultAsync(new {InvitationKey = invitationKey}); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Notifications/NotificationRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Notifications/NotificationRepository.cs new file mode 100644 index 00000000..323539d3 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Notifications/NotificationRepository.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.App.Core.Notifications; +using Coderr.Server.Domain.Modules.UserNotifications; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Notifications +{ + [ContainerService] + public class NotificationRepository : INotificationsRepository, IUserNotificationsRepository + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public NotificationRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task TryGetAsync(int accountId, int applicationId) + { + if (applicationId == 0) + applicationId = -1; + return await _unitOfWork.FirstOrDefaultAsync( + new + { + AccountId = accountId, + ApplicationId = applicationId + }); + } + + public async Task UpdateAsync(UserNotificationSettings notificationSettings) + { + if (notificationSettings.ApplicationId == 0) + notificationSettings.ApplicationId = -1; + await _unitOfWork.UpdateAsync(notificationSettings); + } + + public async Task CreateAsync(UserNotificationSettings notificationSettings) + { + if (notificationSettings.ApplicationId == 0) + notificationSettings.ApplicationId = -1; + await _unitOfWork.InsertAsync(notificationSettings); + } + + public async Task ExistsAsync(int accountId, int applicationId) + { + if (applicationId == 0) + applicationId = -1; + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "SELECT TOP 1 AccountId FROM UserNotificationSettings WHERE AccountId = @id AND ApplicationId = @appId;"; + cmd.AddParameter("id", accountId); + cmd.AddParameter("appId", applicationId); + return await cmd.ExecuteScalarAsync() != null; + } + } + + public async Task> GetAllAsync(int applicationId) + { + var sql = + @"SELECT * + FROM UserNotificationSettings + WHERE ApplicationId = @1 + OR ApplicationId = -1 + ORDER By AccountId, ApplicationId DESC;"; + var settings = await _unitOfWork.ToListAsync(sql, applicationId); + var dict = new Dictionary(); + foreach (var setting in settings) + { + if (dict.ContainsKey(setting.AccountId)) + continue; + dict[setting.AccountId] = setting; + } + + return dict.Values.ToList(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Notifications/UserNotificationSettingsMap.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Notifications/UserNotificationSettingsMap.cs new file mode 100644 index 00000000..11dce76c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Notifications/UserNotificationSettingsMap.cs @@ -0,0 +1,54 @@ +using System; +using Coderr.Server.Domain.Modules.UserNotifications; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Notifications +{ + internal class UserNotificationSettingsMap : CrudEntityMapper + { + public UserNotificationSettingsMap() + : base("UserNotificationSettings") + { + Property(x => x.AccountId).ColumnName("AccountId").PrimaryKey(); + Property(x => x.ApplicationId).PrimaryKey(); + Property(x => x.NewIncident) + .ToColumnValue(StringToEnum) + .ToPropertyValue(EnumToString); + Property(x => x.ReopenedIncident) + .ToColumnValue(StringToEnum) + .ToPropertyValue(EnumToString); + Property(x => x.NewReport) + .ToColumnValue(StringToEnum) + .ToPropertyValue(EnumToString); + Property(x => x.UserFeedback) + .ToColumnValue(StringToEnum) + .ToPropertyValue(EnumToString); + Property(x => x.ApplicationSpike) + .ToColumnValue(StringToEnum) + .ToPropertyValue(EnumToString); + Property(x => x.WeeklySummary) + .ToColumnValue(StringToEnum) + .ToPropertyValue(EnumToString); + } + + public static object StringToEnum(TEnum notificationState) + { + return notificationState.ToString(); + } + + private TEnum EnumToString(object arg) where TEnum : struct + { + if (arg is DBNull) + return default(TEnum); + + TEnum en; + if (!Enum.TryParse(arg.ToString(), true, out en)) + { + throw new MappingException(typeof(UserNotificationSettings), + "Failed to convert '" + arg + "' to '" + typeof(TEnum).FullName + "'."); + } + + return en; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportDtoMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportDtoMapper.cs new file mode 100644 index 00000000..b564795c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportDtoMapper.cs @@ -0,0 +1,28 @@ +using Coderr.Server.Api.Core.Reports; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Reports +{ + public class ErrorReportDtoMapper : CrudEntityMapper + { + public ErrorReportDtoMapper() + : base("ErrorReports") + { + //Property(x => x.ContextCollections) + // .ColumnName("ContextInfo") + // .ToColumnValue(EntitySerializer.Serialize) + // .ToPropertyValue(colValue => EntitySerializer.Deserialize((string) colValue)); + Property(x => x.ContextCollections).Ignore(); + + Property(x => x.ReportVersion).Ignore(); + + Property(x => x.Exception) + .ToPropertyValue(EntitySerializer.Deserialize) + .ToColumnValue(EntitySerializer.Serialize); + + Property(x => x.ReportId) + .ColumnName("ErrorId"); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportRepository.cs new file mode 100644 index 00000000..086caaec --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ErrorReportRepository.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Api.Core.Reports; +using Coderr.Server.Domain.Core.ErrorReports; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Reports +{ + [ContainerService] + internal class ErrorReportRepository : IReportsRepository + { + private readonly IAdoNetUnitOfWork _uow; + + public ErrorReportRepository(IAdoNetUnitOfWork uow) + { + _uow = uow ?? throw new ArgumentNullException(nameof(uow)); + } + + public async Task GetAsync(int id) + { + ErrorReportEntity report; + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + "SELECT * FROM ErrorReports WHERE Id = @id;"; + + cmd.AddParameter("id", id); + report = await cmd.FirstAsync(); + } + + var collections = new List(); + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + "SELECT Name, PropertyName, Value FROM ErrorReportCollectionProperties WHERE ReportId = @id ORDER BY Name;"; + + cmd.AddParameter("id", id); + using (var reader = await cmd.ExecuteReaderAsync()) + { + string previousCollectionName = null; + var properties = new Dictionary(); + string currentCollectionName = null; + while (await reader.ReadAsync()) + { + currentCollectionName = reader.GetString(0); + + // We always want to add the context when the last propery have been found + // so that all props are included. + if (previousCollectionName == null) + previousCollectionName = currentCollectionName; + + if (previousCollectionName != currentCollectionName) + { + var collection = new ErrorReportContextCollection(previousCollectionName ?? currentCollectionName, properties); + collections.Add(collection); + properties = new Dictionary(); + previousCollectionName = currentCollectionName; + report.Add(collection); + } + + properties[reader.GetString(1)] = reader.GetString(2); + } + + // When the last property is in a new collection + if (currentCollectionName != null && collections.All(x => x.Name != currentCollectionName)) + { + var collection = new ErrorReportContextCollection(previousCollectionName, properties); + collections.Add(collection); + report.Add(collection); + } + + } + } + + return report; + } + + public async Task FindByErrorIdAsync(string errorId) + { + using (var cmd = (DbCommand)_uow.CreateCommand()) + { + cmd.CommandText = + @"SELECT ir.Id, ir.IncidentId, i.ApplicationId, ir.ReceivedAtUtc, ir.ErrorId + FROM IncidentReports ir + JOIN Incidents i ON (i.Id = ir.IncidentId) + WHERE ErrorId = @id;"; + + cmd.AddParameter("id", errorId); + return await cmd.FirstOrDefaultAsync(); + } + } + + //public async Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize) + //{ + // using (var cmd = (DbCommand)_uow.CreateCommand()) + // { + // cmd.AddParameter("incidentId", incidentId); + // long totalRows = 0; + // if (pageNumber > 0) + // { + // cmd.CommandText = + // "SELECT count(*) FROM ErrorReports WHERE IncidentId = @incidentId"; + // totalRows = (int)await cmd.ExecuteScalarAsync(); + // } + + // cmd.CommandText = + // "SELECT * FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY CreatedAtUtc DESC"; + // if (pageNumber > 0) + // { + // var offset = (pageNumber - 1) * pageSize; + // cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize); + // } + + // //cmd.AddParameter("incidentId", incidentId); + // var list = await cmd.ToListAsync(); + // return new PagedReports + // { + // TotalCount = (int)totalRows, + // Reports = (IReadOnlyList)list + // }; + // } + //} + + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public IEnumerable GetAll(int[] ids) + { + //TODO: Remove SQL injection vulnerability + + using (var cmd = _uow.CreateCommand()) + { + var idString = string.Join(",", ids.Select(x => "'" + x + "'")); + + cmd.CommandText = + string.Format( + "SELECT * FROM ErrorReports WHERE Id IN ({0});", + idString); + + return cmd.ToList().ToList(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ReportMappingMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ReportMappingMapper.cs new file mode 100644 index 00000000..bba29d4f --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Reports/ReportMappingMapper.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Coderr.Server.Domain.Core.ErrorReports; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Reports +{ + class ReportMappingMapper : CrudEntityMapper + { + public ReportMappingMapper() : base("IncidentReports") + { + Property(x => x.Id) + .PrimaryKey(true); + Property(x => x.ApplicationId) + .Ignore(); + } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Users/ApplicationTeamMemberMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Users/ApplicationTeamMemberMapper.cs new file mode 100644 index 00000000..9bc0f5c6 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Users/ApplicationTeamMemberMapper.cs @@ -0,0 +1,26 @@ +using System; +using Coderr.Server.Domain.Core.Applications; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Users +{ + public class ApplicationTeamMemberMapper : CrudEntityMapper + { + public ApplicationTeamMemberMapper() : base("ApplicationMembers") + { + Property(x => x.Id) + .PrimaryKey(true); + + Property(x => x.AccountId) + .ToColumnValue(x => x == 0 ? (object)DBNull.Value : x) + .ToPropertyValue(x => x is DBNull ? 0 : (int)x); + + Property(x => x.UserName) + .NotForCrud(); + + Property(x => x.Roles) + .ToColumnValue(x => string.Join(",", x)) + .ToPropertyValue(x => ((string)x).Split(',')); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Users/UserMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Users/UserMapper.cs new file mode 100644 index 00000000..765999bd --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Users/UserMapper.cs @@ -0,0 +1,13 @@ +using Coderr.Server.Domain.Core.User; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Users +{ + public class UserMapper : CrudEntityMapper + { + public UserMapper() : base("Users") + { + Property(x => x.AccountId).PrimaryKey(false); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Core/Users/UserRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Core/Users/UserRepository.cs new file mode 100644 index 00000000..3c3de2b7 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Core/Users/UserRepository.cs @@ -0,0 +1,48 @@ +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.User; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Core.Users +{ + [ContainerService] + public class UserRepository : IUserRepository + { + private readonly IAdoNetUnitOfWork _uow; + + public UserRepository(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public async Task CreateAsync(User user) + { + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = "INSERT INTO Users (AccountId, UserName, EmailAddress) VALUES(@id, @userName, @email);"; + cmd.AddParameter("id", user.AccountId); + cmd.AddParameter("userName", user.UserName); + cmd.AddParameter("email", user.EmailAddress); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task GetUserAsync(int accountId) + { + return await _uow.FirstAsync(new {AccountId = accountId}); + } + + public async Task UpdateAsync(User user) + { + await _uow.UpdateAsync(user); + } + + public async Task FindByEmailAsync(string emailAddress) + { + return await _uow.FirstOrDefaultAsync("EmailAddress = @1", emailAddress); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/DateTimeExtensions.cs b/src/Server/Coderr.Server.PostgreSQL/DateTimeExtensions.cs new file mode 100644 index 00000000..dca34d1f --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/DateTimeExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Coderr.Server.PostgreSQL +{ + public static class DateTimeExtensions + { + public static object ToDbNullable(this DateTime value) + { + return value == DateTime.MinValue ? (object) DBNull.Value : value; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/DbConverters.cs b/src/Server/Coderr.Server.PostgreSQL/DbConverters.cs new file mode 100644 index 00000000..926e88e0 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/DbConverters.cs @@ -0,0 +1,47 @@ +using System; +using Coderr.Server.PostgreSQL.Tools; + +namespace Coderr.Server.PostgreSQL +{ + public class DbConverters + { + public static bool BoolFromByteArray(object arg) + { + return Convert.ToBoolean(((byte[]) arg)[0]); + } + + public static TProperty DeserializeEntity(object arg) + { + return EntitySerializer.Deserialize((string) arg); + } + + public static object SerializeEntity(T arg) + { + return EntitySerializer.Serialize(arg); + } + + + public static DateTime ToEntityDate(object columnValue) + { + if (columnValue is DBNull) + return DateTime.MinValue; + + return (DateTime) columnValue; + } + + public static object ToNullableSqlDate(DateTime arg) + { + if (arg == DateTime.MinValue) + return DBNull.Value; + + return arg; + } + + public static object ToSqlNull(DateTime arg) + { + if (arg == DateTime.MinValue) + return DBNull.Value; + return arg; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs b/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs new file mode 100644 index 00000000..72c6a742 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs @@ -0,0 +1,181 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Reflection; +using Griffin.Data; +using Griffin.Data.Mapper; +using log4net; + +namespace Coderr.Server.PostgreSQL.Migrations +{ + public class MigrationRunner + { + private readonly Func _connectionFactory; + private readonly string _migrationName; + private readonly string _scriptNamespace; + private readonly MigrationScripts _scripts; + private ILog _logger = LogManager.GetLogger(typeof(MigrationRunner)); + private Assembly _scriptAssembly; + + public MigrationRunner(Func connectionFactory, string migrationName, string scriptNamespace) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName)); + _scriptNamespace = scriptNamespace; + _scriptAssembly = GetScriptAssembly(migrationName); + _scripts = new MigrationScripts(migrationName, _scriptAssembly); + } + + private Assembly GetScriptAssembly(string migrationName) + { + if (migrationName == null) throw new ArgumentNullException(nameof(migrationName)); + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.IsDynamic) + continue; + if (assembly.GetName().Name?.StartsWith("Coderr", StringComparison.OrdinalIgnoreCase) != true) + continue; + + var isFound = assembly + .GetManifestResourceNames() + .Any(x => x.StartsWith(_scriptNamespace) && x.Contains($"{_migrationName}.v")); + if (isFound) + return assembly; + } + throw new InvalidOperationException($"Failed to find scripts for migration '{migrationName}'."); + } + + public void Run() + { + EnsureLoaded(); + if (!CanSchemaBeUpgraded()) + { + _logger.Debug("Db Schema is up to date."); + return; + } + + _logger.Info("Updating DB schema."); + UpgradeDatabaseSchema(); + } + + private void EnsureLoaded() + { + if (_scripts.IsEmpty) + LoadScripts(); + } + + /// + /// Check if the current DB schema is out of date compared to the embedded schema resources. + /// + protected bool CanSchemaBeUpgraded() + { + var version = GetCurrentSchemaVersion(); + var embeddedSchema = GetLatestSchemaVersion(); + return embeddedSchema > version; + } + + protected void LoadScripts() + { + var names = + _scriptAssembly + .GetManifestResourceNames() + .Where(x => x.StartsWith(_scriptNamespace) && x.Contains($"{_migrationName}.v")); + + foreach (var name in names) + { + var pos = name.IndexOf(".v") + 2; //2 extra for ".v" + var endPos = name.IndexOf(".", pos); + var versionStr = name.Substring(pos, endPos - pos); + var version = int.Parse(versionStr); + _scripts.AddScriptName(version, name); + } + } + + public int GetCurrentSchemaVersion() + { + string[] scripts = new[] + { + @"IF OBJECT_ID (N'DatabaseSchema', N'U') IS NULL + BEGIN + CREATE TABLE [dbo].DatabaseSchema ( + [Version] int not null default 1, + [Name] varchar(50) NOT NULL + ); + END", + + @"IF COL_LENGTH('DatabaseSchema', 'Name') IS NULL + BEGIN + ALTER TABLE DatabaseSchema ADD [Name] varchar(50) NULL; + END;", + @"UPDATE DatabaseSchema SET Name = 'coderr' WHERE Name IS NULL" + }; + + using (var con = _connectionFactory()) + { + foreach (var script in scripts) + { + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = script; + cmd.ExecuteNonQuery(); + } + } + + using (var cmd = con.CreateCommand()) + { + try + { + cmd.CommandText = "SELECT Version FROM DatabaseSchema WHERE Name = @name;"; + cmd.AddParameter("name", _migrationName); + var result = cmd.ExecuteScalar(); + if (result is null) + return -1; + return (int)result; + } + catch (SqlException ex) + { + //invalid object name + if (ex.Number == 208) + return -1; + + throw; + } + } + } + } + + public int GetLatestSchemaVersion() + { + EnsureLoaded(); + return _scripts.GetHighestVersion(); + } + + /// + /// Upgrade schema + /// + /// -1 = latest version + protected void UpgradeDatabaseSchema(int toVersion = -1) + { + if (toVersion == -1) + toVersion = GetLatestSchemaVersion(); + var currentSchema = GetCurrentSchemaVersion(); + if (currentSchema < 1) + currentSchema = 0; + + for (var version = currentSchema + 1; version <= toVersion; version++) + { + _logger.Info("Migrating to v" + version); + using (var con = _connectionFactory()) + { + _scripts.Execute(con, version); + if (version == 1) + { + con.ExecuteNonQuery($"INSERT INTO DatabaseSchema (Name, Version) VALUES('{_migrationName}', 1);"); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationScripts.cs b/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationScripts.cs new file mode 100644 index 00000000..8e6a8335 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationScripts.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Coderr.Server.PostgreSQL.Migrations +{ + public class MigrationScripts + { + private readonly string _migrationName; + private readonly Dictionary _versions = new Dictionary(); + private Assembly _scriptAssembly; + public MigrationScripts(string migrationName, Assembly scriptAssembly) + { + _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName)); + _scriptAssembly = scriptAssembly ?? throw new ArgumentNullException(nameof(scriptAssembly)); + } + + public MigrationScripts(string migrationName) + { + _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName)); + } + + public bool IsEmpty => _versions.Count == 0; + + public void AddScriptName(int version, string scriptName) + { + if (version < 0) throw new ArgumentOutOfRangeException(nameof(version)); + + _versions[version] = scriptName ?? throw new ArgumentNullException(nameof(scriptName)); + } + + public string LoadScript(int version) + { + if (version <= 0) throw new ArgumentOutOfRangeException(nameof(version)); + + var scriptName = _versions[version]; + var res = _scriptAssembly.GetManifestResourceStream(scriptName); + if (res == null) + throw new InvalidOperationException("Failed to find schema " + scriptName); + + return new StreamReader(res).ReadToEnd(); + } + + public void Execute(IDbConnection connection, int version) + { + var script = LoadScript(version); + var sb = new StringBuilder(); + var sr = new StringReader(script); + while (true) + { + var line = sr.ReadLine(); + if (line == null) + break; + + if (!line.Equals("go")) + { + sb.AppendLine(line); + continue; + } + + ExecuteSql(connection, sb.ToString()); + sb.Clear(); + + } + + //do the remaining part of the script (or everything if GO was not used). + ExecuteSql(connection, sb.ToString()); + + ExecuteSql(connection, $"UPDATE DatabaseSchema SET Version={version} WHERE Name = '{_migrationName}';"); + } + + private static void ExecuteSql(IDbConnection connection, string sql) + { + using (var transaction = connection.BeginTransaction()) + { + using (var cmd = connection.CreateCommand()) + { + cmd.Transaction = transaction; + cmd.CommandText = sql; + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + + } + + public int GetHighestVersion() + { + if (_versions.Count == 0) + return -1; + return _versions.Max(x => x.Key); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOrginListItem.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOrginListItem.cs new file mode 100644 index 00000000..072346fc --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOrginListItem.cs @@ -0,0 +1,23 @@ +namespace Coderr.Server.PostgreSQL.Modules.Geolocation +{ + /// + /// List result item for repository queries + /// + public class ErrorOrginListItem + { + /// + /// Latitude + /// + public double Latitude { get; set; } + + /// + /// Longitude + /// + public double Longitude { get; set; } + + /// + /// Number of error reports that have been received from this incident + /// + public int NumberOfErrorReports { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOriginRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOriginRepository.cs new file mode 100644 index 00000000..d7d854a4 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/ErrorOriginRepository.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Modules.ErrorOrigins; +using Coderr.Server.ReportAnalyzer.ErrorOrigins; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.Geolocation +{ + [ContainerService] + public class ErrorOriginRepository : IErrorOriginRepository + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public ErrorOriginRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task CreateAsync(ErrorOrigin entity, int applicationId, int incidentId, int reportId) + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (reportId <= 0) throw new ArgumentOutOfRangeException(nameof(reportId)); + + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = "SELECT Id FROM ErrorOrigins WHERE IpAddress = @ip;"; + cmd.AddParameter("ip", entity.IpAddress); + var id = await cmd.ExecuteScalarAsync(); + if (id is int) + { + await CreateReportInfoAsync((int) id, applicationId, incidentId, reportId); + return; + } + } + + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = "INSERT INTO ErrorOrigins (IpAddress, CountryCode, CountryName, RegionCode, RegionName, City, ZipCode, Latitude, Longitude, CreatedAtUtc) " + + + "VALUES (@IpAddress, @CountryCode, @CountryName, @RegionCode, @RegionName, @City, @ZipCode, @Latitude, @Longitude, @CreatedAtUtc);RETURNING Id;"; + cmd.AddParameter("IpAddress", entity.IpAddress); + cmd.AddParameter("CountryCode", entity.CountryCode); + cmd.AddParameter("CountryName", entity.CountryName); + cmd.AddParameter("RegionCode", entity.RegionCode); + cmd.AddParameter("RegionName", entity.RegionName); + cmd.AddParameter("City", entity.City); + cmd.AddParameter("ZipCode", entity.ZipCode); + //cmd.AddParameter("Point", SqlGeography.Point(command.Latitude, command.Longitude, 4326)); + cmd.AddParameter("Latitude", entity.Latitude); + cmd.AddParameter("Longitude", entity.Longitude); + cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); + var id = (int) await cmd.ExecuteScalarAsync(); + await CreateReportInfoAsync(id, applicationId, incidentId, reportId); + } + } + + public async Task> FindForIncidentAsync(int incidentId) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"SELECT Longitude, Latitude, count(*) + FROM ErrorOrigins eo + JOIN ErrorReportOrigins ON (eo.Id = ErrorReportOrigins.ErrorOriginId) + WHERE IncidentId = @id + GROUP BY IncidentId, Longitude, Latitude;"; + cmd.AddParameter("id", incidentId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var items = new List(); + while (await reader.ReadAsync()) + { + var item = new ErrorOrginListItem + { + Longitude = (double) reader.GetDecimal(0), + Latitude = (double) reader.GetDecimal(1), + NumberOfErrorReports = reader.GetInt32(2) + }; + items.Add(item); + } + return items; + } + } + } + + private async Task CreateReportInfoAsync(int originId, int applicationId, int incidentId, int reportId) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "INSERT INTO ErrorReportOrigins (ErrorOriginId, ApplicationId, IncidentId, ReportId, CreatedAtUtc) VALUES(@oid, @aid, @iid, @rid, GetUtcDate());"; + cmd.AddParameter("oid", originId); + cmd.AddParameter("aid", applicationId); + cmd.AddParameter("iid", incidentId); + cmd.AddParameter("rid", reportId); + await cmd.ExecuteNonQueryAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/GetOriginsForIncidentHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/GetOriginsForIncidentHandler.cs new file mode 100644 index 00000000..3c1fd8a5 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Geolocation/GetOriginsForIncidentHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.ErrorOrigins.Queries; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.Geolocation +{ + /// + /// Handler for . + /// + public class GetOriginsForIncidentHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + + /// + /// Creates a new instance of . + /// + /// repository + public GetOriginsForIncidentHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + /// + /// Method used to execute the query + /// + /// Query to execute. + /// + /// Task which will contain the result once completed. + /// + public async Task HandleAsync(IMessageContext context, GetOriginsForIncident query) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"SELECT Longitude, Latitude, count(*) + FROM ErrorOrigins eo + JOIN ErrorReportOrigins ON (eo.Id = ErrorReportOrigins.ErrorOriginId) + WHERE IncidentId = @id + GROUP BY IncidentId, Longitude, Latitude;"; + cmd.AddParameter("id", query.IncidentId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var items = new List(); + while (await reader.ReadAsync()) + { + var item = new GetOriginsForIncidentResultItem + { + Longitude = (double) reader.GetDecimal(0), + Latitude = (double) reader.GetDecimal(1), + NumberOfErrorReports = reader.GetInt32(2) + }; + items.Add(item); + } + + return new GetOriginsForIncidentResult {Items = items.ToArray()}; + ; + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/History/GetIncidentsForStatesHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/History/GetIncidentsForStatesHandler.cs new file mode 100644 index 00000000..d447605c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/History/GetIncidentsForStatesHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.History.Queries; +using Coderr.Server.Domain.Core.Incidents; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.History +{ + internal class GetIncidentsForStatesHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetIncidentsForStatesHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetIncidentsForStates query) + { + var sql = + @"select i.Id, i.Description, i.FullName, i.CreatedAtUtc, i.AssignedAtUtc, i.SolvedAtUtc, IncidentHistory.State HistoryState + from incidents i + join IncidentHistory on (i.Id= IncidentHistory.IncidentId) + where ApplicationVersion = @version + AND i.ApplicationId = @appId + AND IncidentHistory.State in (0, 3, 4);"; + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("appId", query.ApplicationId); + cmd.AddParameter("version", query.ApplicationVersion); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var items = new List(); + while (await reader.ReadAsync()) + { + var state = (IncidentState) reader["HistoryState"]; + var item = new GetIncidentsForStatesResultItem + { + CreatedAtUtc = (DateTime) reader["CreatedAtUtc"], + IncidentId = (int) reader["Id"], + IncidentName = GetIncidentName((string) reader["Description"]), + IsClosed = state == IncidentState.Closed, + IsNew = state == IncidentState.New, + IsReopened = state == IncidentState.ReOpened + }; + items.Add(item); + } + + return new GetIncidentsForStatesResult {Items = items.ToArray()}; + } + } + } + + private string GetIncidentName(string description) + { + var pos = description.IndexOfAny(new[] {'\r', '\n'}); + if (pos != -1) + return description.Substring(0, pos); + return description; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryEntryMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryEntryMapper.cs new file mode 100644 index 00000000..9e92decd --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryEntryMapper.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Coderr.Server.Domain.Modules.History; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.History +{ + class HistoryEntryMapper : CrudEntityMapper + { + public HistoryEntryMapper() : base("IncidentHistory") + { + Property(x => x.Id).PrimaryKey(true); + Property(x => x.IncidentState) + .ColumnName("State"); + Property(x => x.AccountId) + .ToColumnValue(x => (object)x ?? DBNull.Value) + .ToPropertyValue(x => (int?)(x is DBNull ? null : x)); + } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryRepository.cs new file mode 100644 index 00000000..77bdced3 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/History/HistoryRepository.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Modules.History; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.History +{ + [ContainerService] + internal class HistoryRepository : IHistoryRepository + { + private IAdoNetUnitOfWork _unitOfWork; + + public HistoryRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task CreateAsync(HistoryEntry entry) + { + await _unitOfWork.InsertAsync(entry); + } + + public async Task> GetByIncidentId(int incidentId) + { + return await _unitOfWork.ToListAsync("IncidentId = @0", incidentId); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ErrorReportSpikeMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ErrorReportSpikeMapper.cs new file mode 100644 index 00000000..6f7fca95 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ErrorReportSpikeMapper.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Coderr.Server.Domain.Modules.ReportSpikes; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.ReportSpikes +{ + public class ErrorReportSpikeMapper: CrudEntityMapper + + { + public ErrorReportSpikeMapper() : base("ErrorReportSpikes") + { + Property(x => x.NotifiedAccounts) + .ToColumnValue(x => string.Join(",", x)) + .ToPropertyValue(x => ((string) x) + .Split(',') + .Select(int.Parse) + .ToArray() + ); + } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ReportSpikesRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ReportSpikesRepository.cs new file mode 100644 index 00000000..23deb6cd --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/ReportSpikes/ReportSpikesRepository.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Modules.ReportSpikes; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.ReportSpikes +{ + [ContainerService] + public class ReportSpikesRepository : IReportSpikeRepository + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public ReportSpikesRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public virtual async Task GetAverageReportCountAsync(int applicationId) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"SELECT + [Day] = DATENAME(WEEKDAY, createdatutc), + Totals = cast (COUNT(*) as int) + FROM errorreports + WHERE applicationid=@appId + GROUP BY + DATENAME(WEEKDAY, createdatutc)"; + cmd.AddParameter("appId", applicationId); + var numbers = new List(); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + numbers.Add((int) reader[1]); + } + } + numbers.Sort(); + + RemovePeaks(numbers); + return (int) numbers.Average(); + } + } + + public async Task GetSpikeAsync(int applicationId) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"SELECT * FROM ErrorReportSpikes WHERE ApplicationId = @appId AND SpikeDate = @date"; + cmd.AddParameter("date", DateTime.Today); + cmd.AddParameter("appId", applicationId); + return await cmd.FirstOrDefaultAsync(); + } + } + + public async Task GetTodaysCountAsync(int applicationId) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + @"SELECT count(IncidentReports.Id) + FROM IncidentReports + JOIN Incidents ON (Incidents.Id = IncidentId) + WHERE ApplicationId = @appId + AND CreatedAtUtc >= @date"; + cmd.AddParameter("date", DateTime.UtcNow.AddHours(-24)); + cmd.AddParameter("appId", applicationId); + return (int) await cmd.ExecuteScalarAsync(); + } + } + + public async Task CreateSpikeAsync(ErrorReportSpike spike) + { + await _unitOfWork.InsertAsync(spike); + } + + public async Task UpdateSpikeAsync(ErrorReportSpike spike) + { + await _unitOfWork.UpdateAsync(spike); + } + + private static void RemovePeaks(IList numbers) + { + if (numbers.Count > 3) + { + numbers.RemoveAt(0); + numbers.RemoveAt(numbers.Count - 1); + } + if (numbers.Count > 3) + { + numbers.RemoveAt(0); + numbers.RemoveAt(numbers.Count - 1); + } + if (numbers.Count > 3) + { + numbers.RemoveAt(0); + numbers.RemoveAt(numbers.Count - 1); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs new file mode 100644 index 00000000..cf6649f3 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Coderr.Server.Domain.Modules.Similarities; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities.Entities +{ + public class ContextCollectionPropertyDbEntity + { + public ContextCollectionPropertyDbEntity() + { + } + + public ContextCollectionPropertyDbEntity(Similarity similarity) + { + Name = similarity.PropertyName; + Values = similarity.Values.Select(x => new ContextCollectionPropertyValueDbEntity(x)).ToArray(); + } + + public string Name { get; set; } + public ContextCollectionPropertyValueDbEntity[] Values { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs new file mode 100644 index 00000000..f33a6db3 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs @@ -0,0 +1,26 @@ +using System; +using Coderr.Server.Domain.Modules.Similarities; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities.Entities +{ + public class ContextCollectionPropertyValueDbEntity + { + public ContextCollectionPropertyValueDbEntity(SimilarityValue x) + { + if (x == null) throw new ArgumentNullException(nameof(x)); + Value = x.Value; + Count = x.Count; + Percentage = x.Percentage; + } + + protected ContextCollectionPropertyValueDbEntity() + { + } + + + public int Count { get; set; } + + public int Percentage { get; set; } + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs new file mode 100644 index 00000000..21b9f862 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs @@ -0,0 +1,22 @@ +using Coderr.Server.Domain.Modules.Similarities; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities.Mappers +{ + public class SimilarityCollectionMapper : CrudEntityMapper + { + public SimilarityCollectionMapper() + : base("SimilarityCollections") + { + Property(x => x.Id) + .PrimaryKey(true); + + Property(x => x.Properties) + .NotForCrud() + .NotForQueries(); + + Property(x => x.Name) + .ColumnName("ContextName"); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityMapper.cs new file mode 100644 index 00000000..2544e7e5 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityMapper.cs @@ -0,0 +1,22 @@ +using Coderr.Server.Domain.Modules.Similarities; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities.Mappers +{ + public class SimilarityMapper : CrudEntityMapper + { + public SimilarityMapper() + : base("Similarities") + { + Property(x => x.Id) + .PrimaryKey(true); + + Property(x => x.PropertyName) + .ColumnName("Name"); + + Property(x => x.Values) + .NotForCrud() + .NotForQueries(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityValueMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityValueMapper.cs new file mode 100644 index 00000000..b3e16ebf --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Mappers/SimilarityValueMapper.cs @@ -0,0 +1,14 @@ +using Coderr.Server.Domain.Modules.Similarities; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities.Mappers +{ + public class SimilarityValueMapper : CrudEntityMapper + { + public SimilarityValueMapper() + : base("SimilarityValues") + { + //Property(x => x.Id).PrimaryKey(true); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Queries/GetSimilaritiesHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Queries/GetSimilaritiesHandler.cs new file mode 100644 index 00000000..5ce9812b --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/Queries/GetSimilaritiesHandler.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.ContextData.Queries; +using Coderr.Server.Infrastructure; +using Coderr.Server.PostgreSQL.Modules.Similarities.Entities; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities.Queries +{ + public class GetSimilaritiesHandler : IQueryHandler + { + private readonly ILog _logger = LogManager.GetLogger(typeof(GetSimilaritiesHandler)); + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetSimilaritiesHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetSimilarities query) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + @"select Name, Properties from IncidentContextCollections + where IncidentId = @incidentId;"; + cmd.AddParameter("incidentId", query.IncidentId); + + var collections = new List(); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var json = (string) reader["Properties"]; + var properties = CoderrDtoSerializer.Deserialize(json); + var col = new GetSimilaritiesCollection {Name = reader.GetString(0)}; + col.Similarities = (from prop in properties + let values = + prop.Values.Select(x => new GetSimilaritiesValue(x.Value, x.Percentage, x.Count)) + select new GetSimilaritiesSimilarity(prop.Name) {Values = Enumerable.ToArray(values)} + ).ToArray(); + collections.Add(col); + } + } + + return new GetSimilaritiesResult {Collections = collections.ToArray()}; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/SimilarityRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/SimilarityRepository.cs new file mode 100644 index 00000000..ee5470e3 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Similarities/SimilarityRepository.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Modules.Similarities; +using Coderr.Server.Infrastructure; +using Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing; +using Coderr.Server.PostgreSQL.Modules.Similarities.Entities; +using Coderr.Server.PostgreSQL.Tools; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.PostgreSQL.Modules.Similarities +{ + [ContainerService] + public class SimilarityRepository : ISimilarityRepository + { + private readonly IAdoNetUnitOfWork _uow; + private ILog _logger = LogManager.GetLogger(typeof(SimilarityRepository)); + + public SimilarityRepository(IAdoNetUnitOfWork uow) + { + _uow = uow ?? throw new ArgumentNullException(nameof(uow)); + } + + public async Task CreateAsync(SimilaritiesReport similarity) + { + foreach (var collection in similarity.Collections) + { + var dto = collection.Properties.Select(x => new ContextCollectionPropertyDbEntity(x)).ToArray(); + var json = EntitySerializer.Serialize(dto); + if (collection.Id == 0) + { + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = + @"INSERT INTO IncidentContextCollections (IncidentId, Name, Properties) + VALUES(@incidentId, @name, @props);"; + cmd.AddParameter("incidentId", collection.IncidentId); + cmd.AddParameter("name", collection.Name); + cmd.AddParameter("props", json); + await cmd.ExecuteNonQueryAsync(); + } + } + else + { + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = + @"UPDATE IncidentContextCollections SET Properties=@props WHERE Id = @id;"; + cmd.AddParameter("id", collection.Id); + cmd.AddParameter("props", json); + await cmd.ExecuteNonQueryAsync(); + } + } + } + } + + public SimilaritiesReport FindForIncident(int incidentId) + { + using (var cmd = _uow.CreateCommand()) + { + cmd.CommandText = + @"select Id, Name, Properties from IncidentContextCollections + where IncidentId = @incidentId;"; + cmd.AddParameter("incidentId", incidentId); + + var collections = new List(); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var json = (string) reader["Properties"]; + var properties = CoderrDtoSerializer.Deserialize(json); + foreach (var property in properties) + { + var zeroProps = property.Values.Where(x => x.Count == 0); + foreach (var prop in zeroProps) + { + _logger.Warn( + $"Similarity with 0 count. IncidentId {incidentId}, Name {property.Name}, Value: {prop.Value}"); + prop.Count = 1; + } + } + + var col = new SimilarityCollection(incidentId, reader.GetString(1)); + col.GetType().GetProperty("Id").SetValue(col, reader.GetInt32(0)); + foreach (var entity in properties) + { + var values = entity.Values + .Select(x => new SimilarityValue(x.Value, x.Percentage, x.Count)) + .ToArray(); + var prop = new Similarity(entity.Name); + prop.LoadValues(values); + col.Properties.Add(prop); + } + collections.Add(col); + } + } + + return collections.Count == 0 ? null : new SimilaritiesReport(incidentId, collections); + } + } + + public async Task UpdateAsync(SimilaritiesReport similarity) + { + await CreateAsync(similarity); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/DoInitialRun.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/DoInitialRun.cs new file mode 100644 index 00000000..1802ab29 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/DoInitialRun.cs @@ -0,0 +1,96 @@ +//using System; +//using System.Linq; +//using System.Threading.Tasks; +//using DotNetCqs; +//using Griffin.ApplicationServices; +//using Coderr.Server.ReportAnalyzer.Abstractions; +//using Griffin.Data; +//using Coderr.Core.Api.Reports; +//using Coderr.Core.Api.Reports.Queries; +//using Coderr.Core.IncidentTagging.Data; +//using Coderr.Data; + +//namespace Coderr.Core.IncidentTagging +//{ +// [Component(RegisterAsSelf = true)] +// public class DoInitialRun : IBackgroundJobAsync +// { +// private static bool _hasRun; +// private IAdoNetUnitOfWork _unitOfWork; +// private IQueryBus _queryBus; +// private ITagsRepository _repository; + +// public DoInitialRun(IAdoNetUnitOfWork unitOfWork, IQueryBus queryBus, ITagsRepository repository) +// { +// _unitOfWork = unitOfWork; +// _queryBus = queryBus; +// _repository = repository; +// } + +// public async Task HandleAsync(IMessageContext context, ) +// { +// if (_hasRun) +// return; +// _hasRun = true; + +// using (var cmd = _unitOfWork.CreateCommand()) +// { +// cmd.CommandText = "SELECT Value FROM Settings WHERE Name = 'TagRun'"; +// if (cmd.ExecuteScalar() != null) +// return; +// } + +// using (var cmd = _unitOfWork.CreateCommand()) +// { +// cmd.CommandText = "INSERT INTO Settings (Name, Value) VALUES('TagRun', @date)"; +// cmd.AddParameter("date", DateTime.UtcNow.ToString()); +// cmd.ExecuteNonQuery(); +// } + +// using (var cmd = _unitOfWork.CreateCommand()) +// { +// cmd.CommandText = @"WITH summary AS ( +// SELECT p.id as reportid, +// p.incidentid, +// p.createdatutc, +// ROW_NUMBER() OVER(PARTITION BY p.incidentid +// ORDER BY p.createdatutc asc) AS rk +// FROM errorreports p) +//SELECT s.* +// FROM summary s +// left join incidenttags t on (t.incidentid=s.incidentid) +// WHERE s.rk = 1 +// AND t.incidentid is null"; +// using (var reader = cmd.ExecuteReader()) +// { +// while (reader.Read()) +// { +// var reportId = (int) reader["reportid"]; +// var incidentId = (int)reader["incidentid"]; +// var q = new FindReport() {ReportId = reportId}; +// var report = await _queryBus.QueryAsync(q); + +// var dto = new NewReportDTO +// { +// ContextCollections = report.ContextCollections, +// CreatedAtUtc = report.CreatedAtUtc, +// Exception = report.Exception, +// RemoteAddress = report.RemoteAddress, +// ReportVersion = report.ReportVersion +// }; +// var ctx = new TagIdentifierContext(dto); +// var identiferProvider = new IdentifierProvider(); +// var identifiers = identiferProvider.GetIdentifiers(ctx); +// foreach (var identifier in identifiers) +// { +// identifier.Identify(ctx); +// } + +// _repository.Add(incidentId, ctx.Tags.ToArray()); +// } +// } +// } +// } +// } +//} + diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/TagsRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/TagsRepository.cs new file mode 100644 index 00000000..a944ea32 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Tagging/TagsRepository.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Modules.Tags; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.Tagging +{ + [ContainerService] + public class TagsRepository : ITagsRepository + { + private readonly IAdoNetUnitOfWork _adoNetUnitOfWork; + + public TagsRepository(IAdoNetUnitOfWork adoNetUnitOfWork) + { + _adoNetUnitOfWork = adoNetUnitOfWork; + } + + public async Task AddAsync(int incidentId, Tag[] tags) + { + foreach (var tag in tags) + { + using (var cmd = _adoNetUnitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "INSERT INTO IncidentTags (IncidentId, TagName, OrderNumber) VALUES(@incidentId, @name, @orderNumber);"; + cmd.AddParameter("incidentId", incidentId); + cmd.AddParameter("name", tag.Name); + cmd.AddParameter("orderNumber", tag.OrderNumber); + await cmd.ExecuteNonQueryAsync(); + } + } + } + + public async Task> GetIncidentTagsAsync(int incidentId) + { + using (var cmd = _adoNetUnitOfWork.CreateDbCommand()) + { + cmd.CommandText = "SELECT * FROM IncidentTags WHERE IncidentId = @id ORDER BY OrderNumber;"; + cmd.AddParameter("id", incidentId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var tags = new List(); + while (await reader.ReadAsync()) + { + var tag = new Tag((string) reader["TagName"], (int) reader["OrderNumber"]); + tags.Add(tag); + } + return tags; + } + } + } + + public async Task> GetTagsAsync(int? applicationId, int? incidentId) + { + using (var cmd = _adoNetUnitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"select TagName, min(OrderNumber) + FROM IncidentTags + INNER JOIN Incidents ON (IncidentTags.IncidentId=Incidents.Id);"; + if (incidentId != null) + { + cmd.CommandText += " WHERE Incidents.Id = @incidentId"; + cmd.AddParameter("@incidentId", incidentId.Value); + } + else if (applicationId != null) + { + cmd.CommandText += " WHERE Incidents.ApplicationId = @applicationId"; + cmd.AddParameter("appId", applicationId.Value); + } + + cmd.CommandText += " GROUP BY TagName"; + using (var reader = await cmd.ExecuteReaderAsync()) + { + var tags = new List(); + while (await reader.ReadAsync()) + { + var tag = new Tag((string) reader[0], (int) reader[1]); + tags.Add(tag); + } + return tags; + } + } + } + + public async Task> GetApplicationTagsAsync(int applicationId) + { + using (var cmd = _adoNetUnitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"select TagName, min(OrderNumber) + FROM IncidentTags + INNER JOIN Incidents ON (IncidentTags.IncidentId=Incidents.Id) + WHERE Incidents.ApplicationId = @id + GROUP BY TagName;"; + cmd.AddParameter("id", applicationId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var tags = new List(); + while (await reader.ReadAsync()) + { + var tag = new Tag((string) reader[0], (int) reader[1]); + tags.Add(tag); + } + return tags; + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/CollectionMetadataMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/CollectionMetadataMapper.cs new file mode 100644 index 00000000..981618d8 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/CollectionMetadataMapper.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Coderr.Server.ReportAnalyzer.Triggers; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + public class CollectionMetadataMapper : CrudEntityMapper + { + public CollectionMetadataMapper() : base("CollectionMetaData") + { + Property(x => x.IsUpdated).Ignore(); + + Property(x => x.Properties) + .ToPropertyValue(EntitySerializer.Deserialize>); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/DeleteTriggerHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/DeleteTriggerHandler.cs new file mode 100644 index 00000000..273b0bcf --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/DeleteTriggerHandler.cs @@ -0,0 +1,29 @@ +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Triggers.Commands; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + public class DeleteTriggerHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _uow; + + public DeleteTriggerHandler(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public async Task HandleAsync(IMessageContext context, DeleteTrigger command) + { + using (var cmd = (DbCommand) _uow.CreateCommand()) + { + cmd.CommandText = "DELETE FROM Triggers WHERE Id = @id;"; + cmd.AddParameter("id", command.Id); + await cmd.ExecuteNonQueryAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetContextCollectionMetadataHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetContextCollectionMetadataHandler.cs new file mode 100644 index 00000000..8201d315 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetContextCollectionMetadataHandler.cs @@ -0,0 +1,39 @@ +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Triggers.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + internal class GetContextCollectionMetadataHandler : + IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetContextCollectionMetadataHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetContextCollectionMetadata query) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "SELECT * FROM CollectionMetadata WHERE ApplicationId = @id;"; + + cmd.AddParameter("id", query.ApplicationId); + var items = await cmd.ToListAsync(new CollectionMetadataMapper()); + return items.Select(x => new GetContextCollectionMetadataItem + { + Name = x.Name, + Properties = x.Properties.ToArray() + }).ToArray(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetTriggersForApplicationHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetTriggersForApplicationHandler.cs new file mode 100644 index 00000000..f7d109d0 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/GetTriggersForApplicationHandler.cs @@ -0,0 +1,32 @@ +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Triggers; +using Coderr.Server.Api.Modules.Triggers.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + public class GetTriggersForApplicationHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetTriggersForApplicationHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetTriggersForApplication query) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = "SELECT * FROM Triggers WHERE [ApplicationId]=@appId;"; + cmd.AddParameter("appId", query.ApplicationId); + return (await cmd.ToListAsync()).ToArray(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerDtoMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerDtoMapper.cs new file mode 100644 index 00000000..15c6cd5c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerDtoMapper.cs @@ -0,0 +1,17 @@ +using Coderr.Server.Api.Modules.Triggers; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + public class TriggerDtoMapper : EntityMapper + { + public TriggerDtoMapper() + { + Property(x => x.Id) + .ToPropertyValue(x => x.ToString()); + + Property(x => x.Summary) + .Ignore(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerMapper.cs new file mode 100644 index 00000000..98b96fe8 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerMapper.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.ReportAnalyzer.Triggers; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + public class TriggerMapper : CrudEntityMapper + { + public TriggerMapper() : base("Triggers") + { + Property(x => x.LastTriggerAction) + .ToPropertyValue(o => (LastTriggerAction) Enum.Parse(typeof(LastTriggerAction), o.ToString())) + .ToColumnValue(o => o.ToString()); + + + Property(x => x.Actions) + .ToPropertyValue(o => EntitySerializer.Deserialize>((string) o)) + .ToColumnValue(o => o.ToString()); + + Property(x => x.Rules) + .ToPropertyValue(o => EntitySerializer.Deserialize>((string) o)) + .ToColumnValue(o => o.ToString()); + + Property(x => x.RunForExistingIncidents) + .ToPropertyValue(Convert.ToBoolean); + + Property(x => x.RunForNewIncidents) + .ToPropertyValue(Convert.ToBoolean); + + Property(x => x.RunForReopenedIncidents) + .ToPropertyValue(Convert.ToBoolean); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerRepository.cs new file mode 100644 index 00000000..085c5f44 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Triggers/TriggerRepository.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.ReportAnalyzer.Triggers; +using Coderr.Server.PostgreSQL.Tools; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Triggers +{ + [ContainerService] + public class TriggerRepository : ITriggerRepository + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public TriggerRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + + public async Task> GetCollectionsAsync(int applicationId) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "SELECT * FROM CollectionMetadata WHERE ApplicationId = @id;"; + + cmd.AddParameter("id", applicationId); + return await cmd.ToListAsync(new CollectionMetadataMapper()); + } + } + + + public async Task UpdateAsync(CollectionMetadata collection) + { + var props = EntitySerializer.Serialize(collection.Properties); + + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"UPDATE CollectionMetadata SET Properties = @Properties + WHERE Id = @id;"; + + cmd.AddParameter("Id", collection.Id); + cmd.AddParameter("Properties", props); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task CreateAsync(CollectionMetadata entity) + { + var props = EntitySerializer.Serialize(entity.Properties); + + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + @"INSERT INTO CollectionMetadata (Name, ApplicationId, Properties) VALUES(@Name, @ApplicationId, @Properties);"; + cmd.AddParameter("Name", entity.Name); + cmd.AddParameter("ApplicationId", entity.ApplicationId); + cmd.AddParameter("Properties", props); + await cmd.ExecuteNonQueryAsync(); + } + } + + + public async Task CreateAsync(Trigger trigger) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "INSERT INTO Triggers (ApplicationId, Name, Description, Rules, Actions, LastTriggerAction, RunForNewIncidents, RunForExistingIncidents) " + + "VALUES(@ApplicationId, @Name, @Description, @Rules, @Actions, @LastTriggerAction, @RunForNewIncidents, @RunForExistingIncidents);"; + cmd.AddParameter("ApplicationId", trigger.ApplicationId); + cmd.AddParameter("Name", trigger.Name); + cmd.AddParameter("Description", trigger.Description); + cmd.AddParameter("Rules", EntitySerializer.Serialize(trigger.Rules)); + cmd.AddParameter("Actions", EntitySerializer.Serialize(trigger.Actions)); + cmd.AddParameter("LastTriggerAction", trigger.LastTriggerAction); + cmd.AddParameter("RunForNewIncidents", trigger.RunForNewIncidents); + cmd.AddParameter("RunForExistingIncidents", trigger.RunForExistingIncidents); + await cmd.ExecuteNonQueryAsync(); + } + } + + public IEnumerable GetForApplication(int applicationId) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "SELECT * FROM Triggers WHERE ApplicationId = @applicationId;"; + cmd.AddParameter("applicationId", applicationId); + return cmd.ToList(new TriggerMapper()).ToList(); + } + } + + + public async Task GetAsync(int id) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "SELECT * FROM Triggers WHERE Id = @id;"; + cmd.AddParameter("id", id); + return await cmd.FirstAsync(new TriggerMapper()); + } + } + + public async Task UpdateAsync(Trigger trigger) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "UPDATE Triggers SET Name=@Name, Description=@Description, Rules=@Rules, Actions=@Actions, LastTriggerAction=@LastTriggerAction, RunForNewIncidents = @RunForNewIncidents, RunForExistingIncidents=@RunForExistingIncidents " + + " WHERE Id=@Id;"; + cmd.AddParameter("Id", trigger.Id); + cmd.AddParameter("Name", trigger.Name); + cmd.AddParameter("Description", trigger.Description); + cmd.AddParameter("Rules", EntitySerializer.Serialize(trigger.Rules)); + cmd.AddParameter("Actions", EntitySerializer.Serialize(trigger.Actions)); + cmd.AddParameter("LastTriggerAction", trigger.LastTriggerAction); + cmd.AddParameter("RunForNewIncidents", trigger.RunForNewIncidents); + cmd.AddParameter("RunForExistingIncidents", trigger.RunForExistingIncidents); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task DeleteAsync(int id) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = + "DELETE FROM Triggers" + + " WHERE Id=@Id;"; + cmd.AddParameter("Id", id); + await cmd.ExecuteNonQueryAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfig.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfig.cs new file mode 100644 index 00000000..345b7cbd --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfig.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.PostgreSQL.Modules.Versions +{ + public class ApplicationVersionConfig + { + public int Id { get; set; } + public int ApplicationId { get; set; } + public string AssemblyName { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfigMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfigMapper.cs new file mode 100644 index 00000000..0d28f2aa --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionConfigMapper.cs @@ -0,0 +1,13 @@ +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Versions +{ + public class ApplicationVersionConfigMapper : CrudEntityMapper + { + public ApplicationVersionConfigMapper() : base("ApplicationVersions") + { + Property(x => x.Id) + .PrimaryKey(true); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMapper.cs new file mode 100644 index 00000000..294cadf7 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMapper.cs @@ -0,0 +1,19 @@ +using Coderr.Server.App.Modules.Versions; +using Coderr.Server.Domain.Modules.ApplicationVersions; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Versions +{ + public class ApplicationVersionMapper : CrudEntityMapper + { + public ApplicationVersionMapper() : base("ApplicationVersions") + { + Property(x => x.Id) + .PrimaryKey(true); + Property(x => x.ReceivedFirstReportAtUtc) + .ColumnName("FirstReportDate"); + Property(x => x.ReceivedLastReportAtUtc) + .ColumnName("LastReportDate"); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMonthMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMonthMapper.cs new file mode 100644 index 00000000..0a3f1717 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/ApplicationVersionMonthMapper.cs @@ -0,0 +1,15 @@ +using Coderr.Server.App.Modules.Versions; +using Coderr.Server.Domain.Modules.ApplicationVersions; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Versions +{ + public class ApplicationVersionMonthMapper : CrudEntityMapper + { + public ApplicationVersionMonthMapper() : base("ApplicationVersionMonths") + { + Property(x => x.Id) + .PrimaryKey(true); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetApplicationVersionsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetApplicationVersionsHandler.cs new file mode 100644 index 00000000..ccef71e0 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetApplicationVersionsHandler.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Versions.Queries; +using Coderr.Server.Infrastructure; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.Versions.Queries +{ + internal class GetApplicationVersionsHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _uow; + + public GetApplicationVersionsHandler(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public async Task HandleAsync(IMessageContext context, + GetApplicationVersions query) + { + var sql = + @"SELECT version, sum(incidentcount) incidentcount, sum(reportcount) reportcount, min(FirstReportDate) as FirstReportDate, max(LastReportDate)as LastReportDate + FROM ApplicationVersions + join ApplicationVersionMonths on (versionid=applicationversions.id) + where applicationid=@appId + group by version + order by version; +"; + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("appId", query.ApplicationId); + using (var reader = await cmd.ExecuteReaderAsync()) + { + var items = new List(); + while (await reader.ReadAsync()) + { + var item = new GetApplicationVersionsResultItem + { + Version = reader[0].ToString(), + FirstReportReceivedAtUtc = (DateTime) reader[3], + LastReportReceivedAtUtc = (DateTime) reader[4], + IncidentCount = (int) reader[1], + ReportCount = (int) reader[2] + }; + items.Add(item); + } + + var comparer = new ApplicationVersionComparer(); + var sortedItems = items.OrderByDescending(x => x.Version, comparer).ToArray(); + return new GetApplicationVersionsResult {Items = sortedItems}; + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetVersionHistoryHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetVersionHistoryHandler.cs new file mode 100644 index 00000000..c325e39d --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/GetVersionHistoryHandler.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Versions.Queries; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Modules.Versions.Queries +{ + public class GetVersionHistoryHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetVersionHistoryHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + } + + public async Task HandleAsync(IMessageContext context, GetVersionHistory query) + { + var sql = + @"select avm.YearMonth, avm.IncidentCount, avm.ReportCount, LastUpdateAtUtc, av.Version + from [ApplicationVersionMonths] avm + JOIN ApplicationVersions av ON (av.Id=avm.VersionId) + WHERE av.ApplicationId = @appId + ORDER BY YearMonth, ApplicationId, av.Version;"; + + var first = DateTime.MinValue; + var last = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.AddParameter("appId", query.ApplicationId); + if (query.FromDate != null) + { + sql += " AND YearMonth >= @from"; + cmd.AddParameter("from", query.FromDate.Value); + first = query.FromDate.Value; + } + + if (query.ToDate != null) + { + sql += " AND YearMonth <= @to"; + cmd.AddParameter("to", query.ToDate.Value); + last = query.ToDate.Value; + } + + cmd.CommandText = sql; + var versions = new Versions(); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var month = (DateTime) reader["YearMonth"]; + if (first == DateTime.MinValue) + first = month; + var incindentCount = (int) reader["IncidentCount"]; + var reportCount = (int) reader["ReportCount"]; + var version = (string) reader["Version"]; + + versions.AddCounts(version, month, incindentCount, reportCount); + } + } + + if (versions.IsEmpty) + return new GetVersionHistoryResult + { + Dates = new string[0], + IncidentCounts = new GetVersionHistoryResultSet[0], + ReportCounts = new GetVersionHistoryResultSet[0] + }; + + + versions.PadMonths(first, last); + + var result = new GetVersionHistoryResult + { + Dates = versions.GetDates(), + IncidentCounts = versions.BuildIncidentSeries(), + ReportCounts = versions.BuildReportSeries() + }; + return result; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/VersionRange.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/VersionRange.cs new file mode 100644 index 00000000..1593f044 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/VersionRange.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.PostgreSQL.Modules.Versions.Queries +{ + internal class VersionRange + { + private readonly List _incdentRange = new List(); + private readonly Dictionary _incidents = new Dictionary(); + private readonly List _reportRange = new List(); + private readonly Dictionary _reports = new Dictionary(); + private List _dates = new List(); + public IEnumerable IncidentSeries => _incdentRange; + + public IEnumerable ReportSeries => _reportRange; + + public IEnumerable Dates => _dates; + public string Version { get; set; } + + public void AddCounts(DateTime yearMonth, int incidentCount, int reportCount) + { + _reports[yearMonth] = reportCount; + _incidents[yearMonth] = incidentCount; + } + + public void EnsureMonth(DateTime yearMonth) + { + _dates.Add(yearMonth); + if (!_incidents.TryGetValue(yearMonth, out var count)) + _incdentRange.Add(0); + else + _incdentRange.Add(count); + + if (!_reports.TryGetValue(yearMonth, out var count1)) + _reportRange.Add(0); + else + _reportRange.Add(count1); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/Versions.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/Versions.cs new file mode 100644 index 00000000..a1601b5a --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/Queries/Versions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Coderr.Server.Api.Modules.Versions.Queries; +using Coderr.Server.Infrastructure; + +namespace Coderr.Server.PostgreSQL.Modules.Versions.Queries +{ + internal class Versions + { + private readonly Dictionary _versions = new Dictionary(); + + public bool IsEmpty => _versions.Count == 0; + + public void AddCounts(string version, DateTime yearMonth, int incidentCount, int reportCount) + { + if (!_versions.TryGetValue(version, out var entity)) + { + entity = new VersionRange(); + _versions[version] = entity; + } + + entity.AddCounts(yearMonth, incidentCount, reportCount); + } + + public GetVersionHistoryResultSet[] BuildIncidentSeries() + { + var items = new List(); + foreach (var key in _versions.Keys) + { + items.Add(new GetVersionHistoryResultSet + { + Name = key, + Values = _versions[key].IncidentSeries.ToArray() + }); + } + + return items.OrderBy(x => x.Name, new ApplicationVersionComparer()).ToArray(); + } + + public GetVersionHistoryResultSet[] BuildReportSeries() + { + var items = new List(); + foreach (var key in _versions.Keys) + { + items.Add(new GetVersionHistoryResultSet + { + Name = key, + Values = _versions[key].ReportSeries.ToArray() + }); + } + + return items.OrderBy(x => x.Name, new ApplicationVersionComparer()).ToArray(); + } + + public string[] GetDates() + { + var firstKey = _versions.Keys.First(); + return _versions[firstKey].Dates.Select(x => x.ToShortDateString()).ToArray(); + } + + public void PadMonths(DateTime from, DateTime to) + { + foreach (var version in _versions) + { + var current = from; + while (current <= to) + { + version.Value.EnsureMonth(current); + current = current.AddMonths(1); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/VersionRepository.cs b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/VersionRepository.cs new file mode 100644 index 00000000..558cb230 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Modules/Versions/VersionRepository.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Modules.ApplicationVersions; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Modules.Versions +{ + /// + /// ADO.NET based implementation of . + /// + [ContainerService] + public class VersionRepository : IApplicationVersionRepository + { + private readonly IAdoNetUnitOfWork _uow; + + /// + /// Creates a new instance of + /// + /// Unit of work + public VersionRepository(IAdoNetUnitOfWork uow) + { + _uow = uow ?? throw new ArgumentNullException(nameof(uow)); + } + + /// + public async Task CreateAsync(ApplicationVersionMonth month) + { + if (month == null) throw new ArgumentNullException(nameof(month)); + + await _uow.InsertAsync(month); + } + + /// + public async Task CreateAsync(ApplicationVersion entity) + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + + await _uow.InsertAsync(entity); + } + + + /// + public Task FindMonthForApplicationAsync(int versionId, int year, int month) + { + if (versionId <= 0) throw new ArgumentOutOfRangeException(nameof(versionId)); + if (year <= 0) throw new ArgumentOutOfRangeException(nameof(year)); + if (month <= 0) throw new ArgumentOutOfRangeException(nameof(month)); + + var date = new DateTime(year, month, 1); + return _uow.FirstOrDefaultAsync(new {VersionId = versionId, YearMonth = date}); + } + + /// + public Task FindVersionAsync(int applicationId, string version) + { + if (version == null) throw new ArgumentNullException(nameof(version)); + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + + return _uow.FirstOrDefaultAsync(new {ApplicationId = applicationId, Version = version}); + } + + public async Task> FindForIncidentAsync(int incidentId) + { + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = @"select ApplicationVersions.* + FROM IncidentVersions + JOIN ApplicationVersions ON (IncidentVersions.VersionId = ApplicationVersions.Id) + WHERE IncidentVersions.IncidentId = @incidentId;"; + cmd.AddParameter("incidentId", incidentId); + return await cmd.ToListAsync(); + } + } + + public void SaveIncidentVersion(int incidentId, int versionId) + { + var sql = @"INSERT INTO IncidentVersions (IncidentId, VersionId) + SELECT @incidentId, @versionId + WHERE NOT EXISTS ( + select IncidentId + from IncidentVersions + WHERE VersionId=@versionId + );"; + using (var cmd = _uow.CreateDbCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("incidentId", incidentId); + cmd.AddParameter("versionId", versionId); + cmd.ExecuteNonQuery(); + } + } + + public async Task UpdateAsync(ApplicationVersionMonth month) + { + await _uow.UpdateAsync(month); + } + + public async Task UpdateAsync(ApplicationVersion entity) + { + await _uow.UpdateAsync(entity); + } + + public async Task> FindVersionsAsync(int appId) + { + return await _uow.ToListAsync(new StringMapper(), "SELECT Version FROM ApplicationVersions WHERE ApplicationId = @id;", + new {id = appId}); + } + + + public async Task GetVersionAssemblyNameAsync(int applicationId) + { + var item = await _uow.FirstOrDefaultAsync(new {ApplicationId = applicationId}); + return item?.AssemblyName; + } + } + + public class StringMapper : IEntityMapper + { + public void Map(IDataRecord source, string destination) + { + } + + public object Create(IDataRecord record) + { + return record[0]; + } + + public void Map(IDataRecord source, object destination) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReadMe.md b/src/Server/Coderr.Server.PostgreSQL/ReadMe.md new file mode 100644 index 00000000..a8457905 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReadMe.md @@ -0,0 +1,6 @@ +PostgreSQL implementation of the data layer +========================================== + +Currently there are only one implementation of the data layer. And it's for Microsoft Sql Server version 2012 and above. + +Feel free to make a copy of this project and adapt the SQL queries for your favorite database engine. diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/AnalyticsRepository.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/AnalyticsRepository.cs new file mode 100644 index 00000000..eb9d9387 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/AnalyticsRepository.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.ErrorReports; +using Coderr.Server.Infrastructure; +using Coderr.Server.ReportAnalyzer; +using Coderr.Server.ReportAnalyzer.Inbound.Handlers.Reports; +using Coderr.Server.ReportAnalyzer.Incidents; +using Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs; +using Coderr.Server.PostgreSQL.Tools; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; +using log4net; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer +{ + [ContainerService] + public class AnalyticsRepository : IAnalyticsRepository + { + private readonly ILog _logger = LogManager.GetLogger(typeof(AnalyticsRepository)); + private readonly ReportDtoConverter _reportDtoConverter = new ReportDtoConverter(); + private readonly IAdoNetUnitOfWork _unitOfWork; + private const int MaxCollectionSize = 2000000; + + public AnalyticsRepository(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + } + + public bool ExistsByClientId(string clientReportId) + { + if (clientReportId == null) throw new ArgumentNullException("clientReportId"); + + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = "select id from ErrorReports WHERE ErrorId = @Id;"; + cmd.AddParameter("id", clientReportId); + return cmd.ExecuteScalar() != null; + } + } + + public IncidentBeingAnalyzed FindIncidentForReport(int applicationId, string reportHashCode, + string hashCodeIdentifier) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = + @"SELECT Incidents.* + FROM Incidents + WHERE ReportHashCode = @ReportHashCode + AND ApplicationId = @applicationId;"; + + + cmd.AddParameter("ReportHashCode", reportHashCode); + cmd.AddParameter("applicationId", applicationId); + + var incidents = cmd.ToList(); + return incidents.FirstOrDefault(incident => incident.HashCodeIdentifier == hashCodeIdentifier); + } + } + + public void CreateIncident(IncidentBeingAnalyzed incident) + { + if (incident == null) throw new ArgumentNullException("incident"); + if (string.IsNullOrEmpty(incident.ReportHashCode)) + throw new InvalidOperationException("ReportHashCode is required to be able to detect duplicates"); + + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "INSERT INTO Incidents (ReportHashCode, ApplicationId, CreatedAtUtc, HashCodeIdentifier, StackTrace, ReportCount, UpdatedAtUtc, Description, FullName, IsReOpened, LastReportAtUtc)" + + " VALUES (@ReportHashCode, @ApplicationId, @CreatedAtUtc, @HashCodeIdentifier, @StackTrace, @ReportCount, @UpdatedAtUtc, @Description, @FullName, 0, @LastReportAtUtc);RETURNING Id;"; + cmd.AddParameter("Id", incident.Id); + cmd.AddParameter("ReportHashCode", incident.ReportHashCode); + cmd.AddParameter("ApplicationId", incident.ApplicationId); + cmd.AddParameter("CreatedAtUtc", incident.CreatedAtUtc); + cmd.AddParameter("HashCodeIdentifier", incident.HashCodeIdentifier); + cmd.AddParameter("ReportCount", incident.ReportCount); + cmd.AddParameter("UpdatedAtUtc", incident.UpdatedAtUtc); + cmd.AddParameter("Description", incident.Description); + cmd.AddParameter("StackTrace", incident.StackTrace); + cmd.AddParameter("FullName", incident.FullName); + cmd.AddParameter("LastReportAtUtc", incident.LastReportAtUtc); + var id = (int) (decimal) cmd.ExecuteScalar(); + incident.GetType() + .GetProperty("Id", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .SetValue(incident, id); + } + + //_unitOfWork.Insert(incident); + } + + public void SaveEnvironmentName(int incidentId, string environmentName) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"declare @environmentId int; + select @environmentId = id + from Environments + WHERE Name = @name; + + if @environmentId is null + begin + insert into Environments(Name) VALUES(@name); + set @environmentId = scope_identity(); + insert into IncidentEnvironments (IncidentId, EnvironmentId) VALUES(@incidentId, @environmentId); + end + else + begin + INSERT INTO IncidentEnvironments (IncidentId, EnvironmentId) + SELECT @incidentId, @environmentId + WHERE NOT EXISTS (SELECT IncidentId, EnvironmentId FROM IncidentEnvironments + WHERE IncidentId=@incidentId AND EnvironmentId=@environmentId) + end;"; + cmd.AddParameter("incidentId", incidentId); + cmd.AddParameter("name", environmentName); + cmd.ExecuteNonQuery(); + } + } + + public void UpdateIncident(IncidentBeingAnalyzed incident) + { + if (incident == null) throw new ArgumentNullException("incident"); + _unitOfWork.Update(incident); + } + + public int GetMonthReportCount() + { + using (var cmd = _unitOfWork.CreateCommand()) + { + var from = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + var to = DateTime.UtcNow; + + cmd.CommandText = + "SELECT count(*) FROM IncidentReports WHERE ReceivedAtUtc >= @from ANd ReceivedAtUtc <= @to;"; + cmd.AddParameter("from", from); + cmd.AddParameter("to", to); + return (int)cmd.ExecuteScalar(); + } + } + + public void AddMissedReport(DateTime date) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = + @"update IgnoredReports set NumberOfReports=NumberOfReports+1 WHERE date = @date; + IF @@ROWCOUNT=0 insert into IgnoredReports(NumberOfReports, Date) values(1, Convert(date, GetUtcDate()));"; + cmd.AddParameter("date", date.Date); + cmd.ExecuteNonQuery(); + } + } + + public async Task StoreReportStats(ReportMapping mapping) + { + await _unitOfWork.InsertAsync(mapping); + } + + public void CreateReport(ErrorReportEntity report) + { + if (report == null) throw new ArgumentNullException("report"); + + if (string.IsNullOrEmpty(report.Title) && report.Exception != null) + { + report.Title = report.Exception.Message; + if (report.Title.Length > 100) + report.Title = report.Title.Substring(0, 100); + } + + var collections = new List(); + foreach (var context in report.ContextCollections) + { + var data = EntitySerializer.Serialize(context); + if (data.Length > MaxCollectionSize) + { + var tooLargeCtx = new ErrorReportContextCollection(context.Name, + new Dictionary() + { + { + "Error", + $"This collection was larger ({data.Length}bytes) than the threshold of {MaxCollectionSize}bytes" + } + }); + + data = EntitySerializer.Serialize(tooLargeCtx); + } + collections.Add(data); + } + + _unitOfWork.Insert(report); + + var cols = string.Join(", ", collections); + var inboound = new InboundCollection + { + JsonData = $"[{cols}]", + ReportId = report.Id + }; + _unitOfWork.Insert(inboound); + } + + public string GetAppName(int applicationId) + { + if (applicationId < 1) + throw new ArgumentOutOfRangeException("applicationId", applicationId, "AppId must be a PK"); + + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = "SELECT Name FROM Applications WHERE Id = @id;"; + cmd.AddParameter("id", applicationId); + return (string) cmd.ExecuteScalar(); + } + } + + public IReadOnlyList GetReportsUsingSql() + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = @"SELECT QueueReports.* + FROM QueueReports + ORDER BY QueueReports.Id;"; + cmd.Limit(10); + + try + { + var reports = new List(); + var idsToRemove = new List(); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var json = ""; + try + { + json = (string) reader["body"]; + var report = _reportDtoConverter.LoadReportFromJson(json); + var newReport = _reportDtoConverter.ConvertReport(report, (int) reader["ApplicationId"]); + newReport.RemoteAddress = (string) reader["RemoteAddress"]; + + var claims = CoderrDtoSerializer.Deserialize((string) reader["Claims"]); + newReport.User = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + reports.Add(newReport); + idsToRemove.Add(reader.GetInt32(0)); + } + catch (Exception ex) + { + _logger.Error("Failed to deserialize " + json, ex); + } + } + } + if (idsToRemove.Any()) + _unitOfWork.ExecuteNonQuery("DELETE FROM QueueReports WHERE Id IN (" + + string.Join(",", idsToRemove) + ");"); + return reports; + } + catch (Exception ex) + { + throw cmd.CreateDataException(ex); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportEntityMapper.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportEntityMapper.cs new file mode 100644 index 00000000..666468ad --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportEntityMapper.cs @@ -0,0 +1,34 @@ +using Coderr.Server.Domain.Core.ErrorReports; +using Coderr.Server.PostgreSQL.Tools; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer +{ + public class ErrorReportEntityMapper : CrudEntityMapper + { + public ErrorReportEntityMapper() : base("ErrorReports") + { + Property(x => x.Id) + .PrimaryKey(true); + + Property(x => x.Exception) + .ToPropertyValue(EntitySerializer.Deserialize) + .ToColumnValue(EntitySerializer.Serialize); + + Property(x => x.ContextCollections) + .ColumnName("ContextInfo") + .ToPropertyValue(x => null) + .ToColumnValue(x => ""); + + Property(x => x.EnvironmentName) + .Ignore(); + + Property(x => x.ClientReportId) + .ColumnName("ErrorId"); + + Property(x => x.User) + .Ignore(); + + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportRepository.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportRepository.cs new file mode 100644 index 00000000..38ee94ad --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/ErrorReportRepository.cs @@ -0,0 +1,185 @@ +//using System; +//using System.Collections.Generic; +//using System.Data.Common; +//using System.Linq; +//using System.Threading.Tasks; +//using Coderr.Server.ReportAnalyzer.Abstractions; +//using Griffin.Data; +//using Griffin.Data.Mapper; +//using Coderr.Core; +//using Coderr.ReportsAnalytics.Reports; +//using Coderr.UnitOfWork; + +//namespace Coderr.ReportAnalytics.Data.PostgreSQL +//{ + +// // public interface IReportsRepository +// // { +// // Task Create(InvalidErrorReport invalidReport); +// // /// +// // /// Customer specific id. +// // /// +// // /// +// // /// +// // //Task FindByErrorIdAsync(string errorId); +// // Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize); + +// // /// +// // /// Finds the by error identifier asynchronous. +// // /// +// // /// Customer generated id (from the client library). +// // /// +// // Task FindByErrorIdAsync(string errorId); +// // } + + +// [ContainerService] +// internal class ErrorReportRepository// : IReportsRepository +// { +// private IAdoNetUnitOfWork _uow; + +// public ErrorReportRepository(CustomerUnitOfWork uow) +// { +// if (uow == null) throw new ArgumentNullException("uow"); + +// _uow = uow; +// } + +// public void Create(ErrorReportEntity entity) +// { +// using (var cmd = _uow.CreateCommand()) +// { +// cmd.CommandText = "INSERT INTO ErrorReports (Id, ErrorId, ApplicationId, Title, Exception, ReportHashCode, HashCodeIdentifier, IncidentId, CreatedAtUtc, ContextInfo)" +// + +// " VALUES (@Id, @ErrorId, @ApplicationId, @Title, @Exception, @ReportHashCode, @HashCodeIdentifier, @IncidentId, @CreatedAtUtc, @ContextInfo)"; + +// var ex = JsonSerializer.Serialize(entity.Exception); +// var contexts = JsonSerializer.Serialize(entity.ContextInfo); +// cmd.AddParameter("Id", entity.Id); +// cmd.AddParameter("ErrorId", entity.ClientReportId); +// cmd.AddParameter("ApplicationId", entity.ApplicationId); +// cmd.AddParameter("Exception", ex); +// cmd.AddParameter("ReportHashCode", entity.ReportHashCode); +// cmd.AddParameter("HashCodeIdentifier", entity.HashCodeIdentifier); +// cmd.AddParameter("IncidentId", entity.IncidentId); +// cmd.AddParameter("CreatedAtUtc", entity.CreatedAtUtc); +// cmd.AddParameter("ContextInfo", contexts); + +// if (entity.Exception != null) +// { +// var pos = entity.Exception.Message.IndexOfAny(new[] { '\r', '\n' }); +// cmd.AddParameter("Title", pos == -1 +// ? entity.Exception.Message +// : entity.Exception.Message.Substring(0, pos)); +// } +// else +// cmd.AddParameter("Title", ""); + +// cmd.ExecuteNonQuery(); +// } +// } + + +// public void Update(ErrorReportEntity entity) +// { +// var ex = JsonSerializer.Serialize(entity.Exception); +// var contexts = JsonSerializer.Serialize(entity.ContextInfo); + + +// using (var cmd = _uow.CreateCommand()) +// { +// cmd.CommandText = @"UPDATE ErrorReports SET ErrorId = @ErrorId, +//ApplicationId = @ApplicationId, +//Exception = @Exception, +//ReportHashCode = @ReportHashCode, +//HashCodeIdentifier = @HashCodeIdentifier, +//IncidentId = @incidentId, +//CreatedAtUtc = @CreatedAtUtc, +//ContextInfo = @ContextInfo +//WHERE Id = @id"; + +// cmd.AddParameter("ErrorId", entity.ClientReportId); +// cmd.AddParameter("ApplicationId", entity.ApplicationId); +// cmd.AddParameter("Exception", ex); +// cmd.AddParameter("ReportHashCode", entity.ReportHashCode); +// cmd.AddParameter("HashCodeIdentifier", entity.HashCodeIdentifier); +// cmd.AddParameter("IncidentId", entity.IncidentId); +// cmd.AddParameter("CreatedAtUtc", entity.CreatedAtUtc); +// cmd.AddParameter("ContextInfo", contexts); +// cmd.AddParameter("Id", entity.Id); +// cmd.ExecuteNonQuery(); +// } +// } + + +// //public async Task Create(InvalidErrorReport entity) +// //{ +// // using (var cmd = (DbCommand)_uow.CreateCommand()) +// // { +// // cmd.CommandText = +// // @"INSERT INTO InvalidErrorReports (Id, AddedAtUtc, ApplicationId, Body, Exception) VALUES(@Id, @AddedAtUtc, @OrganizationId, @ApplicationId, @Body, @Exception)"; +// // cmd.AddParameter("Id", entity.Id); +// // cmd.AddParameter("AddedAtUtc", entity.AddedAtUtc); +// // cmd.AddParameter("ApplicationId", entity.ApplicationId); +// // cmd.AddParameter("Body", entity.Report); +// // cmd.AddParameter("Exception", entity.Exception); +// // await cmd.ExecuteNonQueryAsync(); +// // } +// //} + + +// //public async Task FindByErrorIdAsync(string errorId) +// //{ +// // using (var cmd = (DbCommand)_uow.CreateCommand()) +// // { +// // cmd.CommandText = +// // "SELECT * FROM ErrorReports WHERE ErrorId = @id"; + +// // cmd.AddParameter("id", errorId); +// // return await cmd.FirstOrDefaultAsync(); +// // } +// //} + +// //public async Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize) +// //{ +// // using (var cmd = (DbCommand)_uow.CreateCommand()) +// // { +// // cmd.AddParameter("incidentId", incidentId); +// // long totalRows = 0; +// // if (pageNumber > 0) +// // { +// // cmd.CommandText = +// //"SELECT count(*) FROM ErrorReports WHERE IncidentId = @incidentId"; +// // totalRows = (int)await cmd.ExecuteScalarAsync(); +// // } + +// // cmd.CommandText = +// // "SELECT * FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY CreatedAtUtc DESC"; +// // if (pageNumber > 0) +// // { +// // var offset = (pageNumber - 1)*pageSize; +// // cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize); +// // } + +// // //cmd.AddParameter("incidentId", incidentId); +// // var list = await cmd.ToListAsync(); +// // return new PagedReports() +// // { +// // TotalCount = (int) totalRows, +// // Reports = (IReadOnlyList)list +// // }; +// // } +// //} + +// public async Task> GetForIncidentAsync(int incidentId) +// { +// using (var cmd = (DbCommand)_uow.CreateCommand()) +// { +// cmd.CommandText = "SELECT * FROM ErrorReports WHERE IncidentId = @id"; +// cmd.AddParameter("id", incidentId); +// return await cmd.ToListAsync(); +// } +// } +// } +//} + diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs new file mode 100644 index 00000000..ccb8a0f3 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Domain.Core.Feedback; +using Griffin.ApplicationServices; +using Griffin.Data; +using Griffin.Data.Mapper; +using log4net; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Feedback +{ + [ContainerService(RegisterAsSelf = true)] + public class LookupReportsForFeedback : IBackgroundJobAsync + { + private readonly ILog _logger = LogManager.GetLogger(typeof(LookupReportsForFeedback)); + private readonly IAdoNetUnitOfWork _unitOfWork; + + + public LookupReportsForFeedback(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task ExecuteAsync() + { + var items = new List(); + await GetPendingFeedback(items); + await LookupReportInfo(items); + foreach (var item in items) + { + if (item.CanUpdate) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + _logger.Debug("Attaching report to " + item.ReportId + " to feedback " + item.Id); + cmd.CommandText = + "UPDATE IncidentFeedback SET IncidentId=@incidentId, ApplicationId = @appId, ReportId = @reportId WHERE Id = @id;"; + cmd.AddParameter("incidentId", item.IncidentId); + cmd.AddParameter("appId", item.ApplicationId); + cmd.AddParameter("reportId", item.ReportId); + cmd.AddParameter("id", item.Id); + await cmd.ExecuteNonQueryAsync(); + } + } + + else if (item.CanRemove) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + _logger.Debug("Deleting feedback " + item.Id); + cmd.CommandText = + "DELETE FROM IncidentFeedback WHERE Id = @id;"; + cmd.AddParameter("id", item.Id); + await cmd.ExecuteNonQueryAsync(); + } + } + else + { + _logger.Debug("Paria: " + item.Id + "/" + item.ErrorId); + } + } + } + + private async Task GetPendingFeedback(ICollection items) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "SELECT * FROM incidentfeedback WHERE IncidentId is null;"; + var myItems = await cmd.ToListAsync(); + foreach (var item in myItems) + { + items.Add(item); + } + + if (items.Count > 0) + if (items.Count > 0) + _logger.Debug("Added " + items.Count + " items."); + } + } + + private async Task LookupReportInfo(IEnumerable items) + { + foreach (var item in items) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + if (item.ErrorId != null) + { + cmd.CommandText = @"SELECT IncidentReports.Id, ApplicationId, IncidentId + FROM IncidentReports + JOIN Incidents WITH(READUNCOMMITTED) ON (Incidents.Id = IncidentId) + WHERE ErrorId = @id;"; + cmd.AddParameter("id", item.ErrorId); + } + else + { + cmd.CommandText = @"SELECT Id, ApplicationId, IncidentId + FROM ErrorReports + WHERE Id = @id;"; + cmd.AddParameter("id", item.ReportId); + } + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (!await reader.ReadAsync()) + continue; + + item.AssignToReport((int) reader["Id"], + (int) reader["IncidentId"], + (int) reader["ApplicationId"]); + _logger.Debug("Identified report " + item.ReportId + "/" + item.ReportId + " for feedback " + + item.Id); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs new file mode 100644 index 00000000..668d0e33 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Coderr.Server.Domain.Core.ErrorReports; +using Coderr.Server.ReportAnalyzer; +using Coderr.Server.ReportAnalyzer.Abstractions.Incidents; +using Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs; +using Coderr.Server.PostgreSQL.Tools; +using DotNetCqs; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Handlers +{ + /// + /// Process collections that was attached to inbound reports + /// + /// + /// + /// Will convert them. + /// + /// + internal class ProcessInboundContextCollectionsHandler : IMessageHandler + { + private static int _isProcessing; + private readonly Importer _importer; + private readonly IAdoNetUnitOfWork _unitOfWork; + + public ProcessInboundContextCollectionsHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + var db = (AnalysisUnitOfWork) unitOfWork; + _importer = new Importer((SqlTransaction)db.Transaction); + } + + + public async Task HandleAsync(IMessageContext context, ProcessInboundContextCollections message) + { + // We can receive multiple reports simultaneously. + // Make sure that we only got one handler running. + if (Interlocked.CompareExchange(ref _isProcessing, 1, 0) == 1) + return; + + try + { + var collections = await GetInboundCollections(); + foreach (var collection in collections) + { + var contexts = EntitySerializer.Deserialize(collection.JsonData); + _importer.AddContextCollections(collection.ReportId, contexts); + } + + if (collections.Any()) + { + await _importer.Execute(); + await DeleteImportedRows(collections); + _importer.Clear(); + } + } + finally + { + Interlocked.Exchange(ref _isProcessing, 0); + } + } + + + private async Task DeleteImportedRows(IEnumerable collections) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + var ids = string.Join(",", collections.Select(x => x.Id).ToArray()); + var sql = $"DELETE FROM ErrorReportCollectionInbound WHERE Id IN ({ids});"; + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + } + } + + + private async Task> GetInboundCollections() + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = "SELECT TOP(50) Id, ReportId, Body FROM ErrorReportCollectionInbound;"; + return await cmd.ToListAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs new file mode 100644 index 00000000..0950c0eb --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs @@ -0,0 +1,40 @@ +using Coderr.Server.ReportAnalyzer.Incidents; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer +{ + public class IncidentBeingAnalyzedMapper : CrudEntityMapper + { + public IncidentBeingAnalyzedMapper() + : base("Incidents") + { + Property(x => x.UpdatedAtUtc) + .ToPropertyValue(DbConverters.ToEntityDate) + .ToColumnValue(DbConverters.ToNullableSqlDate); + + Property(x => x.PreviousSolutionAtUtc) + .ToPropertyValue(DbConverters.ToEntityDate) + .ToColumnValue(DbConverters.ToNullableSqlDate); + + Property(x => x.ReOpenedAtUtc) + .ToPropertyValue(DbConverters.ToEntityDate) + .ToColumnValue(DbConverters.ToNullableSqlDate); + + Property(x => x.SolvedAtUtc) + .ToPropertyValue(DbConverters.ToEntityDate) + .ToColumnValue(DbConverters.ToNullableSqlDate); + + Property(x => x.EnvironmentNames).Ignore(); + + Property(x => x.IsClosed) + .Ignore(); + + Property(x => x.IsIgnored) + .Ignore(); + + Property(x => x.State) + .ToPropertyValue(x => (AnalyzedIncidentState) x) + .ToColumnValue(x => (int) x); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs new file mode 100644 index 00000000..76b99bed --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs @@ -0,0 +1,19 @@ +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs +{ + public class ErrorReportContextCollectionProperty + { + public int Id { get; set; } + + public int ReportId { get; set; } + + /// + /// Context collection name + /// + public string CollectionName { get; set; } + + public string PropertyName { get; set; } + + public string Value { get; set; } + } + +} diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs new file mode 100644 index 00000000..af92bb62 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs @@ -0,0 +1,12 @@ +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs +{ + class ErrorReportContextCollectionPropertyMapper : CrudEntityMapper + { + public ErrorReportContextCollectionPropertyMapper() : base("ErrorReportCollectionProperties") + { + + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/Importer.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/Importer.cs new file mode 100644 index 00000000..2f090eea --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/Importer.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Coderr.Server.Domain.Core.ErrorReports; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs +{ + internal class Importer + { + private readonly SqlTransaction _transaction; + private readonly DataTable _dataTable = new DataTable(); + private ILog _logger = LogManager.GetLogger(typeof(Importer)); + + + public Importer(SqlTransaction transaction) + { + _transaction = transaction; + + _dataTable.Columns.Add("ReportId", typeof(int)); + _dataTable.Columns.Add("Name"); + _dataTable.Columns.Add("PropertyName"); + _dataTable.Columns.Add("Value"); + } + + public void AddContextCollections(int reportId, ErrorReportContextCollection[] contexts) + { + foreach (var context in contexts) + { + if (context.Properties.Count > 300) + { + _logger.Warn($"Report {reportId}, Ignoring collection {context.Name}, since it got {context.Properties.Count} properties"); + continue; + } + + foreach (var property in context.Properties) + { + if (property.Value == null) + continue; + + var row = CreateDataTableRow(_dataTable, reportId, context, property); + _dataTable.Rows.Add(row); + } + } + } + + public async Task Execute() + { + //TODO: Remove once all processing is in a seperate library. + using (var bulkCopy = new SqlBulkCopy(_transaction.Connection, SqlBulkCopyOptions.Default, _transaction)) + { + bulkCopy.DestinationTableName = "ErrorReportCollectionProperties"; + bulkCopy.ColumnMappings.Add("ReportId", "ReportId"); + bulkCopy.ColumnMappings.Add("Name", "Name"); + bulkCopy.ColumnMappings.Add("PropertyName", "PropertyName"); + bulkCopy.ColumnMappings.Add("Value", "Value"); + await bulkCopy.WriteToServerAsync(_dataTable); + } + } + + private static DataRow CreateDataTableRow(DataTable dataTable, int reportId, + ErrorReportContextCollection context, + KeyValuePair property) + { + var contextName = context.Name.Length > 50 + ? context.Name.Substring(0, 47) + "..." + : context.Name; + var propertyName = property.Key.Length > 50 + ? property.Key.Substring(0, 47) + "..." + : property.Key; + + var row = dataTable.NewRow(); + row["ReportId"] = reportId; + row["Name"] = contextName; + row["PropertyName"] = propertyName; + row["Value"] = property.Value; + return row; + } + + public void Clear() + { + _dataTable.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollection.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollection.cs new file mode 100644 index 00000000..2f23093a --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollection.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs +{ + public class InboundCollection + { + public int Id { get; set; } + public int ReportId { get; set; } + public string JsonData { get; set; } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollectionMapper.cs b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollectionMapper.cs new file mode 100644 index 00000000..0b9b5483 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/ReportAnalyzer/Jobs/InboundCollectionMapper.cs @@ -0,0 +1,14 @@ +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.ReportAnalyzer.Jobs +{ + class InboundCollectionMapper : CrudEntityMapper + { + public InboundCollectionMapper() : base("ErrorReportCollectionInbound") + { + Property(x => x.JsonData) + .ColumnName("Body"); + + } + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql new file mode 100644 index 00000000..4b55ba08 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql @@ -0,0 +1,325 @@ +IF OBJECT_ID(N'dbo.[Settings]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Settings]( + [Section] [varchar](50) NOT NULL, + [Name] [varchar](50) NOT NULL, + [Value] [varchar](512), + ) ON [PRIMARY] + END + + +IF OBJECT_ID(N'dbo.[Accounts]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Accounts]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [UserName] [varchar](50) NOT NULL, + [HashedPassword] [varchar](512) NOT NULL, + [CreatedAtUtc] [datetime] NOT NULL, + [Email] [varchar](255) NOT NULL, + [Salt] [varchar](512) NOT NULL, + [AccountState] [varchar](20) NOT NULL, + [TrackingId] [varchar](40) NULL, + [LoginAttempts] [int] NOT NULL, + [LastLoginAtUtc] [datetime] NULL, + [ActivationKey] [varchar](50) NULL, + [PromotionCode] [varchar](50) NULL, + [UpdatedAtUtc] [datetime] NULL, + CONSTRAINT [accounts_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) + ) ON [PRIMARY] + END + + IF OBJECT_ID(N'dbo.[InvalidReports]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[InvalidReports]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [AppKey] [varchar](36) NOT NULL, + [Signature] [varchar](36) NOT NULL, + [ReportBody] [ntext] NOT NULL, + [ErrorMessage] [varchar](2000) NOT NULL, + [CreatedAtUtc] [datetime] NOT NULL, + CONSTRAINT [invalidreports_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) + ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +END + + +IF OBJECT_ID(N'dbo.[Invitations]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Invitations]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [Email] [varchar](2000) NOT NULL, + [InvitationKey] [char](32) NOT NULL, + [CreatedAtUtc] [datetime] NOT NULL, + [InvitedBy] varchar(50) NOT NULL, + [Invitations] varchar(2500) NOT NULL, + + CONSTRAINT [invitations_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) + ) ON [PRIMARY] +END + +IF OBJECT_ID(N'dbo.[Applications]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Applications] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [Name] NVARCHAR (50) NOT NULL, + [AppKey] VARCHAR (36) NOT NULL, + [CreatedById] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [ApplicationType] VARCHAR (40) NOT NULL, + [SharedSecret] VARCHAR (36) NOT NULL + ); +END + +IF OBJECT_ID(N'dbo.[CollectionMetadata]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[CollectionMetadata] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [Name] NVARCHAR (50) NOT NULL, + [ApplicationId] INT NOT NULL, + [Properties] NTEXT NOT NULL + ); +END + +IF OBJECT_ID(N'dbo.[ErrorOrigins]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[ErrorOrigins] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [IpAddress] VARCHAR (20) NOT NULL, + [CountryCode] VARCHAR (5) NULL, + [CountryName] VARCHAR (30) NULL, + [RegionCode] VARCHAR (5) NULL, + [RegionName] VARCHAR (30) NULL, + [City] NVARCHAR (30) NULL, + [ZipCode] VARCHAR (10) NULL, + [Latitude] DECIMAL (9, 6) NOT NULL, + [Longitude] DECIMAL (9, 6) NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL + ); +END + +IF OBJECT_ID(N'dbo.[ErrorReportOrigins]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[ErrorReportOrigins] ( + [ErrorOriginId] INT NOT NULL, + [IncidentId] INT NOT NULL, + [ReportId] INT NOT NULL, + [ApplicationId] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL + ); +END + + +IF OBJECT_ID(N'dbo.[ErrorReports]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[ErrorReports] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [IncidentId] INT NOT NULL, + [ErrorId] VARCHAR (36) NOT NULL, + [ApplicationId] INT NOT NULL, + [ReportHashCode] VARCHAR (20) NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [SolvedAtUtc] DATETIME NULL, + [Title] NVARCHAR(100) NULL, + [RemoteAddress] VARCHAR (45) NULL, --SEE http://stackoverflow.com/questions/166132/maximum-length-of-the-textual-representation-of-an-ipv6-address + [Exception] NTEXT NOT NULL, + [ContextInfo] NTEXT NOT NULL + ); +END + + +IF OBJECT_ID(N'dbo.[ErrorReports_IncidentId]', N'I') IS NULL +BEGIN + CREATE NONCLUSTERED INDEX [ErrorReports_IncidentId] + ON [dbo].[ErrorReports]([IncidentId] ASC, [CreatedAtUtc] DESC); +END + +IF OBJECT_ID(N'dbo.[Application_GetWeeklyStats]', N'I') IS NULL +BEGIN + CREATE NONCLUSTERED INDEX [Application_GetWeeklyStats] + ON [dbo].[ErrorReports]([ApplicationId] ASC, [CreatedAtUtc] DESC); +END + +IF OBJECT_ID(N'dbo.[IncidentFeedback]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[IncidentFeedback] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [ApplicationId] INT NULL, + [IncidentId] INT NULL, + [ReportId] INT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [RemoteAddress] VARCHAR (20) NOT NULL, + [Description] NTEXT NOT NULL, + [EmailAddress] NVARCHAR (512) NULL, + [Conversation] NTEXT NOT NULL, + [ConversationLength] INT NOT NULL, + [ErrorReportId] VARCHAR (40) NOT NULL, + [Replied] INT NOT NULL default 0 + ); +END + + +IF OBJECT_ID(N'dbo.[Incidents]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Incidents] ( + [Id] INT IDENTITY (1, 1) NOT NULL, + [ReportHashCode] VARCHAR (20) NOT NULL, + [ApplicationId] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [HashCodeIdentifier] VARCHAR (1024) NOT NULL, + [ReportCount] INT NOT NULL, + [UpdatedAtUtc] DATETIME NULL, + [Description] NTEXT NOT NULL, + [FullName] NVARCHAR (255) NOT NULL, + [Solution] NTEXT NULL, + [IsSolved] BINARY (1) NOT NULL default(0), + [IsSolutionShared] BINARY (1) NOT NULL default(0), + [SolvedAtUtc] DATETIME NULL, + [StackTrace] NTEXT NULL, + [IsReOpened] BIT NOT NULL default(0), + [ReOpenedAtUtc] DATETIME NULL, + [PreviousSolutionAtUtc] DATETIME NULL, + [IgnoreReports] BIT NOT NULL default(0), + [IgnoringReportsSinceUtc] DATETIME NULL, + [IgnoringRequestedBy] NVARCHAR (50) NULL, + [LastSolutionAtUtc] DATETIME NULL + ); +END + +IF OBJECT_ID(N'dbo.[IncidentTags]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[IncidentTags] ( + [id] INT IDENTITY (1, 1) NOT NULL primary key, + [IncidentId] INT NOT NULL, + [TagName] VARCHAR (40) NOT NULL, + [OrderNumber] INT NOT NULL + ); +END + + +IF OBJECT_ID(N'dbo.[IncidentTags_FromIncident]', N'U') IS NULL +BEGIN + CREATE NONCLUSTERED INDEX [IncidentTags_FromIncident] + ON [dbo].[IncidentTags]([IncidentId] ASC, [OrderNumber] ASC); +END + +IF OBJECT_ID(N'dbo.[ReportContextInfo]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[ReportContextInfo] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [IncidentId] INT NOT NULL, + [ReportId] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [UpdatedAtUtc] DATETIME NULL, + [Name] NVARCHAR (1024) NULL, + [Value] NVARCHAR (20) NULL, + [LargeValue] NTEXT NOT NULL + ); +END + +IF OBJECT_ID(N'dbo.[IncidentContextCollections]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[IncidentContextCollections] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [IncidentId] INT NOT NULL, + [Name] VARCHAR (250) NOT NULL, + [Properties] text NOT NULL + ); +END + +IF OBJECT_ID(N'dbo.[IncidentContextCollections_IncidentId]', N'U') IS NULL +BEGIN + CREATE NONCLUSTERED INDEX [IncidentContextCollections_IncidentId] + ON [dbo].[IncidentContextCollections]([IncidentId] ASC); +END + + +IF OBJECT_ID(N'dbo.[Triggers]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Triggers] ( + [Id] INT IDENTITY (1, 1) NOT NULL primary key, + [Name] NVARCHAR (50) NOT NULL, + [Description] NVARCHAR (512) NOT NULL, + [ApplicationId] INT NOT NULL, + [Rules] NTEXT NOT NULL, + [Actions] NTEXT NOT NULL, + [LastTriggerAction] NVARCHAR (50) NOT NULL, + [RunForNewIncidents] BIT NOT NULL, + [RunForExistingIncidents] BIT NOT NULL, + [RunForReOpenedIncidents] BIT NOT NULL + ); +END + + +IF OBJECT_ID(N'dbo.[UserNotificationSettings]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[UserNotificationSettings] ( + [AccountId] INT NOT NULL, + [ApplicationId] INT NOT NULL, + [NewIncident] VARCHAR (20) NOT NULL default 'Disabled', + [NewReport] VARCHAR (20) NOT NULL default 'Disabled', + [ReOpenedIncident] VARCHAR (20) NOT NULL default 'Disabled', + [WeeklySummary] VARCHAR (20) NOT NULL default 'Disabled', + [ApplicationSpike] VARCHAR (20) NOT NULL default 'Disabled', + [UserFeedback] VARCHAR (20) NOT NULL default 'Disabled' + ); +END +ALTER TABLE [UserNotificationSettings] + ADD CONSTRAINT pk_UserNotificationSettings PRIMARY KEY (AccountId, ApplicationId); + +CREATE TABLE dbo.Users +( + AccountId INT NOT NULL primary key, + EmailAddress varchar(255) not null, + FirstName varchar(100), + LastName varchar(100), + UserName varchar(100), + MobileNumber varchar(100) + ); + + +IF OBJECT_ID(N'dbo.[ApplicationMembers]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[ApplicationMembers] ( + [AccountId] INT NULL foreign key references Accounts (Id), + [ApplicationId] INT NOT NULL foreign key references Applications (Id), + [EmailAddress] nvarchar(255) not null, + [AddedAtUtc] DATETIME NOT NULL, + [AddedByName] VARCHAR (50) NOT NULL, + [Roles] VARCHAR (255) NOT NULL + ); +END + +IF OBJECT_ID(N'dbo.[QueueEvents]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[QueueEvents] ( + [Id] INT identity not NULL primary key, + [ApplicationId] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, + [Body] text NOT NULL, + ); +END + +IF OBJECT_ID(N'dbo.[QueueReports]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[QueueReports] ( + [Id] INT identity not NULL primary key, + [ApplicationId] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, + [Body] text NOT NULL, + + ); +END + + +IF OBJECT_ID(N'dbo.[QueueFeedback]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[QueueFeedback] ( + [Id] INT identity not NULL primary key, + [ApplicationId] INT NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, + [Body] text NOT NULL, + + ); +END diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql new file mode 100644 index 00000000..964b7434 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql @@ -0,0 +1,18 @@ +IF OBJECT_ID(N'dbo.[ApiKeys]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[ApiKeys] ( + [Id] INT identity not NULL primary key, + [ApplicationName] varchar(40) NOT NULL, + [CreatedAtUtc] DATETIME NOT NULL, + [CreatedById] int NOT NULL, + [GeneratedKey] varchar(36) NOT NULL, + [SharedSecret] varchar(36) NOT NULL + ); + CREATE TABLE [dbo].[ApiKeyApplications] ( + [ApiKeyId] INT not NULL, + [ApplicationId] INT NOT NULL, + Primary key (ApiKeyId, ApplicationId), + FOREIGN KEY (ApiKeyId) REFERENCES ApiKeys(Id) ON DELETE CASCADE, + FOREIGN KEY (ApplicationId) REFERENCES Applications(Id) ON DELETE NO ACTION + ); +END diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql new file mode 100644 index 00000000..cde927e6 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql @@ -0,0 +1,41 @@ +ALTER TABLE Incidents ADD PRIMARY KEY (Id); +ALTER TABLE ErrorReportOrigins WITH CHECK ADD CONSTRAINT FK_ErrorReportOrigins_Reports FOREIGN KEY (ReportId) REFERENCES ErrorReports (Id) ON DELETE CASCADE; +ALTER TABLE ErrorReports WITH CHECK ADD CONSTRAINT FK_ErrorReports_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +ALTER TABLE CollectionMetadata WITH CHECK ADD CONSTRAINT FK_COLME_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +ALTER TABLE IncidentFeedback WITH CHECK ADD CONSTRAINT FK_IncidentFeedback_incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +ALTER TABLE Incidents WITH CHECK ADD CONSTRAINT FK_Incidents_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +ALTER TABLE IncidentTags WITH CHECK ADD CONSTRAINT FK_IncidentTags_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +ALTER TABLE ReportContextInfo WITH CHECK ADD CONSTRAINT FK_ReportContextInfo_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +ALTER TABLE IncidentContextCollections WITH CHECK ADD CONSTRAINT FK_ICC_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +ALTER TABLE [UserNotificationSettings] WITH CHECK ADD CONSTRAINT FK_UNS_accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; +ALTER TABLE [Triggers] WITH CHECK ADD CONSTRAINT FK_Triggers_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; + +DECLARE @ConstraintName nvarchar(200) +SELECT @ConstraintName = KCU.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU + ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +WHERE + KCU.TABLE_NAME = 'ApplicationMembers' AND + KCU.COLUMN_NAME = 'AccountId' +IF @ConstraintName IS NOT NULL +BEGIN + EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) +END; +SELECT @ConstraintName = KCU.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU + ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +WHERE + KCU.TABLE_NAME = 'ApplicationMembers' AND + KCU.COLUMN_NAME = 'ApplicationId' +IF @ConstraintName IS NOT NULL +BEGIN + EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) +END +ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; +ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql new file mode 100644 index 00000000..be6918ec --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql @@ -0,0 +1,5 @@ +--version 1.0 (part A) of OneTrueError + +ALTER TABLE Accounts ADD IsSysAdmin bit not null default 0; +alter table ApplicationMembers add Id int identity not null primary key; +ALTER TABLE ApplicationMembers ALTER COLUMN [EmailAddress] nvarchar(255) null; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql new file mode 100644 index 00000000..36d1a4c5 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql @@ -0,0 +1,3 @@ +--version 1.0 of OneTrueError +--a split was required due to updating created column +UPDATE Accounts SET IsSysAdmin = 1 WHERE Id = (SELECT TOP 1 Id FROM ACCOUNTS ORDER BY Id); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql new file mode 100644 index 00000000..258123df --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql @@ -0,0 +1,39 @@ +create table ApplicationVersions +( + Id int not null identity primary key, + ApplicationId int not null foreign key references Applications (Id), + ApplicationName varchar(40) not null, + FirstReportDate datetime not null, + LastReportDate datetime not null, + Version varchar(10) not null +); + +create table ApplicationVersionMonths +( + Id int not null identity primary key, + VersionId int not null foreign key references ApplicationVersions (Id), + YearMonth date not null, + IncidentCount int not null, + ReportCount int not null, + LastUpdateAtUtc datetime not null +); + +create table IncidentVersions +( + IncidentId int not null constraint FK_IncidentVersions_Incidents references Incidents(Id), + VersionId int not null constraint FK_IncidentVersions_ApplicationVersions references ApplicationVersions(Id) +); + +IF COL_LENGTH('dbo.Incidents', 'StackTrace') IS NULL +BEGIN +ALTER TABLE Incidents ADD StackTrace varchar(MAX); + +UPDATE Incidents + SET StackTrace = ( + SELECT TOP (1) Substring(Exception, + CHARINDEX('"StackTrace":"', Exception) + 14, + DATALENGTH(exception)-CHARINDEX('"StackTrace":"', Exception) - 14 - 1) + FROM ErrorReports + WHERE IncidentId = Incidents.Id) + +END diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql new file mode 100644 index 00000000..14f3e4a8 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql @@ -0,0 +1,12 @@ +CREATE TABLE MessageQueue +( + Id int not null identity primary key, + QueueName varchar(40) not null, + CreatedAtUtc datetime not null, + MessageType varchar(512) not null, + Body nvarchar(MAX) not null +); + +DROP TABLE QueueEvents; +DROP TABLE QueueFeedback; +DROP TABLE QueueReports; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql new file mode 100644 index 00000000..cfcf1a52 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql @@ -0,0 +1,34 @@ +ALTER TABLE Incidents ADD State int not null default(0); +ALTER TABLE Incidents ADD AssignedToId int; +ALTER TABLE Incidents ADD AssignedAtUtc datetime; +ALTER TABLE Incidents ADD LastReportAtUtc datetime; + +DECLARE @ConstraintName nvarchar(200) +SELECT @ConstraintName = d.Name + FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c + WHERE c.name = 'IsSolved' + AND c.object_id = OBJECT_ID(N'Incidents') + AND d.PARENT_COLUMN_ID = c.column_id +EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName) +alter table Incidents drop column IsSolved; + +SELECT @ConstraintName = d.Name + FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c + WHERE c.name = 'IgnoreReports' + AND c.object_id = OBJECT_ID(N'Incidents') + AND d.PARENT_COLUMN_ID = c.column_id +EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName) +alter table Incidents drop column IgnoreReports; + +-- ApiKey module deletes relations manually. +SELECT + @ConstraintName = KCU.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU + ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +WHERE + KCU.TABLE_NAME = 'ApiKeyApplications' AND + KCU.COLUMN_NAME = 'ApplicationId' +IF @ConstraintName IS NOT NULL EXEC('alter table ApiKeyApplications drop CONSTRAINT ' + @ConstraintName) diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql new file mode 100644 index 00000000..18b10340 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql @@ -0,0 +1,64 @@ +DECLARE @ConstraintName nvarchar(200) +SELECT @ConstraintName = KCU.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU + ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +WHERE + KCU.TABLE_NAME = 'ApplicationVersionMonths' AND + KCU.COLUMN_NAME = 'VersionId' +IF @ConstraintName IS NOT NULL +BEGIN + EXEC('ALTER TABLE ApplicationVersionMonths DROP CONSTRAINT ' + @ConstraintName) +END; +ALTER TABLE ApplicationVersionMonths WITH CHECK ADD CONSTRAINT FK_ApplicationVersionMonths_ApplicationVersions FOREIGN KEY (VersionId) REFERENCES ApplicationVersions (Id) ON DELETE CASCADE; + + + +SELECT @ConstraintName = KCU.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU + ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +WHERE + KCU.TABLE_NAME = 'ApplicationVersions' AND + KCU.COLUMN_NAME = 'ApplicationId' +IF @ConstraintName IS NOT NULL +BEGIN + EXEC('ALTER TABLE ApplicationVersions DROP CONSTRAINT ' + @ConstraintName) +END; +ALTER TABLE ApplicationVersions WITH CHECK ADD CONSTRAINT FK_ApplicationVersions_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; + +SELECT @ConstraintName = KCU.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU + ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +WHERE + KCU.TABLE_NAME = 'IncidentVersions' AND + KCU.COLUMN_NAME = 'IncidentId' +IF @ConstraintName IS NOT NULL +BEGIN + EXEC('ALTER TABLE IncidentVersions DROP CONSTRAINT ' + @ConstraintName) +END; +ALTER TABLE IncidentVersions WITH CHECK ADD CONSTRAINT FK_IncVersions_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; + +create table ErrorReportCollectionProperties +( + Id int identity not null primary key, + ReportId int not null constraint FK_ErrorReportCollectionProperties_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE, + Name varchar(50) not null, + PropertyName varchar(50) not null, + Value nvarchar(MAX) not null +); + + +create table ErrorReportCollectionInbound +( + Id int identity not null primary key, + ReportId int not null constraint FK_ErrorReportCollectionInbound_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE, + Body nvarchar(max) not null +); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql new file mode 100644 index 00000000..40b351f8 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql @@ -0,0 +1,18 @@ +create table IncidentEnvironments +( + Id int not null identity primary key, + IncidentId int not null constraint FK_IncidentEnvironment_Incidents REFERENCES Incidents(Id) ON DELETE CASCADE, + EnvironmentName varchar(50) not null +); + +create table IncidentHistory +( + Id int not null identity primary key, + IncidentId int not null constraint FK_IncidentHistory_Incidents REFERENCES Incidents(Id) ON DELETE CASCADE, + CreatedAtUtc datetime not null, + AccountId int NULL, -- for system entries + State int not null, + ApplicationVersion varchar(40) NULL -- for action where version is not related to the action +); +alter table Incidents add IgnoredUntilVersion varchar(20) null; +CREATE NONCLUSTERED INDEX IX_IncidentHistory_Incidents ON dbo.IncidentHistory (IncidentId); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v11.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v11.sql new file mode 100644 index 00000000..72a7800d --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v11.sql @@ -0,0 +1 @@ +--Purpose of this script is just to get in phase with Live and OnPrem diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v12.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v12.sql new file mode 100644 index 00000000..72a7800d --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v12.sql @@ -0,0 +1 @@ +--Purpose of this script is just to get in phase with Live and OnPrem diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql new file mode 100644 index 00000000..efa6bac9 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql @@ -0,0 +1,8 @@ +create table ErrorReportSpikes +( + Id int identity not null primary key, + ApplicationId int not null constraint FK_ErrorReportSpikes_Applications REFERENCES Applications(Id) ON DELETE CASCADE, + SpikeDate datetime not null, + Count int not null, + NotifiedAccounts varchar(max) not null +); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql new file mode 100644 index 00000000..39e970e9 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql @@ -0,0 +1 @@ +alter table ApplicationVersions alter column Version varchar(20) not null; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql new file mode 100644 index 00000000..da505608 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql @@ -0,0 +1,10 @@ +create Table IgnoredReports +( + Id int not null identity primary key, + NumberOfReports int not null, + Date datetime not null +); + +alter table Applications add NumberOfFtes decimal; +alter table Applications add [EstimatedNumberOfErrors] int; +alter table Applications add [MuteStatisticsQuestion] bit not null default 0; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql new file mode 100644 index 00000000..276ee38b --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql @@ -0,0 +1 @@ +alter table applications alter column NumberOfFtes decimal(4,1) null; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql new file mode 100644 index 00000000..8e697cd4 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql @@ -0,0 +1,12 @@ +create table Environments +( + Id int identity not null primary key, + Name varchar(100) not null, +); + +drop table IncidentEnvironments; +CREATE TABLE IncidentEnvironments +( + IncidentId int not null constraint FK_IncidentEnvironment_Incident foreign key references Incidents(Id) ON DELETE CASCADE, + EnvironmentId int not null constraint FK_IncidentEnvironment_Environments foreign key references Environments(Id), +); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql new file mode 100644 index 00000000..0b472bcb --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql @@ -0,0 +1,11 @@ +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name='IDX_ErrorReportCollectionProperties_ReportId' +AND object_id = OBJECT_ID('ErrorReportCollectionProperties')) +begin + CREATE INDEX IDX_ErrorReportCollectionProperties_ReportId + ON ErrorReportCollectionProperties (ReportId); + + CREATE INDEX IDX_ErrorReportCollectionInbound_ReportId + ON ErrorReportCollectionInbound (ReportId); + +end + diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql new file mode 100644 index 00000000..88755c7c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql @@ -0,0 +1,12 @@ +CREATE TABLE IncidentReports +( + Id int not null identity primary key, + IncidentId int not null constraint FK_IncidentReports_Incidents foreign key references Incidents(Id) on delete cascade, + ReceivedAtUtc datetime not null, + ErrorId varchar(40) not null +); +create index IDX_IncidentReports_IncidentId ON IncidentReports (IncidentId, ReceivedAtUtc); + +insert into IncidentReports (IncidentId, ReceivedAtUtc, ErrorId) +SELECT IncidentId, CreatedAtUtc, ErrorId +FROM ErrorReports; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql new file mode 100644 index 00000000..b8c7af2e --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql @@ -0,0 +1,14 @@ +create table CorrelationIds +( + Id int identity not null primary key, + Value varchar(40) not null +); + +create table IncidentCorrelations +( + Id int identity not null primary key, + CorrelationId int not null constraint FK_IncidentCorrelations_CorrelationIds foreign key references CorrelationIds(Id), + IncidentId int not null constraint FK_IncidentCorrelations_Incidents foreign key references Incidents(Id) ON DELETE CASCADE +); + +create unique index IDX_IncidentCorrelations_Pair ON IncidentCorrelations(CorrelationId, IncidentId); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/CoderrMigrationPointer.cs b/src/Server/Coderr.Server.PostgreSQL/Schema/CoderrMigrationPointer.cs new file mode 100644 index 00000000..25833195 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/CoderrMigrationPointer.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.PostgreSQL.Schema +{ + public class CoderrMigrationPointer + { + } +} diff --git a/src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs b/src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs new file mode 100644 index 00000000..2ce7c818 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs @@ -0,0 +1,76 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics.CodeAnalysis; +using Coderr.Server.Abstractions; +using Coderr.Server.Infrastructure; +using Coderr.Server.PostgreSQL.Migrations; +using Coderr.Server.PostgreSQL.Schema; + +namespace Coderr.Server.PostgreSQL +{ + /// + /// MS Sql Server specific implementation of the database tools. + /// + /// + /// + /// These tools should only be used during setup and updates. + /// + /// + public class PostgreSQLTools : ISetupDatabaseTools + { + private readonly Func _connectionFactory; + private readonly MigrationRunner _schemaManager; + + public PostgreSQLTools(Func connectionFactory) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + _schemaManager = new MigrationRunner(_connectionFactory, "Coderr", typeof(CoderrMigrationPointer).Namespace); + } + + /// + /// Checks if the tables exists and are for the current DB schema. + /// + public bool IsTablesInstalled() + { + try + { + using (var con = _connectionFactory()) + { + using (var cmd = con.CreateCommand()) + { + cmd.CommandText = "SELECT OBJECT_ID(N'dbo.[Accounts]', N'U');"; + var result = cmd.ExecuteScalar(); + + //null for SQL Express and DbNull for SQL Server + return result != null && !(result is DBNull); + } + } + } + catch (Exception) + { + return false; + } + + } + + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", + Justification = "Installation import = control over SQL")] + public void CreateTables() + { + _schemaManager.Run(); + } + + IDbConnection ISetupDatabaseTools.OpenConnection() + { + return _connectionFactory(); + } + + public void TestConnection(string connectionString) + { + var con = new SqlConnection(connectionString); + con.Open(); + con.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Tools/ConnectionStringHelper.cs b/src/Server/Coderr.Server.PostgreSQL/Tools/ConnectionStringHelper.cs new file mode 100644 index 00000000..c9b9fd3f --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Tools/ConnectionStringHelper.cs @@ -0,0 +1,23 @@ +//using System; +//using System.Configuration; + +//namespace Coderr.Server.PostgreSQL.Tools +//{ +// public static class ConnectionStringHelper +// { +// public static ConnectionStringSettings GetConnectionString() +// { +// var connectionStringName = ConfigurationManager.AppSettings["ConnectionStringName"] ?? "Db"; + +// var connectionString = ConfigurationManager.ConnectionStrings[connectionStringName]; + +// var environmentConnectionString = Environment.GetEnvironmentVariable("coderr_ConnectionString"); +// if (!string.IsNullOrEmpty(environmentConnectionString)) +// { +// connectionString = new ConnectionStringSettings(connectionStringName, environmentConnectionString, connectionString.ProviderName); +// } + +// return connectionString; +// } +// } +//} diff --git a/src/Server/Coderr.Server.PostgreSQL/Tools/DbCommandExtensions.cs b/src/Server/Coderr.Server.PostgreSQL/Tools/DbCommandExtensions.cs new file mode 100644 index 00000000..2bd986b4 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Tools/DbCommandExtensions.cs @@ -0,0 +1,23 @@ +using System.Data; +using System.Diagnostics.CodeAnalysis; + +namespace Coderr.Server.PostgreSQL.Tools +{ + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", + Justification = "Invoker have control over the CommandText.")] + public static class DbCommandExtensions + { + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public static void Limit(this IDbCommand cmd, int count) + { + cmd.CommandText += string.Format(" OFFSET 0 ROWS FETCH NEXT {0} ROWS ONLY", count); + } + + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public static void Paging(this IDbCommand cmd, int pageNumber, int pageSize) + { + var offset = (pageNumber - 1)*pageSize; + cmd.CommandText += string.Format(" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Tools/EntitySerializer.cs b/src/Server/Coderr.Server.PostgreSQL/Tools/EntitySerializer.cs new file mode 100644 index 00000000..ccef2b72 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Tools/EntitySerializer.cs @@ -0,0 +1,47 @@ +using System; +using Coderr.Server.Infrastructure.Messaging; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Coderr.Server.PostgreSQL.Tools +{ + /// + /// Used for serialization of DB entities (typically value objects or child entities)v. + /// + public class EntitySerializer + { + /// + /// + /// + /// DBNull or string + /// + public static T Deserialize(object json) + { + if (json is DBNull) + return default(T); + + var settings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new IncludeNonPublicMembersContractResolver(), + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor + }; + //settings.Converters.Add(new DomainTriggerRuleJsonConverter()); + settings.Converters.Add(new StringEnumConverter()); + return JsonConvert.DeserializeObject((string) json, settings); + } + + public static string Serialize(object dto) + { + var jsonSerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new IncludeNonPublicMembersContractResolver(), + TypeNameHandling = TypeNameHandling.Objects + }; + jsonSerializerSettings.Converters.Add(new StringEnumConverter()); + return JsonConvert.SerializeObject(dto, typeof(object), + jsonSerializerSettings); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs b/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs new file mode 100644 index 00000000..43fba7e2 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs @@ -0,0 +1,73 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.PostgreSQL +{ + /// + /// Required for background jobs which uses + /// + public class UnitOfWorkWithTransaction : IAdoNetUnitOfWork + { + private ILog _logger = LogManager.GetLogger(typeof(UnitOfWorkWithTransaction)); + private SqlCommand _lastCommand; + + + public UnitOfWorkWithTransaction(SqlTransaction transaction) + { + Transaction = transaction; + } + + public SqlTransaction Transaction { get; private set; } + + public void Dispose() + { + if (Transaction == null) + return; + + if (_lastCommand != null) + _logger.Debug($"Rolling back {GetHashCode()}, last command: {_lastCommand.CommandText}"); + else + _logger.Info($"Rolling back {GetHashCode()}"); + + var connection = Transaction.Connection; + Transaction.Rollback(); + Transaction.Dispose(); + Transaction = null; + + connection?.Dispose(); + _logger.Info("Rolled back " + GetHashCode()); + } + + public void SaveChanges() + { + // Already committed. + // some scenarios requires early SaveChanges + // to prevent dead locks. + // when there is time, find and eliminate the reason of the deadlocks :( + if (Transaction == null) + return; + + var connection = Transaction.Connection; + Transaction.Commit(); + Transaction.Dispose(); + Transaction = null; + connection.Dispose(); + } + + public IDbCommand CreateCommand() + { + var cmd = Transaction.Connection.CreateCommand(); + cmd.Transaction = Transaction; + _lastCommand = cmd; + return cmd; + } + + public void Execute(string sql, object parameters) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs new file mode 100644 index 00000000..b4febc2e --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Web.Feedback.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Web.Feedback.Queries +{ + public class GetApplicationFeedbackHandler : + IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetApplicationFeedbackHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetFeedbackForApplicationPage query) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = + @"select IncidentFeedback.Description Message, IncidentFeedback.EmailAddress, IncidentFeedback.CreatedAtUtc as WrittenAtUtc, + Incidents.Description as IncidentName, IncidentId +from IncidentFeedback +join Incidents on (IncidentId = Incidents.Id) +WHERE IncidentFeedback.ApplicationId = @appId; +"; + cmd.AddParameter("appId", query.ApplicationId); + var items = await cmd.ToListAsync(); + return new GetFeedbackForApplicationPageResult + { + Items = items.Where(x => !string.IsNullOrEmpty(x.Message)).ToArray(), + Emails = + items.Where(x => !string.IsNullOrEmpty(x.EmailAddress)).Select(x => x.EmailAddress).ToList() + }; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs new file mode 100644 index 00000000..0c21a838 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs @@ -0,0 +1,26 @@ +using Coderr.Server.Api.Web.Feedback.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Web.Feedback.Queries +{ + public class GetApplicationFeedbackResultItemMapper : EntityMapper + { + public GetApplicationFeedbackResultItemMapper() + { + Property(x => x.EmailAddress); + Property(x => x.IncidentId); + Property(x => x.IncidentName) + .NotForCrud() + .ToPropertyValue(FirstLine); + Property(x => x.Message); + Property(x => x.WrittenAtUtc); + } + + private string FirstLine(object arg) + { + var str = arg.ToString(); + var pos = str.IndexOfAny(new[] {'\r', '\n'}); + return pos != -1 ? str.Substring(0, pos) : str; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs new file mode 100644 index 00000000..778614c0 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Coderr.Server.Api.Web.Feedback.Queries; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Web.Feedback.Queries +{ + public class GetIncidentFeedbackHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetIncidentFeedbackHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetIncidentFeedback query) + { + using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) + { + cmd.CommandText = + "SELECT IncidentId, IncidentFeedback.CreatedAtUtc, EmailAddress, IncidentFeedback.Description" + + " FROM IncidentFeedback" + + " JOIN Incidents ON (Incidents.Id = IncidentFeedback.IncidentId)" + + " WHERE IncidentId = @id"; + cmd.AddParameter("id", query.IncidentId); + cmd.CommandText += " ORDER BY IncidentFeedback.CreatedAtUtc DESC;"; + + using (var reader = await cmd.ExecuteReaderAsync()) + { + var emails = new List(); + var items = new List(); + while (reader.Read()) + { + var description = Convert.ToString(reader["Description"]); + var email = Convert.ToString(Convert.ToString(reader["EmailAddress"])); + if (!string.IsNullOrEmpty(description)) + { + var item = new GetIncidentFeedbackResultItem(); + item.EmailAddress = email; + item.Message = description; + item.WrittenAtUtc = (DateTime) reader["CreatedAtUtc"]; + items.Add(item); + } + if (!string.IsNullOrEmpty(email) && !emails.Contains(email)) + { + emails.Add(email); + } + } + + return new GetIncidentFeedbackResult(items, emails); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs new file mode 100644 index 00000000..ea152ee8 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Web.Feedback.Queries; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; +using Coderr.Server.ReportAnalyzer.Abstractions; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Web.Feedback.Queries +{ + [ContainerService] + public class GetOverviewFeedbackHandler : + IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetOverviewFeedbackHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetFeedbackForDashboardPage query) + { + using (var cmd = _unitOfWork.CreateCommand()) + { + var sql = + @"select IncidentFeedback.Description Message, IncidentFeedback.EmailAddress, IncidentFeedback.CreatedAtUtc as WrittenAtUtc, + ApplicationId, Applications.Name ApplicationName + from IncidentFeedback + join Applications on (ApplicationId = Applications.Id) + WHERE ApplicationId IN ({0});"; + + // roundtrip to int to prevent + // something getting a string into our claims + // since the code below would otherwise + // allow SQL injection + var appIds = context.Principal + .FindAll(x => x.Type == CoderrClaims.Application) + .Select(x => int.Parse(x.Value).ToString()) + .ToList(); + if (appIds.Count == 0) + { + return new GetFeedbackForDashboardPageResult + { + Items = new GetFeedbackForDashboardPageResultItem[0], + Emails = new List(), + TotalCount = 0 + }; + } + + cmd.CommandText = sql.Replace("{0}", string.Join(",", appIds)); + //WHERE Description is not null AND datalength(message) > 0"; + var items = await cmd.ToListAsync(); + return new GetFeedbackForDashboardPageResult + { + Items = items.Where(x => !string.IsNullOrEmpty(x.Message)).ToArray(), + Emails = + items.Where(x => !string.IsNullOrEmpty(x.EmailAddress)).Select(x => x.EmailAddress).ToList() + }; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs new file mode 100644 index 00000000..00d59597 --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs @@ -0,0 +1,9 @@ +using Coderr.Server.Api.Web.Feedback.Queries; +using Griffin.Data.Mapper; + +namespace Coderr.Server.PostgreSQL.Web.Feedback.Queries +{ + public class GetOverviewFeedbackResultItemMapper : EntityMapper + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Web/Overview/GetOverviewHandler.cs b/src/Server/Coderr.Server.PostgreSQL/Web/Overview/GetOverviewHandler.cs new file mode 100644 index 00000000..65caba4c --- /dev/null +++ b/src/Server/Coderr.Server.PostgreSQL/Web/Overview/GetOverviewHandler.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Web.Overview.Queries; +using Coderr.Server.Domain.Core.Incidents; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.PostgreSQL.Web.Overview +{ + internal class GetOverviewHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetOverviewHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + private DateTime StartDateForHours + { + get + { + //since we want to 22 if time is 21:30 + return DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-22); + } + } + + private string ApplicationIds { get; set; } + + public async Task HandleAsync(IMessageContext context, GetOverview query) + { + if (query.NumberOfDays == 0) + query.NumberOfDays = 30; + var labels = CreateTimeLabels(query); + + var isSysAdmin = context.Principal.IsSysAdmin(); + var gotApps = context.Principal.FindAll(x => x.Type == CoderrClaims.Application).Any(); + + if (!isSysAdmin && !gotApps) + { + return new GetOverviewResult() + { + StatSummary = new OverviewStatSummary(), + IncidentsPerApplication = new GetOverviewApplicationResult[0], + TimeAxisLabels = labels + }; + } + + if (isSysAdmin) + { + var appIds = new List(); + using (var cmd = _unitOfWork.CreateCommand()) + { + cmd.CommandText = "SELECT id FROM Applications WITH(READUNCOMMITTED);"; + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + appIds.Add(reader.GetInt32(0)); + } + } + } + ApplicationIds = string.Join(",", appIds); + } + else + { + var appIds = context.Principal + .FindAll(x => x.Type == CoderrClaims.Application) + .Select(x => int.Parse(x.Value).ToString()) + .ToList(); + ApplicationIds = string.Join(",", appIds); + } + + if (!ApplicationIds.Any()) + return new GetOverviewResult(); + + if (query.NumberOfDays == 1) + return await GetTodaysOverviewAsync(query); + + var apps = new Dictionary(); + var startDate = DateTime.Today.AddDays(-query.NumberOfDays); + var result = new GetOverviewResult(); + result.Days = query.NumberOfDays; + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = $@"select Applications.Id, Applications.Name, cte.Date, cte.Count +FROM +( + select Incidents.ApplicationId , cast(Incidents.CreatedAtUtc as date) as Date, count(Incidents.Id) as Count + from Incidents WITH(READUNCOMMITTED) + where Incidents.CreatedAtUtc >= @minDate + AND Incidents.CreatedAtUtc <= GetUtcDate() + AND Incidents.ApplicationId in ({ApplicationIds}) + group by Incidents.ApplicationId, cast(Incidents.CreatedAtUtc as date) +) cte +right join applications on (applicationid=applications.id); + +;"; + + + cmd.AddParameter("minDate", startDate); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var appId = reader.GetInt32(0); + GetOverviewApplicationResult app; + if (!apps.TryGetValue(appId, out app)) + { + app = new GetOverviewApplicationResult(reader.GetString(1), startDate, + query.NumberOfDays + 1); //+1 for today + apps[appId] = app; + } + //no stats at all for this app + if (reader[2] is DBNull) + { + var startDate2 = DateTime.Today.AddDays(-query.NumberOfDays + 1); + for (var i = 0; i < query.NumberOfDays; i++) + { + app.AddValue(startDate2.AddDays(i), 0); + } + } + else + app.AddValue(reader.GetDateTime(2), reader.GetInt32(3)); + } + + result.TimeAxisLabels = labels; + result.IncidentsPerApplication = apps.Values.ToArray(); + } + } + + using (var cmd = _unitOfWork.CreateCommand()) + { + var from = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + var to = DateTime.UtcNow; + cmd.CommandText = + "SELECT sum(NumberOfReports) FROM IgnoredReports WHERE date >= @from ANd date <= @to;"; + cmd.AddParameter("from", from); + cmd.AddParameter("to", to); + var value = cmd.ExecuteScalar(); + if (value != DBNull.Value) + result.MissedReports = (int) value; + + } + + await GetStatSummary(query, result); + + + return result; + } + + private static string[] CreateTimeLabels(GetOverview query) + { + var startDate = DateTime.Today.AddDays(-query.NumberOfDays); + var labels = new string[query.NumberOfDays + 1]; //+1 for today + for (var i = 0; i <= query.NumberOfDays; i++) + { + labels[i] = startDate.AddDays(i).ToString("yyyy-MM-dd"); + } + return labels; + } + + private async Task GetStatSummary(GetOverview query, GetOverviewResult result) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = $@"select count(id) +from incidents With(READUNCOMMITTED) +where CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND Incidents.ApplicationId IN ({ApplicationIds}) +AND Incidents.State <> {(int)IncidentState.Ignored} +AND Incidents.State <> {(int)IncidentState.Closed}; + +select count(id) +from errorreports WITH(READUNCOMMITTED) +where CreatedAtUtc >= @minDate +AND ApplicationId IN ({ApplicationIds}) + +select count(distinct emailaddress) +from IncidentFeedback WITH(READUNCOMMITTED) +where CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId IN ({ApplicationIds}) +AND emailaddress is not null +AND DATALENGTH(emailaddress) > 0; + +select count(*) +from IncidentFeedback WITH(READUNCOMMITTED) +where CreatedAtUtc >= @minDate +AND CreatedAtUtc <= GetUtcDate() +AND ApplicationId IN ({ApplicationIds}) +AND Description is not null +AND DATALENGTH(Description) > 0;"; + + var minDate = query.NumberOfDays == 1 + ? StartDateForHours + : DateTime.Today.AddDays(-query.NumberOfDays); + cmd.AddParameter("minDate", minDate); + + using (var reader = await cmd.ExecuteReaderAsync()) + { + if (!await reader.ReadAsync()) + { + throw new InvalidOperationException("Expected to be able to read."); + } + + var data = new OverviewStatSummary(); + data.Incidents = reader.GetInt32(0); + await reader.NextResultAsync(); + await reader.ReadAsync(); + data.Reports = reader.GetInt32(0); + await reader.NextResultAsync(); + await reader.ReadAsync(); + data.Followers = reader.GetInt32(0); + await reader.NextResultAsync(); + await reader.ReadAsync(); + data.UserFeedback = reader.GetInt32(0); + result.StatSummary = data; + } + } + } + + private async Task GetTodaysOverviewAsync(GetOverview query) + { + var result = new GetOverviewResult + { + TimeAxisLabels = new string[24] + }; + var startDate = StartDateForHours; + var apps = new Dictionary(); + for (var i = 0; i < 24; i++) + { + result.TimeAxisLabels[i] = startDate.AddHours(i).ToString("HH:mm"); + } + + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = string.Format(@"select Applications.Id, Applications.Name, cte.Date, cte.Count +FROM +( + select Incidents.ApplicationId , DATEPART(HOUR, Incidents.CreatedAtUtc) as Date, count(Incidents.Id) as Count + from Incidents WITH(READUNCOMMITTED) + where Incidents.CreatedAtUtc >= @minDate + AND CreatedAtUtc <= GetUtcDate() + AND Incidents.ApplicationId IN ({0}) + group by Incidents.ApplicationId, DATEPART(HOUR, Incidents.CreatedAtUtc) +) cte +right join applications WITH(READUNCOMMITTED) on (applicationid=applications.id);", ApplicationIds); + + + cmd.AddParameter("minDate", startDate); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var appId = reader.GetInt32(0); + GetOverviewApplicationResult app; + if (!apps.TryGetValue(appId, out app)) + { + app = new GetOverviewApplicationResult(reader.GetString(1), startDate, 1); + apps[appId] = app; + } + + if (reader[2] is DBNull) + { + for (var i = 0; i < 24; i++) + { + app.AddValue(startDate.AddHours(i), 0); + } + } + else + { + var hour = reader.GetInt32(2); + app.AddValue( + hour < DateTime.Now.AddHours(1).Hour //since we want 22:00 if time is 21:30 + ? DateTime.Today.AddHours(hour) + : DateTime.Today.AddDays(-1).AddHours(hour), reader.GetInt32(3)); + } + } + + result.IncidentsPerApplication = apps.Values.ToArray(); + } + } + + await GetStatSummary(query, result); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Web/appsettings.json b/src/Server/Coderr.Server.Web/appsettings.json index 28f62b11..eaaf61df 100644 --- a/src/Server/Coderr.Server.Web/appsettings.json +++ b/src/Server/Coderr.Server.Web/appsettings.json @@ -3,9 +3,9 @@ "IsConfigured": true, "Password": "changeThis" } , - "EnableCors": true, + "EnableCors": true, "ConnectionStrings": { - "Db": "Data Source=.;Initial Catalog=Coderr;Integrated Security=True;Connect Timeout=15;" + "Db": "Data Source=HB4LCP2;Initial Catalog=Coderr;Persist Security Info=True;User ID=sa;Password=1234;Connect Timeout=15;" }, "Logging": { "IncludeScopes": false, diff --git a/src/Server/Coderr.Server.Web/npm-shrinkwrap.json b/src/Server/Coderr.Server.Web/npm-shrinkwrap.json index 3bab0359..5d75b1da 100644 --- a/src/Server/Coderr.Server.Web/npm-shrinkwrap.json +++ b/src/Server/Coderr.Server.Web/npm-shrinkwrap.json @@ -3029,7 +3029,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3050,12 +3051,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3070,17 +3073,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3197,7 +3203,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3209,6 +3216,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3223,6 +3231,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3230,12 +3239,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3254,6 +3265,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3334,7 +3346,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3346,6 +3359,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3431,7 +3445,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3467,6 +3482,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3486,6 +3502,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3529,12 +3546,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -5867,7 +5886,8 @@ }, "lodash": { "version": "4.17.11", - "resolved": "", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true } } diff --git a/src/Server/Coderr.Server.sln b/src/Server/Coderr.Server.sln index ebfe203b..2637e87f 100644 --- a/src/Server/Coderr.Server.sln +++ b/src/Server/Coderr.Server.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Domain", "Cod EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Web", "Coderr.Server.Web\Coderr.Server.Web.csproj", "{C3505B24-8213-45C1-BAAE-73AAA4B75955}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.PostgreSQL", "Coderr.Server.PostgreSQL\Coderr.Server.PostgreSQL.csproj", "{81CEFF7E-EEF7-43CA-B0F1-3E1E4CD8395E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,6 +95,10 @@ Global {C3505B24-8213-45C1-BAAE-73AAA4B75955}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3505B24-8213-45C1-BAAE-73AAA4B75955}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3505B24-8213-45C1-BAAE-73AAA4B75955}.Release|Any CPU.Build.0 = Release|Any CPU + {81CEFF7E-EEF7-43CA-B0F1-3E1E4CD8395E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81CEFF7E-EEF7-43CA-B0F1-3E1E4CD8395E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81CEFF7E-EEF7-43CA-B0F1-3E1E4CD8395E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81CEFF7E-EEF7-43CA-B0F1-3E1E4CD8395E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 2f3495b8d79087b028e1d1fc32fc6aae073e7e15 Mon Sep 17 00:00:00 2001 From: Diogenes Polanco Martinez Date: Mon, 21 Oct 2019 16:06:18 -0400 Subject: [PATCH 2/3] add Npgsql nuget --- src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj | 1 + src/Server/Coderr.Server.Web/Startup.cs | 10 +++++++--- src/Server/Coderr.Server.Web/appsettings.json | 11 ++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj b/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj index ce572997..1f75d9dd 100644 --- a/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj +++ b/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Server/Coderr.Server.Web/Startup.cs b/src/Server/Coderr.Server.Web/Startup.cs index 8e28e839..1697df2f 100644 --- a/src/Server/Coderr.Server.Web/Startup.cs +++ b/src/Server/Coderr.Server.Web/Startup.cs @@ -37,6 +37,7 @@ using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Npgsql; using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; namespace Coderr.Server.Web @@ -131,7 +132,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, private void UpgradeDatabaseSchema() { // Dont run for new installations - if (!IsConfigured) + if (!IsConfigured) return; try @@ -142,7 +143,7 @@ private void UpgradeDatabaseSchema() catch (Exception ex) { _logger.Fatal("DB Migration failed.", ex); - Err.Report(ex, new {Migration = true}); + Err.Report(ex, new { Migration = true }); } } @@ -290,8 +291,11 @@ private void OnShutdown() private IDbConnection OpenConnection(ClaimsPrincipal arg) { var db = Configuration.GetConnectionString("Db"); - var con = new SqlConnection(db); + var con = new NpgsqlConnection(db); con.Open(); + + //var con = new SqlConnection(db); + //con.Open(); return con; } diff --git a/src/Server/Coderr.Server.Web/appsettings.json b/src/Server/Coderr.Server.Web/appsettings.json index eaaf61df..2af07ef8 100644 --- a/src/Server/Coderr.Server.Web/appsettings.json +++ b/src/Server/Coderr.Server.Web/appsettings.json @@ -1,11 +1,12 @@ { "Installation": { - "IsConfigured": true, - "Password": "changeThis" - } , - "EnableCors": true, + "IsConfigured": true, + "Password": "changeThis" + }, + "EnableCors": true, "ConnectionStrings": { - "Db": "Data Source=HB4LCP2;Initial Catalog=Coderr;Persist Security Info=True;User ID=sa;Password=1234;Connect Timeout=15;" + "Db": "User ID=qmpevmwl;Password=OFc9nWUvk7wlPoeZOVk-0lalb6AqPAR3;Host=salt.db.elephantsql.com;Port=5432;Database=qmpevmwl;Pooling=true;", + "Dbx": "Data Source=HB4LCP2;Initial Catalog=Coderr;Persist Security Info=True;User ID=sa;Password=1234;Connect Timeout=15;" }, "Logging": { "IncludeScopes": false, From ad8296d8261f6eaaa13f9160859322f8d704f1da Mon Sep 17 00:00:00 2001 From: Diogenes Polanco Martinez Date: Tue, 22 Oct 2019 15:25:50 -0400 Subject: [PATCH 3/3] It works but there are several scripts that are not compatible with possgressql --- .../Jobs/DeleteReportsBelowReportLimit.cs | 1 + .../Configuration/Database/DatabaseStore.cs | 4 +- .../IDatabaseUtilities.cs | 3 +- .../Coderr.Server.PostgreSQL.csproj | 1 + .../Migrations/MigrationRunner.cs | 19 +- .../{SqlServerTools.cs => PostgreSQLTools.cs} | 26 +- .../Schema/Coderr.v01.sql | 532 ++++++++---------- .../Schema/Coderr.v02.sql | 27 +- .../Schema/Coderr.v03.sql | 80 +-- .../Schema/Coderr.v04.sql | 6 +- .../Schema/Coderr.v05.sql | 2 +- .../Schema/Coderr.v06.sql | 48 +- .../Schema/Coderr.v07.sql | 14 +- .../Schema/Coderr.v08.sql | 62 +- .../Schema/Coderr.v09.sql | 114 ++-- .../Schema/Coderr.v10.sql | 16 +- .../Schema/Coderr.v13.sql | 10 +- .../Schema/Coderr.v14.sql | 2 +- .../Schema/Coderr.v15.sql | 13 +- .../Schema/Coderr.v16.sql | 2 +- .../Schema/Coderr.v17.sql | 13 +- .../Schema/Coderr.v18.sql | 16 +- .../Schema/Coderr.v19.sql | 10 +- .../Schema/Coderr.v20.sql | 14 +- ...=> UnitOfWorkPostgreSQLWithTransaction.cs} | 11 +- .../Schema/Coderr.v01.sql | 2 +- .../Coderr.Server.SqlServer/SqlServerTools.cs | 20 +- .../Controllers/AccountController.cs | 1 + .../Boot/Cqs/RegisterCqsServices.cs | 4 + .../Boot/Modules/DbConnectionConfig.cs | 11 +- .../Boot/Modules/RegisterContainerServices.cs | 4 + .../Coderr.Server.Web.csproj | 1 + src/Server/Coderr.Server.Web/Startup.cs | 20 +- src/Server/Coderr.Server.Web/appsettings.json | 8 +- 34 files changed, 533 insertions(+), 584 deletions(-) rename src/Server/Coderr.Server.PostgreSQL/{SqlServerTools.cs => PostgreSQLTools.cs} (75%) rename src/Server/Coderr.Server.PostgreSQL/{UnitOfWorkWithTransaction.cs => UnitOfWorkPostgreSQLWithTransaction.cs} (85%) diff --git a/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs b/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs index 140d6182..f58561fd 100644 --- a/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs +++ b/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs @@ -51,6 +51,7 @@ public void Execute() { using (var cmd = _connection.CreateCommand()) { + //TODO: # not support in Coderr.Server.PostgreSQL but need a procedure or view in this case. var sql = $@"CREATE TABLE #Incidents (Id int NOT NULL PRIMARY KEY, NumberOfItems int) INSERT #Incidents (Id, NumberOfItems) SELECT TOP(100) IncidentId, Count(Id) - @max diff --git a/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs b/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs index 04457be3..83979315 100644 --- a/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs +++ b/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs @@ -91,8 +91,8 @@ public override void Store(IConfigurationSection section) { cmd.CommandText += string.Format( - "INSERT INTO Settings (Section, Name, Value) VALUES(@section, @name{0}, @value{0})", - index); + "INSERT INTO Settings (Section, Name, Value) VALUES(@section, @name{0}, @value{0});", + index) ; cmd.AddParameter("name" + index, kvp.Key); cmd.AddParameter("value" + index, kvp.Value); ++index; diff --git a/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs b/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs index 3d223630..b460de38 100644 --- a/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs +++ b/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Security.Claims; namespace Coderr.Server.Infrastructure { @@ -19,7 +20,7 @@ public interface ISetupDatabaseTools /// /// Connection IDbConnection OpenConnection(); - + IDbConnection GetConnection(string connectionString, ClaimsPrincipal arg); void TestConnection(string connectionString); } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj b/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj index 281acec9..482efbeb 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj +++ b/src/Server/Coderr.Server.PostgreSQL/Coderr.Server.PostgreSQL.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs b/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs index 72c6a742..32a52d4e 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs +++ b/src/Server/Coderr.Server.PostgreSQL/Migrations/MigrationRunner.cs @@ -95,25 +95,18 @@ protected void LoadScripts() public int GetCurrentSchemaVersion() { + //TODO: its only this support in Coderr.Server.PostgreSQL. string[] scripts = new[] { - @"IF OBJECT_ID (N'DatabaseSchema', N'U') IS NULL - BEGIN - CREATE TABLE [dbo].DatabaseSchema ( - [Version] int not null default 1, - [Name] varchar(50) NOT NULL - ); - END", - - @"IF COL_LENGTH('DatabaseSchema', 'Name') IS NULL - BEGIN - ALTER TABLE DatabaseSchema ADD [Name] varchar(50) NULL; - END;", + @"CREATE TABLE IF NOT EXISTS DatabaseSchema ( + Version int not null default 1, + Name varchar(50) NOT NULL + ); ", @"UPDATE DatabaseSchema SET Name = 'coderr' WHERE Name IS NULL" }; using (var con = _connectionFactory()) - { + { foreach (var script in scripts) { using (var cmd = con.CreateCommand()) diff --git a/src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs b/src/Server/Coderr.Server.PostgreSQL/PostgreSQLTools.cs similarity index 75% rename from src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs rename to src/Server/Coderr.Server.PostgreSQL/PostgreSQLTools.cs index 2ce7c818..4d75d0f4 100644 --- a/src/Server/Coderr.Server.PostgreSQL/SqlServerTools.cs +++ b/src/Server/Coderr.Server.PostgreSQL/PostgreSQLTools.cs @@ -2,15 +2,17 @@ using System.Data; using System.Data.SqlClient; using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; using Coderr.Server.Abstractions; using Coderr.Server.Infrastructure; using Coderr.Server.PostgreSQL.Migrations; using Coderr.Server.PostgreSQL.Schema; +using Npgsql; namespace Coderr.Server.PostgreSQL { /// - /// MS Sql Server specific implementation of the database tools. + /// PostgreSQL Server specific implementation of the database tools. /// /// /// @@ -21,13 +23,20 @@ public class PostgreSQLTools : ISetupDatabaseTools { private readonly Func _connectionFactory; private readonly MigrationRunner _schemaManager; - + public PostgreSQLTools(string connectionString, ClaimsPrincipal arg) + { + _connectionFactory = () => + { + return this.GetConnection(connectionString, arg); + }; + _schemaManager = new MigrationRunner(_connectionFactory, "Coderr", typeof(CoderrMigrationPointer).Namespace); + } public PostgreSQLTools(Func connectionFactory) { _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _schemaManager = new MigrationRunner(_connectionFactory, "Coderr", typeof(CoderrMigrationPointer).Namespace); } - + /// /// Checks if the tables exists and are for the current DB schema. /// @@ -51,7 +60,7 @@ public bool IsTablesInstalled() { return false; } - + } [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", @@ -68,9 +77,16 @@ IDbConnection ISetupDatabaseTools.OpenConnection() public void TestConnection(string connectionString) { - var con = new SqlConnection(connectionString); + var con = new NpgsqlConnection(connectionString); con.Open(); con.Dispose(); } + + public IDbConnection GetConnection(string connectionString, ClaimsPrincipal arg) + { + var con = new NpgsqlConnection(connectionString); + con.Open(); + return con; + } } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql index 4b55ba08..4884864f 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v01.sql @@ -1,271 +1,191 @@ -IF OBJECT_ID(N'dbo.[Settings]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Settings]( - [Section] [varchar](50) NOT NULL, - [Name] [varchar](50) NOT NULL, - [Value] [varchar](512), - ) ON [PRIMARY] - END - - -IF OBJECT_ID(N'dbo.[Accounts]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Accounts]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [UserName] [varchar](50) NOT NULL, - [HashedPassword] [varchar](512) NOT NULL, - [CreatedAtUtc] [datetime] NOT NULL, - [Email] [varchar](255) NOT NULL, - [Salt] [varchar](512) NOT NULL, - [AccountState] [varchar](20) NOT NULL, - [TrackingId] [varchar](40) NULL, - [LoginAttempts] [int] NOT NULL, - [LastLoginAtUtc] [datetime] NULL, - [ActivationKey] [varchar](50) NULL, - [PromotionCode] [varchar](50) NULL, - [UpdatedAtUtc] [datetime] NULL, - CONSTRAINT [accounts_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) - ) ON [PRIMARY] - END - - IF OBJECT_ID(N'dbo.[InvalidReports]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[InvalidReports]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [AppKey] [varchar](36) NOT NULL, - [Signature] [varchar](36) NOT NULL, - [ReportBody] [ntext] NOT NULL, - [ErrorMessage] [varchar](2000) NOT NULL, - [CreatedAtUtc] [datetime] NOT NULL, - CONSTRAINT [invalidreports_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) - ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END - - -IF OBJECT_ID(N'dbo.[Invitations]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Invitations]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [Email] [varchar](2000) NOT NULL, - [InvitationKey] [char](32) NOT NULL, - [CreatedAtUtc] [datetime] NOT NULL, - [InvitedBy] varchar(50) NOT NULL, - [Invitations] varchar(2500) NOT NULL, - - CONSTRAINT [invitations_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) - ) ON [PRIMARY] -END - -IF OBJECT_ID(N'dbo.[Applications]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Applications] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [Name] NVARCHAR (50) NOT NULL, - [AppKey] VARCHAR (36) NOT NULL, - [CreatedById] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [ApplicationType] VARCHAR (40) NOT NULL, - [SharedSecret] VARCHAR (36) NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[CollectionMetadata]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[CollectionMetadata] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [Name] NVARCHAR (50) NOT NULL, - [ApplicationId] INT NOT NULL, - [Properties] NTEXT NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[ErrorOrigins]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ErrorOrigins] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IpAddress] VARCHAR (20) NOT NULL, - [CountryCode] VARCHAR (5) NULL, - [CountryName] VARCHAR (30) NULL, - [RegionCode] VARCHAR (5) NULL, - [RegionName] VARCHAR (30) NULL, - [City] NVARCHAR (30) NULL, - [ZipCode] VARCHAR (10) NULL, - [Latitude] DECIMAL (9, 6) NOT NULL, - [Longitude] DECIMAL (9, 6) NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[ErrorReportOrigins]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ErrorReportOrigins] ( - [ErrorOriginId] INT NOT NULL, - [IncidentId] INT NOT NULL, - [ReportId] INT NOT NULL, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[ErrorReports]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ErrorReports] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [ErrorId] VARCHAR (36) NOT NULL, - [ApplicationId] INT NOT NULL, - [ReportHashCode] VARCHAR (20) NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [SolvedAtUtc] DATETIME NULL, - [Title] NVARCHAR(100) NULL, - [RemoteAddress] VARCHAR (45) NULL, --SEE http://stackoverflow.com/questions/166132/maximum-length-of-the-textual-representation-of-an-ipv6-address - [Exception] NTEXT NOT NULL, - [ContextInfo] NTEXT NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[ErrorReports_IncidentId]', N'I') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [ErrorReports_IncidentId] - ON [dbo].[ErrorReports]([IncidentId] ASC, [CreatedAtUtc] DESC); -END - -IF OBJECT_ID(N'dbo.[Application_GetWeeklyStats]', N'I') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [Application_GetWeeklyStats] - ON [dbo].[ErrorReports]([ApplicationId] ASC, [CreatedAtUtc] DESC); -END - -IF OBJECT_ID(N'dbo.[IncidentFeedback]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[IncidentFeedback] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [ApplicationId] INT NULL, - [IncidentId] INT NULL, - [ReportId] INT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [RemoteAddress] VARCHAR (20) NOT NULL, - [Description] NTEXT NOT NULL, - [EmailAddress] NVARCHAR (512) NULL, - [Conversation] NTEXT NOT NULL, - [ConversationLength] INT NOT NULL, - [ErrorReportId] VARCHAR (40) NOT NULL, - [Replied] INT NOT NULL default 0 - ); -END - - -IF OBJECT_ID(N'dbo.[Incidents]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Incidents] ( - [Id] INT IDENTITY (1, 1) NOT NULL, - [ReportHashCode] VARCHAR (20) NOT NULL, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [HashCodeIdentifier] VARCHAR (1024) NOT NULL, - [ReportCount] INT NOT NULL, - [UpdatedAtUtc] DATETIME NULL, - [Description] NTEXT NOT NULL, - [FullName] NVARCHAR (255) NOT NULL, - [Solution] NTEXT NULL, - [IsSolved] BINARY (1) NOT NULL default(0), - [IsSolutionShared] BINARY (1) NOT NULL default(0), - [SolvedAtUtc] DATETIME NULL, - [StackTrace] NTEXT NULL, - [IsReOpened] BIT NOT NULL default(0), - [ReOpenedAtUtc] DATETIME NULL, - [PreviousSolutionAtUtc] DATETIME NULL, - [IgnoreReports] BIT NOT NULL default(0), - [IgnoringReportsSinceUtc] DATETIME NULL, - [IgnoringRequestedBy] NVARCHAR (50) NULL, - [LastSolutionAtUtc] DATETIME NULL - ); -END - -IF OBJECT_ID(N'dbo.[IncidentTags]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[IncidentTags] ( - [id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [TagName] VARCHAR (40) NOT NULL, - [OrderNumber] INT NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[IncidentTags_FromIncident]', N'U') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [IncidentTags_FromIncident] - ON [dbo].[IncidentTags]([IncidentId] ASC, [OrderNumber] ASC); -END - -IF OBJECT_ID(N'dbo.[ReportContextInfo]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ReportContextInfo] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [ReportId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [UpdatedAtUtc] DATETIME NULL, - [Name] NVARCHAR (1024) NULL, - [Value] NVARCHAR (20) NULL, - [LargeValue] NTEXT NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[IncidentContextCollections]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[IncidentContextCollections] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [Name] VARCHAR (250) NOT NULL, - [Properties] text NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[IncidentContextCollections_IncidentId]', N'U') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [IncidentContextCollections_IncidentId] - ON [dbo].[IncidentContextCollections]([IncidentId] ASC); -END - - -IF OBJECT_ID(N'dbo.[Triggers]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Triggers] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [Name] NVARCHAR (50) NOT NULL, - [Description] NVARCHAR (512) NOT NULL, - [ApplicationId] INT NOT NULL, - [Rules] NTEXT NOT NULL, - [Actions] NTEXT NOT NULL, - [LastTriggerAction] NVARCHAR (50) NOT NULL, - [RunForNewIncidents] BIT NOT NULL, - [RunForExistingIncidents] BIT NOT NULL, - [RunForReOpenedIncidents] BIT NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[UserNotificationSettings]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[UserNotificationSettings] ( - [AccountId] INT NOT NULL, - [ApplicationId] INT NOT NULL, - [NewIncident] VARCHAR (20) NOT NULL default 'Disabled', - [NewReport] VARCHAR (20) NOT NULL default 'Disabled', - [ReOpenedIncident] VARCHAR (20) NOT NULL default 'Disabled', - [WeeklySummary] VARCHAR (20) NOT NULL default 'Disabled', - [ApplicationSpike] VARCHAR (20) NOT NULL default 'Disabled', - [UserFeedback] VARCHAR (20) NOT NULL default 'Disabled' - ); -END -ALTER TABLE [UserNotificationSettings] +CREATE TABLE IF NOT EXISTS Settings ( + Section varchar(50) NOT NULL, + Name varchar(50) NOT NULL, + Value varchar(512) + ); + +CREATE TABLE IF NOT EXISTS Accounts ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + UserName varchar(50) NOT NULL, + HashedPassword varchar(512) NOT NULL, + CreatedAtUtc timestamp NOT NULL, + Email varchar(255) NOT NULL, + Salt varchar(512) NOT NULL, + AccountState varchar(20) NOT NULL, + TrackingId varchar(40) NULL, + LoginAttempts int NOT NULL, + LastLoginAtUtc timestamp NULL, + ActivationKey varchar(50) NULL, + PromotionCode varchar(50) NULL, + UpdatedAtUtc timestamp NULL + ); + +CREATE TABLE IF NOT EXISTS InvalidReports( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + AppKey varchar(36) NOT NULL, + Signature varchar(36) NOT NULL, + ReportBody text NOT NULL, + ErrorMessage varchar(2000) NOT NULL, + CreatedAtUtc timestamp NOT NULL + ); + + +CREATE TABLE IF NOT EXISTS Invitations( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + Email varchar(2000) NOT NULL, + InvitationKey char(32) NOT NULL, + CreatedAtUtc timestamp NOT NULL, + InvitedBy varchar(50) NOT NULL, + Invitations varchar(2500) NOT NULL); + +CREATE TABLE IF NOT EXISTS Applications ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + Name VARCHAR (50) NOT NULL, + AppKey VARCHAR (36) NOT NULL, + CreatedById INT NOT NULL, + CreatedAtUtc timestamp NOT NULL, + ApplicationType VARCHAR (40) NOT NULL, + SharedSecret VARCHAR (36) NOT NULL + ); + +CREATE TABLE IF NOT EXISTS CollectionMetadata ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + Name VARCHAR (50) NOT NULL, + ApplicationId INT NOT NULL, + Properties text NOT NULL + ); + +CREATE TABLE IF NOT EXISTS ErrorOrigins ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IpAddress VARCHAR (20) NOT NULL, + CountryCode VARCHAR (5) NULL, + CountryName VARCHAR (30) NULL, + RegionCode VARCHAR (5) NULL, + RegionName VARCHAR (30) NULL, + City VARCHAR (30) NULL, + ZipCode VARCHAR (10) NULL, + Latitude DECIMAL (9, 6) NOT NULL, + Longitude DECIMAL (9, 6) NOT NULL, + CreatedAtUtc timestamp NOT NULL + ); +CREATE TABLE IF NOT EXISTS ErrorReportOrigins ( + ErrorOriginId INT NOT NULL, + IncidentId INT NOT NULL, + ReportId INT NOT NULL, + ApplicationId INT NOT NULL, + CreatedAtUtc timestamp NOT NULL + ); + + +CREATE TABLE IF NOT EXISTS ErrorReports ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId INT NOT NULL, + ErrorId VARCHAR (36) NOT NULL, + ApplicationId INT NOT NULL, + ReportHashCode VARCHAR (20) NOT NULL, + CreatedAtUtc timestamp NOT NULL, + SolvedAtUtc timestamp NULL, + Title VARCHAR(100) NULL, + RemoteAddress VARCHAR (45) NULL, --SEE http://stackoverflow.com/questions/166132/maximum-length-of-the-textual-representation-of-an-ipv6-address + Exception TEXT NOT NULL, + ContextInfo TEXT NOT NULL + ); + +CREATE TABLE IF NOT EXISTS IncidentFeedback ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationId INT NULL, + IncidentId INT NULL, + ReportId INT NULL, + CreatedAtUtc timestamp NOT NULL, + RemoteAddress VARCHAR (20) NOT NULL, + Description TEXT NOT NULL, + EmailAddress VARCHAR (512) NULL, + Conversation TEXT NOT NULL, + ConversationLength INT NOT NULL, + ErrorReportId VARCHAR (40) NOT NULL, + Replied INT NOT NULL default 0 + ); + + +CREATE TABLE IF NOT EXISTS Incidents ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ReportHashCode VARCHAR (20) NOT NULL, + ApplicationId INT NOT NULL, + CreatedAtUtc timestamp NOT NULL, + HashCodeIdentifier VARCHAR (1024) NOT NULL, + ReportCount INT NOT NULL, + UpdatedAtUtc timestamp NULL, + Description TEXT NOT NULL, + FullName VARCHAR (255) NOT NULL, + Solution TEXT NULL, + IsSolved BYTEA NOT NULL , + IsSolutionShared BYTEA NOT NULL , + SolvedAtUtc timestamp NULL, + StackTrace TEXT NULL, + IsReOpened BIT NOT NULL , + ReOpenedAtUtc timestamp NULL, + PreviousSolutionAtUtc timestamp NULL, + IgnoreReports BIT NOT NULL , + IgnoringReportsSinceUtc timestamp NULL, + IgnoringRequestedBy VARCHAR (50) NULL, + LastSolutionAtUtc timestamp NULL + ); + +CREATE TABLE IF NOT EXISTS IncidentTags ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId INT NOT NULL, + TagName VARCHAR (40) NOT NULL, + OrderNumber INT NOT NULL + ); + +CREATE TABLE IF NOT EXISTS ReportContextInfo ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId INT NOT NULL, + ReportId INT NOT NULL, + CreatedAtUtc timestamp NOT NULL, + UpdatedAtUtc timestamp NULL, + Name VARCHAR (1024) NULL, + Value VARCHAR (20) NULL, + LargeValue TEXT NOT NULL + ); + +CREATE TABLE IF NOT EXISTS IncidentContextCollections ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId INT NOT NULL, + Name VARCHAR (250) NOT NULL, + Properties text NOT NULL + ); + + +CREATE TABLE IF NOT EXISTS Triggers ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + Name VARCHAR (50) NOT NULL, + Description VARCHAR (512) NOT NULL, + ApplicationId INT NOT NULL, + Rules TEXT NOT NULL, + Actions TEXT NOT NULL, + LastTriggerAction VARCHAR (50) NOT NULL, + RunForNewIncidents BIT NOT NULL, + RunForExistingIncidents BIT NOT NULL, + RunForReOpenedIncidents BIT NOT NULL + ); + + +CREATE TABLE IF NOT EXISTS UserNotificationSettings ( + AccountId INT NOT NULL, + ApplicationId INT NOT NULL, + NewIncident VARCHAR (20) NOT NULL default 'Disabled', + NewReport VARCHAR (20) NOT NULL default 'Disabled', + ReOpenedIncident VARCHAR (20) NOT NULL default 'Disabled', + WeeklySummary VARCHAR (20) NOT NULL default 'Disabled', + ApplicationSpike VARCHAR (20) NOT NULL default 'Disabled', + UserFeedback VARCHAR (20) NOT NULL default 'Disabled' + ); + +ALTER TABLE UserNotificationSettings ADD CONSTRAINT pk_UserNotificationSettings PRIMARY KEY (AccountId, ApplicationId); -CREATE TABLE dbo.Users +CREATE TABLE IF NOT EXISTS Users ( AccountId INT NOT NULL primary key, EmailAddress varchar(255) not null, @@ -276,50 +196,40 @@ CREATE TABLE dbo.Users ); -IF OBJECT_ID(N'dbo.[ApplicationMembers]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ApplicationMembers] ( - [AccountId] INT NULL foreign key references Accounts (Id), - [ApplicationId] INT NOT NULL foreign key references Applications (Id), - [EmailAddress] nvarchar(255) not null, - [AddedAtUtc] DATETIME NOT NULL, - [AddedByName] VARCHAR (50) NOT NULL, - [Roles] VARCHAR (255) NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[QueueEvents]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[QueueEvents] ( - [Id] INT identity not NULL primary key, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, - [Body] text NOT NULL, - ); -END - -IF OBJECT_ID(N'dbo.[QueueReports]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[QueueReports] ( - [Id] INT identity not NULL primary key, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, - [Body] text NOT NULL, +CREATE TABLE IF NOT EXISTS ApplicationMembers ( + AccountId INT NULL, + ApplicationId INT NOT NULL, + EmailAddress varchar(255) not null, + AddedAtUtc timestamp NOT NULL, + AddedByName VARCHAR (50) NOT NULL, + Roles VARCHAR (255) NOT NULL + ); + +--ALTER TABLE ApplicationMembers ADD CONSTRAINT constraint_name FOREIGN KEY (AccountId) REFERENCES Accounts (Id); +--ALTER TABLE ApplicationMembers ADD CONSTRAINT constraint_name FOREIGN KEY (ApplicationId) REFERENCES Applications (Id); + +CREATE TABLE IF NOT EXISTS QueueEvents ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationId INT NOT NULL, + CreatedAtUtc timestamp NOT NULL, + AssemblyQualifiedTypeName VARCHAR (255) NOT NULL, + Body text NOT NULL + ); + +CREATE TABLE IF NOT EXISTS QueueReports ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationId INT NOT NULL, + CreatedAtUtc timestamp NOT NULL, + AssemblyQualifiedTypeName VARCHAR (255) NOT NULL, + Body text NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[QueueFeedback]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[QueueFeedback] ( - [Id] INT identity not NULL primary key, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, - [Body] text NOT NULL, + ); + +CREATE TABLE IF NOT EXISTS QueueFeedback ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationId INT NOT NULL, + CreatedAtUtc timestamp NOT NULL, + AssemblyQualifiedTypeName VARCHAR (255) NOT NULL, + Body text NOT NULL - ); -END + ); \ No newline at end of file diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql index 964b7434..29f04bfe 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v02.sql @@ -1,18 +1,13 @@ -IF OBJECT_ID(N'dbo.[ApiKeys]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ApiKeys] ( - [Id] INT identity not NULL primary key, - [ApplicationName] varchar(40) NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [CreatedById] int NOT NULL, - [GeneratedKey] varchar(36) NOT NULL, - [SharedSecret] varchar(36) NOT NULL +CREATE TABLE IF NOT EXISTS ApiKeys ( + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationName varchar(40) NOT NULL, + CreatedAtUtc timestamp NOT NULL, + CreatedById int NOT NULL, + GeneratedKey varchar(36) NOT NULL, + SharedSecret varchar(36) NOT NULL ); - CREATE TABLE [dbo].[ApiKeyApplications] ( - [ApiKeyId] INT not NULL, - [ApplicationId] INT NOT NULL, - Primary key (ApiKeyId, ApplicationId), - FOREIGN KEY (ApiKeyId) REFERENCES ApiKeys(Id) ON DELETE CASCADE, - FOREIGN KEY (ApplicationId) REFERENCES Applications(Id) ON DELETE NO ACTION +CREATE TABLE IF NOT EXISTS ApiKeyApplications ( + ApiKeyId INT not NULL, + ApplicationId INT NOT NULL, + Primary key (ApiKeyId, ApplicationId) ); -END diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql index cde927e6..fe355a03 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v03.sql @@ -1,41 +1,41 @@ -ALTER TABLE Incidents ADD PRIMARY KEY (Id); -ALTER TABLE ErrorReportOrigins WITH CHECK ADD CONSTRAINT FK_ErrorReportOrigins_Reports FOREIGN KEY (ReportId) REFERENCES ErrorReports (Id) ON DELETE CASCADE; -ALTER TABLE ErrorReports WITH CHECK ADD CONSTRAINT FK_ErrorReports_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE CollectionMetadata WITH CHECK ADD CONSTRAINT FK_COLME_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; -ALTER TABLE IncidentFeedback WITH CHECK ADD CONSTRAINT FK_IncidentFeedback_incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE Incidents WITH CHECK ADD CONSTRAINT FK_Incidents_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; -ALTER TABLE IncidentTags WITH CHECK ADD CONSTRAINT FK_IncidentTags_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE ReportContextInfo WITH CHECK ADD CONSTRAINT FK_ReportContextInfo_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE IncidentContextCollections WITH CHECK ADD CONSTRAINT FK_ICC_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE [UserNotificationSettings] WITH CHECK ADD CONSTRAINT FK_UNS_accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; -ALTER TABLE [Triggers] WITH CHECK ADD CONSTRAINT FK_Triggers_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +--ALTER TABLE Incidents ADD PRIMARY KEY (Id); +--ALTER TABLE ErrorReportOrigins ADD CONSTRAINT FK_ErrorReportOrigins_Reports FOREIGN KEY (ReportId) REFERENCES ErrorReports (Id) ON DELETE CASCADE; +--ALTER TABLE ErrorReports ADD CONSTRAINT FK_ErrorReports_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +--ALTER TABLE CollectionMetadata ADD CONSTRAINT FK_COLME_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +--ALTER TABLE IncidentFeedback ADD CONSTRAINT FK_IncidentFeedback_incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +--ALTER TABLE Incidents ADD CONSTRAINT FK_Incidents_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +--ALTER TABLE IncidentTags ADD CONSTRAINT FK_IncidentTags_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +--ALTER TABLE ReportContextInfo ADD CONSTRAINT FK_ReportContextInfo_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +--ALTER TABLE IncidentContextCollections ADD CONSTRAINT FK_ICC_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +--ALTER TABLE UserNotificationSettings ADD CONSTRAINT FK_UNS_accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; +--ALTER TABLE Triggers ADD CONSTRAINT FK_Triggers_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; -DECLARE @ConstraintName nvarchar(200) -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApplicationMembers' AND - KCU.COLUMN_NAME = 'AccountId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) -END; -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApplicationMembers' AND - KCU.COLUMN_NAME = 'ApplicationId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) -END -ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; -ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +--DECLARE @ConstraintName nvarchar(200) +--SELECT @ConstraintName = KCU.CONSTRAINT_NAME +--FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU +-- ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +-- AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +-- AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +--WHERE +-- KCU.TABLE_NAME = 'ApplicationMembers' AND +-- KCU.COLUMN_NAME = 'AccountId' +--IF @ConstraintName IS NOT NULL +--BEGIN +-- EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) +--END; +--SELECT @ConstraintName = KCU.CONSTRAINT_NAME +--FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU +-- ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +-- AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +-- AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +--WHERE +-- KCU.TABLE_NAME = 'ApplicationMembers' AND +-- KCU.COLUMN_NAME = 'ApplicationId' +--IF @ConstraintName IS NOT NULL +--BEGIN +-- EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) +--END +--ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; +--ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql index be6918ec..26534d57 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v04.sql @@ -1,5 +1,5 @@ --version 1.0 (part A) of OneTrueError -ALTER TABLE Accounts ADD IsSysAdmin bit not null default 0; -alter table ApplicationMembers add Id int identity not null primary key; -ALTER TABLE ApplicationMembers ALTER COLUMN [EmailAddress] nvarchar(255) null; +--ALTER TABLE Accounts ADD IsSysAdmin bit not null default 0; +--alter table ApplicationMembers add Id int identity not null primary key; +--ALTER TABLE ApplicationMembers ALTER COLUMN [EmailAddress] nvarchar(255) null; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql index 36d1a4c5..70c97180 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v05.sql @@ -1,3 +1,3 @@ --version 1.0 of OneTrueError --a split was required due to updating created column -UPDATE Accounts SET IsSysAdmin = 1 WHERE Id = (SELECT TOP 1 Id FROM ACCOUNTS ORDER BY Id); +--UPDATE Accounts SET IsSysAdmin = 1 WHERE Id = (SELECT TOP 1 Id FROM ACCOUNTS ORDER BY Id); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql index 258123df..1bc8e0e2 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v06.sql @@ -1,39 +1,39 @@ -create table ApplicationVersions +CREATE TABLE IF NOT EXISTS ApplicationVersions ( - Id int not null identity primary key, - ApplicationId int not null foreign key references Applications (Id), +Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationId int not null , ApplicationName varchar(40) not null, - FirstReportDate datetime not null, - LastReportDate datetime not null, + FirstReportDate timestamp not null, + LastReportDate timestamp not null, Version varchar(10) not null ); -create table ApplicationVersionMonths +CREATE TABLE IF NOT EXISTS ApplicationVersionMonths ( - Id int not null identity primary key, - VersionId int not null foreign key references ApplicationVersions (Id), - YearMonth date not null, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + VersionId int not null , + YearMonth timestamp not null, IncidentCount int not null, ReportCount int not null, - LastUpdateAtUtc datetime not null + LastUpdateAtUtc timestamp not null ); -create table IncidentVersions +CREATE TABLE IF NOT EXISTS IncidentVersions ( - IncidentId int not null constraint FK_IncidentVersions_Incidents references Incidents(Id), - VersionId int not null constraint FK_IncidentVersions_ApplicationVersions references ApplicationVersions(Id) + IncidentId int not null , + VersionId int not null ); -IF COL_LENGTH('dbo.Incidents', 'StackTrace') IS NULL -BEGIN -ALTER TABLE Incidents ADD StackTrace varchar(MAX); +--IF COL_LENGTH('dbo.Incidents', 'StackTrace') IS NULL +--BEGIN +--ALTER TABLE Incidents ADD StackTrace varchar(MAX); -UPDATE Incidents - SET StackTrace = ( - SELECT TOP (1) Substring(Exception, - CHARINDEX('"StackTrace":"', Exception) + 14, - DATALENGTH(exception)-CHARINDEX('"StackTrace":"', Exception) - 14 - 1) - FROM ErrorReports - WHERE IncidentId = Incidents.Id) +--UPDATE Incidents +-- SET StackTrace = ( +-- SELECT TOP (1) Substring(Exception, +-- CHARINDEX('"StackTrace":"', Exception) + 14, +-- DATALENGTH(exception)-CHARINDEX('"StackTrace":"', Exception) - 14 - 1) +-- FROM ErrorReports +-- WHERE IncidentId = Incidents.Id) -END +--END diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql index 14f3e4a8..b9885e40 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v07.sql @@ -1,12 +1,12 @@ -CREATE TABLE MessageQueue +CREATE TABLE IF NOT EXISTS MessageQueue ( - Id int not null identity primary key, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, QueueName varchar(40) not null, - CreatedAtUtc datetime not null, + CreatedAtUtc timestamp not null, MessageType varchar(512) not null, - Body nvarchar(MAX) not null + Body varchar not null ); -DROP TABLE QueueEvents; -DROP TABLE QueueFeedback; -DROP TABLE QueueReports; +DROP TABLE IF EXISTS QueueEvents; +DROP TABLE IF EXISTS QueueFeedback; +DROP TABLE IF EXISTS QueueReports; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql index cfcf1a52..38234de5 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v08.sql @@ -1,34 +1,34 @@ -ALTER TABLE Incidents ADD State int not null default(0); -ALTER TABLE Incidents ADD AssignedToId int; -ALTER TABLE Incidents ADD AssignedAtUtc datetime; -ALTER TABLE Incidents ADD LastReportAtUtc datetime; +--ALTER TABLE Incidents ADD State int not null default(0); +--ALTER TABLE Incidents ADD AssignedToId int; +--ALTER TABLE Incidents ADD AssignedAtUtc datetime; +--ALTER TABLE Incidents ADD LastReportAtUtc datetime; -DECLARE @ConstraintName nvarchar(200) -SELECT @ConstraintName = d.Name - FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c - WHERE c.name = 'IsSolved' - AND c.object_id = OBJECT_ID(N'Incidents') - AND d.PARENT_COLUMN_ID = c.column_id -EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName) -alter table Incidents drop column IsSolved; +--DECLARE @ConstraintName nvarchar(200) +--SELECT @ConstraintName = d.Name +-- FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c +-- WHERE c.name = 'IsSolved' +-- AND c.object_id = OBJECT_ID(N'Incidents') +-- AND d.PARENT_COLUMN_ID = c.column_id +--EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName) +--alter table Incidents drop column IsSolved; -SELECT @ConstraintName = d.Name - FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c - WHERE c.name = 'IgnoreReports' - AND c.object_id = OBJECT_ID(N'Incidents') - AND d.PARENT_COLUMN_ID = c.column_id -EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName) -alter table Incidents drop column IgnoreReports; +--SELECT @ConstraintName = d.Name +-- FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c +-- WHERE c.name = 'IgnoreReports' +-- AND c.object_id = OBJECT_ID(N'Incidents') +-- AND d.PARENT_COLUMN_ID = c.column_id +--EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName) +--alter table Incidents drop column IgnoreReports; --- ApiKey module deletes relations manually. -SELECT - @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApiKeyApplications' AND - KCU.COLUMN_NAME = 'ApplicationId' -IF @ConstraintName IS NOT NULL EXEC('alter table ApiKeyApplications drop CONSTRAINT ' + @ConstraintName) +---- ApiKey module deletes relations manually. +--SELECT +-- @ConstraintName = KCU.CONSTRAINT_NAME +--FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU +-- ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +-- AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +-- AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +--WHERE +-- KCU.TABLE_NAME = 'ApiKeyApplications' AND +-- KCU.COLUMN_NAME = 'ApplicationId' +--IF @ConstraintName IS NOT NULL EXEC('alter table ApiKeyApplications drop CONSTRAINT ' + @ConstraintName) diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql index 18b10340..6cd23749 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v09.sql @@ -1,64 +1,64 @@ -DECLARE @ConstraintName nvarchar(200) -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApplicationVersionMonths' AND - KCU.COLUMN_NAME = 'VersionId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE ApplicationVersionMonths DROP CONSTRAINT ' + @ConstraintName) -END; -ALTER TABLE ApplicationVersionMonths WITH CHECK ADD CONSTRAINT FK_ApplicationVersionMonths_ApplicationVersions FOREIGN KEY (VersionId) REFERENCES ApplicationVersions (Id) ON DELETE CASCADE; +--DECLARE @ConstraintName nvarchar(200) +--SELECT @ConstraintName = KCU.CONSTRAINT_NAME +--FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU +-- ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +-- AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +-- AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +--WHERE +-- KCU.TABLE_NAME = 'ApplicationVersionMonths' AND +-- KCU.COLUMN_NAME = 'VersionId' +--IF @ConstraintName IS NOT NULL +--BEGIN +-- EXEC('ALTER TABLE ApplicationVersionMonths DROP CONSTRAINT ' + @ConstraintName) +--END; +--ALTER TABLE ApplicationVersionMonths WITH CHECK ADD CONSTRAINT FK_ApplicationVersionMonths_ApplicationVersions FOREIGN KEY (VersionId) REFERENCES ApplicationVersions (Id) ON DELETE CASCADE; -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApplicationVersions' AND - KCU.COLUMN_NAME = 'ApplicationId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE ApplicationVersions DROP CONSTRAINT ' + @ConstraintName) -END; -ALTER TABLE ApplicationVersions WITH CHECK ADD CONSTRAINT FK_ApplicationVersions_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; +--SELECT @ConstraintName = KCU.CONSTRAINT_NAME +--FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU +-- ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +-- AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +-- AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +--WHERE +-- KCU.TABLE_NAME = 'ApplicationVersions' AND +-- KCU.COLUMN_NAME = 'ApplicationId' +--IF @ConstraintName IS NOT NULL +--BEGIN +-- EXEC('ALTER TABLE ApplicationVersions DROP CONSTRAINT ' + @ConstraintName) +--END; +--ALTER TABLE ApplicationVersions WITH CHECK ADD CONSTRAINT FK_ApplicationVersions_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'IncidentVersions' AND - KCU.COLUMN_NAME = 'IncidentId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE IncidentVersions DROP CONSTRAINT ' + @ConstraintName) -END; -ALTER TABLE IncidentVersions WITH CHECK ADD CONSTRAINT FK_IncVersions_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; +--SELECT @ConstraintName = KCU.CONSTRAINT_NAME +--FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC +--INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU +-- ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +-- AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +-- AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +--WHERE +-- KCU.TABLE_NAME = 'IncidentVersions' AND +-- KCU.COLUMN_NAME = 'IncidentId' +--IF @ConstraintName IS NOT NULL +--BEGIN +-- EXEC('ALTER TABLE IncidentVersions DROP CONSTRAINT ' + @ConstraintName) +--END; +--ALTER TABLE IncidentVersions WITH CHECK ADD CONSTRAINT FK_IncVersions_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -create table ErrorReportCollectionProperties -( - Id int identity not null primary key, - ReportId int not null constraint FK_ErrorReportCollectionProperties_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE, - Name varchar(50) not null, - PropertyName varchar(50) not null, - Value nvarchar(MAX) not null -); +--create table ErrorReportCollectionProperties +--( +-- Id int identity not null primary key, +-- ReportId int not null constraint FK_ErrorReportCollectionProperties_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE, +-- Name varchar(50) not null, +-- PropertyName varchar(50) not null, +-- Value nvarchar(MAX) not null +--); -create table ErrorReportCollectionInbound -( - Id int identity not null primary key, - ReportId int not null constraint FK_ErrorReportCollectionInbound_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE, - Body nvarchar(max) not null -); +--create table ErrorReportCollectionInbound +--( +-- Id int identity not null primary key, +-- ReportId int not null constraint FK_ErrorReportCollectionInbound_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE, +-- Body nvarchar(max) not null +--); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql index 40b351f8..39975b78 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v10.sql @@ -1,18 +1,18 @@ -create table IncidentEnvironments +CREATE TABLE IF NOT EXISTS IncidentEnvironments ( - Id int not null identity primary key, - IncidentId int not null constraint FK_IncidentEnvironment_Incidents REFERENCES Incidents(Id) ON DELETE CASCADE, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId int not null , EnvironmentName varchar(50) not null ); -create table IncidentHistory +CREATE TABLE IF NOT EXISTS IncidentHistory ( - Id int not null identity primary key, - IncidentId int not null constraint FK_IncidentHistory_Incidents REFERENCES Incidents(Id) ON DELETE CASCADE, - CreatedAtUtc datetime not null, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId int not null , + CreatedAtUtc timestamp not null, AccountId int NULL, -- for system entries State int not null, ApplicationVersion varchar(40) NULL -- for action where version is not related to the action ); alter table Incidents add IgnoredUntilVersion varchar(20) null; -CREATE NONCLUSTERED INDEX IX_IncidentHistory_Incidents ON dbo.IncidentHistory (IncidentId); +--CREATE NONCLUSTERED INDEX IX_IncidentHistory_Incidents ON dbo.IncidentHistory (IncidentId); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql index efa6bac9..cdc6d4a8 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v13.sql @@ -1,8 +1,8 @@ -create table ErrorReportSpikes +CREATE TABLE IF NOT EXISTS ErrorReportSpikes ( - Id int identity not null primary key, - ApplicationId int not null constraint FK_ErrorReportSpikes_Applications REFERENCES Applications(Id) ON DELETE CASCADE, - SpikeDate datetime not null, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + ApplicationId int not null , + SpikeDate timestamp not null, Count int not null, - NotifiedAccounts varchar(max) not null + NotifiedAccounts varchar not null ); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql index 39e970e9..2e9a90e1 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v14.sql @@ -1 +1 @@ -alter table ApplicationVersions alter column Version varchar(20) not null; +--alter table ApplicationVersions alter column Version varchar(20) not null; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql index da505608..aafc9939 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v15.sql @@ -1,10 +1,11 @@ -create Table IgnoredReports +CREATE TABLE IF NOT EXISTS IgnoredReports ( - Id int not null identity primary key, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, NumberOfReports int not null, - Date datetime not null + Date timestamp not null, + NumberOfFtes decimal, + EstimatedNumberOfErrors int, + MuteStatisticsQuestion BOOLEAN not null ); -alter table Applications add NumberOfFtes decimal; -alter table Applications add [EstimatedNumberOfErrors] int; -alter table Applications add [MuteStatisticsQuestion] bit not null default 0; + diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql index 276ee38b..32e7fff6 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v16.sql @@ -1 +1 @@ -alter table applications alter column NumberOfFtes decimal(4,1) null; +--alter table applications alter column NumberOfFtes decimal(4,1) null; diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql index 8e697cd4..e1b18dad 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v17.sql @@ -1,12 +1,11 @@ -create table Environments +CREATE TABLE IF NOT EXISTS Environments ( - Id int identity not null primary key, - Name varchar(100) not null, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + Name varchar(100) not null ); -drop table IncidentEnvironments; -CREATE TABLE IncidentEnvironments +CREATE TABLE IF NOT EXISTS IncidentEnvironments ( - IncidentId int not null constraint FK_IncidentEnvironment_Incident foreign key references Incidents(Id) ON DELETE CASCADE, - EnvironmentId int not null constraint FK_IncidentEnvironment_Environments foreign key references Environments(Id), + IncidentId int not null , + EnvironmentId int not null ); diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql index 0b472bcb..a8981444 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v18.sql @@ -1,11 +1,11 @@ -IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name='IDX_ErrorReportCollectionProperties_ReportId' -AND object_id = OBJECT_ID('ErrorReportCollectionProperties')) -begin - CREATE INDEX IDX_ErrorReportCollectionProperties_ReportId - ON ErrorReportCollectionProperties (ReportId); +--IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name='IDX_ErrorReportCollectionProperties_ReportId' +--AND object_id = OBJECT_ID('ErrorReportCollectionProperties')) +--begin +-- CREATE INDEX IDX_ErrorReportCollectionProperties_ReportId +-- ON ErrorReportCollectionProperties (ReportId); - CREATE INDEX IDX_ErrorReportCollectionInbound_ReportId - ON ErrorReportCollectionInbound (ReportId); +-- CREATE INDEX IDX_ErrorReportCollectionInbound_ReportId +-- ON ErrorReportCollectionInbound (ReportId); -end +--end diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql index 88755c7c..c7ac6497 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v19.sql @@ -1,11 +1,11 @@ -CREATE TABLE IncidentReports +CREATE TABLE IF NOT EXISTS IncidentReports ( - Id int not null identity primary key, - IncidentId int not null constraint FK_IncidentReports_Incidents foreign key references Incidents(Id) on delete cascade, - ReceivedAtUtc datetime not null, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + IncidentId int not null , + ReceivedAtUtc timestamp not null, ErrorId varchar(40) not null ); -create index IDX_IncidentReports_IncidentId ON IncidentReports (IncidentId, ReceivedAtUtc); +--create index IDX_IncidentReports_IncidentId ON IncidentReports (IncidentId, ReceivedAtUtc); insert into IncidentReports (IncidentId, ReceivedAtUtc, ErrorId) SELECT IncidentId, CreatedAtUtc, ErrorId diff --git a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql index b8c7af2e..d39cf96a 100644 --- a/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql +++ b/src/Server/Coderr.Server.PostgreSQL/Schema/Coderr.v20.sql @@ -1,14 +1,14 @@ -create table CorrelationIds +CREATE TABLE IF NOT EXISTS CorrelationIds ( - Id int identity not null primary key, + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, Value varchar(40) not null ); -create table IncidentCorrelations +CREATE TABLE IF NOT EXISTS IncidentCorrelations ( - Id int identity not null primary key, - CorrelationId int not null constraint FK_IncidentCorrelations_CorrelationIds foreign key references CorrelationIds(Id), - IncidentId int not null constraint FK_IncidentCorrelations_Incidents foreign key references Incidents(Id) ON DELETE CASCADE + Id int GENERATED ALWAYS AS IDENTITY NOT NULL, + CorrelationId int not null , + IncidentId int not null ); -create unique index IDX_IncidentCorrelations_Pair ON IncidentCorrelations(CorrelationId, IncidentId); +--create unique index IDX_IncidentCorrelations_Pair ON IncidentCorrelations(CorrelationId, IncidentId); diff --git a/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs b/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkPostgreSQLWithTransaction.cs similarity index 85% rename from src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs rename to src/Server/Coderr.Server.PostgreSQL/UnitOfWorkPostgreSQLWithTransaction.cs index 43fba7e2..893235dd 100644 --- a/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkWithTransaction.cs +++ b/src/Server/Coderr.Server.PostgreSQL/UnitOfWorkPostgreSQLWithTransaction.cs @@ -3,24 +3,25 @@ using System.Data.SqlClient; using Griffin.Data; using log4net; +using Npgsql; namespace Coderr.Server.PostgreSQL { /// /// Required for background jobs which uses /// - public class UnitOfWorkWithTransaction : IAdoNetUnitOfWork + public class UnitOfWorkPostgreSQLWithTransaction : IAdoNetUnitOfWork { - private ILog _logger = LogManager.GetLogger(typeof(UnitOfWorkWithTransaction)); - private SqlCommand _lastCommand; + private ILog _logger = LogManager.GetLogger(typeof(UnitOfWorkPostgreSQLWithTransaction)); + private NpgsqlCommand _lastCommand; - public UnitOfWorkWithTransaction(SqlTransaction transaction) + public UnitOfWorkPostgreSQLWithTransaction(NpgsqlTransaction transaction) { Transaction = transaction; } - public SqlTransaction Transaction { get; private set; } + public NpgsqlTransaction Transaction { get; private set; } public void Dispose() { diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql index 4b55ba08..19b0023c 100644 --- a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql +++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql @@ -1,4 +1,4 @@ -IF OBJECT_ID(N'dbo.[Settings]', N'U') IS NULL +xIF OBJECT_ID(N'dbo.[Settings]', N'U') IS NULL BEGIN CREATE TABLE [dbo].[Settings]( [Section] [varchar](50) NOT NULL, diff --git a/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs b/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs index a862458a..367ff76a 100644 --- a/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs +++ b/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs @@ -2,6 +2,7 @@ using System.Data; using System.Data.SqlClient; using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; using Coderr.Server.Abstractions; using Coderr.Server.Infrastructure; using Coderr.Server.SqlServer.Migrations; @@ -22,12 +23,20 @@ public class SqlServerTools : ISetupDatabaseTools private readonly Func _connectionFactory; private readonly MigrationRunner _schemaManager; + public SqlServerTools(string connectionString, ClaimsPrincipal arg) + { + _connectionFactory = () => + { + return this.GetConnection(connectionString, arg); + }; + _schemaManager = new MigrationRunner(_connectionFactory, "Coderr", typeof(CoderrMigrationPointer).Namespace); + } public SqlServerTools(Func connectionFactory) { _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _schemaManager = new MigrationRunner(_connectionFactory, "Coderr", typeof(CoderrMigrationPointer).Namespace); } - + /// /// Checks if the tables exists and are for the current DB schema. /// @@ -51,7 +60,7 @@ public bool IsTablesInstalled() { return false; } - + } [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", @@ -65,7 +74,12 @@ IDbConnection ISetupDatabaseTools.OpenConnection() { return _connectionFactory(); } - + public IDbConnection GetConnection(string connectionString, ClaimsPrincipal arg) + { + var con = new SqlConnection(connectionString); + con.Open(); + return con; + } public void TestConnection(string connectionString) { var con = new SqlConnection(connectionString); diff --git a/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs b/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs index ab8d723c..59bea00e 100644 --- a/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs +++ b/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs @@ -108,6 +108,7 @@ private void SetStateFlag() { using (var uow = new AdoNetUnitOfWork(con)) { + //TODO: TOP not support in Coderr.Server.PostgreSQL but LIMIT yes. var id = uow.ExecuteScalar("SELECT TOP 1 Id FROM Accounts"); if (id != null) ViewBag.AlreadyCreated = true; diff --git a/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs b/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs index 10a56810..d14c3912 100644 --- a/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs +++ b/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs @@ -10,6 +10,7 @@ using Coderr.Server.Abstractions.Security; using Coderr.Server.App.Core.Accounts; using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.PostgreSQL; using Coderr.Server.SqlServer; using Coderr.Server.Web.Boot.Adapters; using DotNetCqs; @@ -56,6 +57,9 @@ public void Configure(ConfigurationContext context) assembly = typeof(SqlServerTools).Assembly; context.Services.RegisterMessageHandlers(assembly); + assembly = typeof(PostgreSQLTools).Assembly; + context.Services.RegisterMessageHandlers(assembly); + context.Services.AddScoped(); context.Services.AddSingleton(CreateQueueProvider(context)); context.Services.AddSingleton(x => diff --git a/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs b/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs index 834a01d7..ff10aa3f 100644 --- a/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs +++ b/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs @@ -2,9 +2,10 @@ using System.Data; using System.Data.SqlClient; using Coderr.Server.Abstractions.Boot; -using Coderr.Server.SqlServer; +using Coderr.Server.Infrastructure; using Griffin.Data; using Microsoft.Extensions.DependencyInjection; +using Npgsql; namespace Coderr.Server.Web.Boot.Modules { @@ -16,8 +17,9 @@ public void Configure(ConfigurationContext context) { _config = context.Configuration; context.Services.AddScoped(x => OpenConnection()); - context.Services.AddScoped(x => x.GetRequiredService().BeginTransaction()); - context.Services.AddScoped(x => new UnitOfWorkWithTransaction((SqlTransaction)x.GetRequiredService())); + context.Services.AddScoped(x => x.GetRequiredService().BeginTransaction()); + context.Services.AddScoped(x => new SqlServer.UnitOfWorkWithTransaction((SqlTransaction)x.GetRequiredService())); + context.Services.AddScoped(x => new PostgreSQL.UnitOfWorkPostgreSQLWithTransaction((NpgsqlTransaction)x.GetRequiredService())); } public IDbConnection OpenConnection() @@ -27,8 +29,7 @@ public IDbConnection OpenConnection() { throw new InvalidOperationException("Missing the connection string 'Db'."); } - var con = new SqlConnection(db); - con.Open(); + var con = SetupTools.DbTools.OpenConnection(); return con; } diff --git a/src/Server/Coderr.Server.Web/Boot/Modules/RegisterContainerServices.cs b/src/Server/Coderr.Server.Web/Boot/Modules/RegisterContainerServices.cs index ba23a03e..ba604f32 100644 --- a/src/Server/Coderr.Server.Web/Boot/Modules/RegisterContainerServices.cs +++ b/src/Server/Coderr.Server.Web/Boot/Modules/RegisterContainerServices.cs @@ -1,5 +1,6 @@ using Coderr.Server.Abstractions.Boot; using Coderr.Server.App.Core.Accounts; +using Coderr.Server.PostgreSQL; using Coderr.Server.SqlServer; namespace Coderr.Server.Web.Boot.Modules @@ -13,6 +14,9 @@ public void Configure(ConfigurationContext context) assembly = typeof(SqlServerTools).Assembly; context.Services.RegisterContainerServices(assembly); + + assembly = typeof(PostgreSQLTools).Assembly; + context.Services.RegisterContainerServices(assembly); } public void Start(StartContext context) diff --git a/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj b/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj index 1f75d9dd..07e72826 100644 --- a/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj +++ b/src/Server/Coderr.Server.Web/Coderr.Server.Web.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Server/Coderr.Server.Web/Startup.cs b/src/Server/Coderr.Server.Web/Startup.cs index 1697df2f..0bf5d7d4 100644 --- a/src/Server/Coderr.Server.Web/Startup.cs +++ b/src/Server/Coderr.Server.Web/Startup.cs @@ -16,6 +16,7 @@ using Coderr.Server.Infrastructure.Configuration; using Coderr.Server.Infrastructure.Configuration.Database; using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.PostgreSQL; using Coderr.Server.SqlServer; using Coderr.Server.SqlServer.Migrations; using Coderr.Server.SqlServer.Schema; @@ -233,6 +234,16 @@ private void ConfigureCoderr(IApplicationBuilder app) Err.Configuration.ThrowExceptions = false; app.CatchOwinExceptions(); + //TODO: we need to automate this block. + //TODO: you can use according to the one you need. + + //SetupTools.DbTools = + // new SqlServerTools(Configuration.GetConnectionString("Db"), + // CoderrClaims.SystemPrincipal); + + SetupTools.DbTools = + new PostgreSQLTools(Configuration.GetConnectionString("Db"), + CoderrClaims.SystemPrincipal); if (!IsConfigured) return; @@ -240,7 +251,7 @@ private void ConfigureCoderr(IApplicationBuilder app) CoderrConfigSection config; try - { + { var dbStore = new DatabaseStore(() => OpenConnection(CoderrClaims.SystemPrincipal)); config = dbStore.Load(); if (config == null) @@ -291,11 +302,7 @@ private void OnShutdown() private IDbConnection OpenConnection(ClaimsPrincipal arg) { var db = Configuration.GetConnectionString("Db"); - var con = new NpgsqlConnection(db); - con.Open(); - - //var con = new SqlConnection(db); - //con.Open(); + var con = SetupTools.DbTools.OpenConnection(); return con; } @@ -308,7 +315,6 @@ private void RegisterConfigurationStores(IServiceCollection services) private void RegisterInstallationConfiguration(IServiceCollection services) { - SetupTools.DbTools = new SqlServerTools(() => OpenConnection(CoderrClaims.SystemPrincipal)); var store = new DatabaseStore(() => OpenConnection(CoderrClaims.SystemPrincipal)); services.AddSingleton(store); services.Configure(Configuration.GetSection("Installation")); diff --git a/src/Server/Coderr.Server.Web/appsettings.json b/src/Server/Coderr.Server.Web/appsettings.json index 2af07ef8..845b46a4 100644 --- a/src/Server/Coderr.Server.Web/appsettings.json +++ b/src/Server/Coderr.Server.Web/appsettings.json @@ -1,12 +1,12 @@ { "Installation": { - "IsConfigured": true, - "Password": "changeThis" + "IsConfigured": false, + "Password": "changeThis@@@" }, "EnableCors": true, "ConnectionStrings": { - "Db": "User ID=qmpevmwl;Password=OFc9nWUvk7wlPoeZOVk-0lalb6AqPAR3;Host=salt.db.elephantsql.com;Port=5432;Database=qmpevmwl;Pooling=true;", - "Dbx": "Data Source=HB4LCP2;Initial Catalog=Coderr;Persist Security Info=True;User ID=sa;Password=1234;Connect Timeout=15;" + "Db": "User ID=postgres;Password=xxx;Host=localhost;Port=5432;Database=postgres;Pooling=true;", + "DbSqlServer": "Data Source=localhost;Initial Catalog=Coderr;Integrated Security=True;Connect Timeout=15;" }, "Logging": { "IncludeScopes": false,