Skip to content

Commit

Permalink
[PM-10560] Create notification database storage (#4688)
Browse files Browse the repository at this point in the history
* Add new tables

* Add stored procedures

* Add core entities and models

* Setup EF

* Add repository interfaces

* Add dapper repos

* Add EF repos

* Add order by

* EF updates

* PM-10560: Notifications repository matching requirements.

* PM-10560: Notifications repository matching requirements.

* PM-10560: Migration scripts

* PM-10560: EF index optimizations

* PM-10560: Cleanup

* PM-10560: Priority in natural order, Repository, sql simplifications

* PM-10560: Title column update

* PM-10560: Incorrect EF migration removal

* PM-10560: EF migrations

* PM-10560: Added views, SP naming simplification

* PM-10560: Notification entity Title update, EF migrations

* PM-10560: Removing Notification_ReadByUserId

* PM-10560: Notification ReadByUserIdAndStatus fix

* PM-10560: Notification ReadByUserIdAndStatus fix to be in line with requirements and EF

---------

Co-authored-by: Maciej Zieniuk <[email protected]>
Co-authored-by: Matt Bishop <[email protected]>
  • Loading branch information
3 people committed Sep 9, 2024
1 parent 55bf815 commit 4c0f8d5
Show file tree
Hide file tree
Showing 39 changed files with 9,983 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/Core/NotificationCenter/Entities/Notification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.Utilities;

namespace Bit.Core.NotificationCenter.Entities;

public class Notification : ITableObject<Guid>
{
public Guid Id { get; set; }
public Priority Priority { get; set; }
public bool Global { get; set; }
public ClientType ClientType { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
[MaxLength(256)]
public string? Title { get; set; }
public string? Body { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }

public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
}
10 changes: 10 additions & 0 deletions src/Core/NotificationCenter/Entities/NotificationStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable enable
namespace Bit.Core.NotificationCenter.Entities;

public class NotificationStatus
{
public Guid NotificationId { get; set; }
public Guid UserId { get; set; }
public DateTime? ReadDate { get; set; }
public DateTime? DeletedDate { get; set; }
}
18 changes: 18 additions & 0 deletions src/Core/NotificationCenter/Enums/ClientType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#nullable enable
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.NotificationCenter.Enums;

public enum ClientType : byte
{
[Display(Name = "All")]
All = 0,
[Display(Name = "Web Vault")]
Web = 1,
[Display(Name = "Browser Extension")]
Browser = 2,
[Display(Name = "Desktop App")]
Desktop = 3,
[Display(Name = "Mobile App")]
Mobile = 4
}
18 changes: 18 additions & 0 deletions src/Core/NotificationCenter/Enums/Priority.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#nullable enable
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.NotificationCenter.Enums;

public enum Priority : byte
{
[Display(Name = "Informational")]
Informational = 0,
[Display(Name = "Low")]
Low = 1,
[Display(Name = "Medium")]
Medium = 2,
[Display(Name = "High")]
High = 3,
[Display(Name = "Critical")]
Critical = 4
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#nullable enable
namespace Bit.Core.NotificationCenter.Models.Filter;

public class NotificationStatusFilter
{
public bool? Read { get; set; }
public bool? Deleted { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#nullable enable
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationCenter.Models.Filter;
using Bit.Core.Repositories;

namespace Bit.Core.NotificationCenter.Repositories;

public interface INotificationRepository : IRepository<Notification, Guid>
{
/// <summary>
/// Get notifications for a user with the given filters.
/// Includes global notifications.
/// </summary>
/// <param name="userId">User Id</param>
/// <param name="clientType">
/// Filter for notifications by client type. Always includes notifications with <see cref="ClientType.All"/>.
/// </param>
/// <param name="statusFilter">
/// Filters notifications by status.
/// If both <see cref="NotificationStatusFilter.Read"/> and <see cref="NotificationStatusFilter.Deleted"/>
/// are not set, includes notifications without a status.
/// </param>
/// <returns>
/// Ordered by priority (highest to lowest) and creation date (descending).
/// </returns>
Task<IEnumerable<Notification>> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType,
NotificationStatusFilter? statusFilter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable
using Bit.Core.NotificationCenter.Entities;

namespace Bit.Core.NotificationCenter.Repositories;

public interface INotificationStatusRepository
{
Task<NotificationStatus?> GetByNotificationIdAndUserIdAsync(Guid notificationId, Guid userId);
Task<NotificationStatus> CreateAsync(NotificationStatus notificationStatus);
Task UpdateAsync(NotificationStatus notificationStatus);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Repositories;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
using Bit.Infrastructure.Dapper.Auth.Repositories;
using Bit.Infrastructure.Dapper.Billing.Repositories;
using Bit.Infrastructure.Dapper.NotificationCenter.Repositories;
using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.SecretsManager.Repositories;
using Bit.Infrastructure.Dapper.Tools.Repositories;
Expand Down Expand Up @@ -52,6 +54,8 @@ public static void AddDapperRepositories(this IServiceCollection services, bool
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();
services.AddSingleton<INotificationRepository, NotificationRepository>();
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#nullable enable
using System.Data;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationCenter.Models.Filter;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
using Microsoft.Data.SqlClient;

namespace Bit.Infrastructure.Dapper.NotificationCenter.Repositories;

public class NotificationRepository : Repository<Notification, Guid>, INotificationRepository
{
public NotificationRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{
}

public NotificationRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{
}

public async Task<IEnumerable<Notification>> GetByUserIdAndStatusAsync(Guid userId,
ClientType clientType, NotificationStatusFilter? statusFilter)
{
await using var connection = new SqlConnection(ConnectionString);

var results = await connection.QueryAsync<Notification>(
"[dbo].[Notification_ReadByUserIdAndStatus]",
new { UserId = userId, ClientType = clientType, statusFilter?.Read, statusFilter?.Deleted },
commandType: CommandType.StoredProcedure);

return results.ToList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#nullable enable
using System.Data;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
using Microsoft.Data.SqlClient;

namespace Bit.Infrastructure.Dapper.NotificationCenter.Repositories;

public class NotificationStatusRepository : BaseRepository, INotificationStatusRepository
{
public NotificationStatusRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{
}

public NotificationStatusRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{
}

public async Task<NotificationStatus?> GetByNotificationIdAndUserIdAsync(Guid notificationId, Guid userId)
{
await using var connection = new SqlConnection(ConnectionString);

return await connection.QueryFirstOrDefaultAsync<NotificationStatus>(
"[dbo].[NotificationStatus_ReadByNotificationIdAndUserId]",
new { NotificationId = notificationId, UserId = userId },
commandType: CommandType.StoredProcedure);
}

public async Task<NotificationStatus> CreateAsync(NotificationStatus notificationStatus)
{
await using var connection = new SqlConnection(ConnectionString);

await connection.ExecuteAsync("[dbo].[NotificationStatus_Create]",
notificationStatus, commandType: CommandType.StoredProcedure);

return notificationStatus;
}

public async Task UpdateAsync(NotificationStatus notificationStatus)
{
await using var connection = new SqlConnection(ConnectionString);

await connection.ExecuteAsync("[dbo].[NotificationStatus_Update]",
notificationStatus, commandType: CommandType.StoredProcedure);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Tools.Repositories;
Expand Down Expand Up @@ -89,6 +91,8 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();
services.AddSingleton<INotificationRepository, NotificationRepository>();
services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#nullable enable
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bit.Infrastructure.EntityFramework.NotificationCenter.Configurations;

public class NotificationEntityTypeConfiguration : IEntityTypeConfiguration<Notification>
{
public void Configure(EntityTypeBuilder<Notification> builder)
{
builder
.Property(n => n.Id)
.ValueGeneratedNever();

builder
.HasKey(n => n.Id)
.IsClustered();

builder
.HasIndex(n => new { n.ClientType, n.Global, n.UserId, n.OrganizationId, n.Priority, n.CreationDate })
.IsDescending(false, false, false, false, true, true)
.IsClustered(false);

builder
.HasIndex(n => n.OrganizationId)
.IsClustered(false);

builder
.HasIndex(n => n.UserId)
.IsClustered(false);

builder.ToTable(nameof(Notification));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#nullable enable
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bit.Infrastructure.EntityFramework.NotificationCenter.Configurations;

public class NotificationStatusEntityTypeConfiguration : IEntityTypeConfiguration<NotificationStatus>
{
public void Configure(EntityTypeBuilder<NotificationStatus> builder)
{
builder
.HasKey(ns => new { ns.UserId, ns.NotificationId })
.IsClustered();

builder.ToTable(nameof(NotificationStatus));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.Models;

namespace Bit.Infrastructure.EntityFramework.NotificationCenter.Models;

public class Notification : Core.NotificationCenter.Entities.Notification
{
public virtual User User { get; set; }
public virtual Organization Organization { get; set; }
}

public class NotificationMapperProfile : Profile
{
public NotificationMapperProfile()
{
CreateMap<Core.NotificationCenter.Entities.Notification, Notification>()
.PreserveReferences()
.ReverseMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.Models;

namespace Bit.Infrastructure.EntityFramework.NotificationCenter.Models;

public class NotificationStatus : Core.NotificationCenter.Entities.NotificationStatus
{
public virtual Notification Notification { get; set; }
public virtual User User { get; set; }
}

public class NotificationStatusMapperProfile : Profile
{
public NotificationStatusMapperProfile()
{
CreateMap<Core.NotificationCenter.Entities.NotificationStatus, NotificationStatus>()
.PreserveReferences()
.ReverseMap();
}
}
Loading

0 comments on commit 4c0f8d5

Please sign in to comment.