From af522842bad48a279350177033efbba6d95379a7 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Fri, 11 Oct 2024 20:50:30 +0200 Subject: [PATCH 01/11] RavenDB repository --- .../FluentCMS.Entities/AuditableEntityLog.cs | 3 +- src/Backend/FluentCMS.Entities/Entity.cs | 3 + src/Backend/FluentCMS.Entities/User.cs | 1 + .../ApiTokenRepository.cs | 24 +++ .../AuditableEntityRepository.cs | 64 +++++++ .../BlockRepository.cs | 5 + .../ContentRepository.cs | 18 ++ .../ContentTypeRepository.cs | 14 ++ .../EntityRepository.cs | 156 ++++++++++++++++++ .../FileRepository.cs | 9 + .../FluentCMS.Repositories.RavenDB.csproj | 17 ++ .../FolderRepository.cs | 8 + .../GlobalSettingsRepository.cs | 93 +++++++++++ .../LayoutRepository.cs | 8 + .../PageRepository.cs | 8 + .../PermissionRepository.cs | 39 +++++ .../PluginContentRepository.cs | 20 +++ .../PluginDefinitionRepository.cs | 9 + .../PluginRepository.cs | 62 +++++++ .../RavenDBContext.cs | 41 +++++ .../RavenDBExtensions.cs | 35 ++++ .../RavenDBOptions.cs | 44 +++++ .../RavenDBServiceExtension.cs | 45 +++++ .../RoleRepository.cs | 5 + .../SiteAssociatedRepository.cs | 41 +++++ .../SiteRepository.cs | 17 ++ .../UserRepository.cs | 97 +++++++++++ .../UserRoleRepository.cs | 40 +++++ .../_GlobalUsings.cs | 8 + src/FluentCMS.sln | 8 +- src/FluentCMS/FluentCMS.csproj | 1 + src/FluentCMS/Program.cs | 5 +- src/Shared/DictionaryJsonConverter.cs | 3 + 33 files changed, 947 insertions(+), 4 deletions(-) create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/BlockRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FileRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FluentCMS.Repositories.RavenDB.csproj create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FolderRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/LayoutRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PageRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginDefinitionRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBExtensions.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBOptions.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RoleRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/_GlobalUsings.cs diff --git a/src/Backend/FluentCMS.Entities/AuditableEntityLog.cs b/src/Backend/FluentCMS.Entities/AuditableEntityLog.cs index 14bde57f1..0633878b6 100644 --- a/src/Backend/FluentCMS.Entities/AuditableEntityLog.cs +++ b/src/Backend/FluentCMS.Entities/AuditableEntityLog.cs @@ -1,8 +1,7 @@ namespace FluentCMS.Entities; -public class AuditableEntityLog +public class AuditableEntityLog : Entity { - public Guid Id { get; set; } public string EntityType { get; set; } = default!; public Guid EntityId { get; set; } public string Action { get; set; } = default!; diff --git a/src/Backend/FluentCMS.Entities/Entity.cs b/src/Backend/FluentCMS.Entities/Entity.cs index b420af6c2..dd28878a2 100644 --- a/src/Backend/FluentCMS.Entities/Entity.cs +++ b/src/Backend/FluentCMS.Entities/Entity.cs @@ -2,10 +2,13 @@ public interface IEntity { + string RavenId { get; set; } + Guid Id { get; set; } } public abstract class Entity : IEntity { + public string RavenId { get; set; } public Guid Id { get; set; } } diff --git a/src/Backend/FluentCMS.Entities/User.cs b/src/Backend/FluentCMS.Entities/User.cs index f1c204260..04bb8425d 100644 --- a/src/Backend/FluentCMS.Entities/User.cs +++ b/src/Backend/FluentCMS.Entities/User.cs @@ -4,6 +4,7 @@ namespace FluentCMS.Entities; public class User : IdentityUser, IAuditableEntity { + public string RavenId { get; set; } public DateTime? LoginAt { get; set; } public int LoginCount { get; set; } public DateTime? PasswordChangedAt { get; set; } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs new file mode 100644 index 000000000..2060617dd --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs @@ -0,0 +1,24 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class ApiTokenRepository(IRavenDBContext dbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(dbContext, apiExecutionContext), IApiTokenRepository +{ + public async Task GetByKey(string apiKey, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = dbContext.Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Key == apiKey, cancellationToken); + } + } + + public async Task GetByName(string name, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = dbContext.Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Name == name, cancellationToken); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs new file mode 100644 index 000000000..3b47a2b64 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs @@ -0,0 +1,64 @@ +namespace FluentCMS.Repositories.RavenDB; + +public abstract class AuditableEntityRepository(IRavenDBContext dbContext, IApiExecutionContext apiExecutionContext) : EntityRepository(dbContext), IAuditableEntityRepository where TEntity : IAuditableEntity +{ + protected readonly IApiExecutionContext ApiExecutionContext = apiExecutionContext; + + public override async Task Create(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + SetAuditableFieldsForCreate(entity); + return await base.Create(entity, cancellationToken); + } + + public override async Task> CreateMany(IEnumerable entities, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var entity in entities) + SetAuditableFieldsForCreate(entity); + + return await base.CreateMany(entities, cancellationToken); + } + + public override async Task Update(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); + if (dbEntity == null) + { + SetAuditableFieldsForCreate(entity); + + await session.StoreAsync(entity); + + dbEntity = entity; + } + else + { + entity.CopyProperties(dbEntity); + + SetAuditableFieldsForUpdate(entity, dbEntity); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity; + } + } + + protected void SetAuditableFieldsForCreate(TEntity entity) + { + entity.CreatedAt = DateTime.UtcNow; + entity.CreatedBy = ApiExecutionContext.Username; + } + + protected void SetAuditableFieldsForUpdate(TEntity entity, TEntity oldEntity) + { + entity.CreatedAt = oldEntity.CreatedAt; + entity.CreatedBy = oldEntity.CreatedBy; + entity.ModifiedAt = DateTime.UtcNow; + entity.ModifiedBy = ApiExecutionContext.Username; + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/BlockRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/BlockRepository.cs new file mode 100644 index 000000000..ce612f316 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/BlockRepository.cs @@ -0,0 +1,5 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class BlockRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : SiteAssociatedRepository(RavenDbContext, apiExecutionContext), IBlockRepository +{ +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs new file mode 100644 index 000000000..55d60cd7d --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs @@ -0,0 +1,18 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class ContentRepository(IRavenDBContext dbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(dbContext, apiExecutionContext), IContentRepository +{ + public async Task> GetAll(Guid contentTypeId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entities = await session.Query() + .Where(x => x.TypeId == contentTypeId) + .ToListAsync(cancellationToken); + + return entities.AsEnumerable(); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs new file mode 100644 index 000000000..444ad0a7d --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs @@ -0,0 +1,14 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class ContentTypeRepository(IRavenDBContext dbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(dbContext, apiExecutionContext), IContentTypeRepository +{ + public async Task GetBySlug(string contentTypeSlug, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Slug == contentTypeSlug); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs new file mode 100644 index 000000000..7ed841d3e --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs @@ -0,0 +1,156 @@ +using Raven.Client.Documents.Linq; + +namespace FluentCMS.Repositories.RavenDB; + +public abstract class EntityRepository : IEntityRepository where TEntity : IEntity +{ + //protected readonly IRavenCollection Collection; + //protected readonly IRavenDatabase RavenDatabase; + protected readonly IDocumentStore Store; + + public EntityRepository(IRavenDBContext RavenDbContext) + { + //RavenDatabase = RavenDbContext.Database; + //Collection = RavenDbContext.Database.GetCollection(EntityRepository.GetCollectionName()); + + Store = RavenDbContext.Store; + + // Ensure index on Id field + + + // var indexKeysDefinition = Builders.IndexKeys.Ascending(x => x.Id); + // Collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + } + + // private static string GetCollectionName() + // { + // var entityTypeName = typeof(TEntity).Name; + // return entityTypeName.Pluralize().ToLowerInvariant(); + // } + + public virtual async Task> GetAll(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entities = await session.Query().ToListAsync(cancellationToken); + return entities.AsEnumerable(); + } + } + + public virtual async Task GetById(Guid id, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + } + } + + public virtual async Task> GetByIds(IEnumerable ids, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entities = await session.Query().Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); + return entities.AsEnumerable(); + } + } + + public virtual async Task Create(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + await session.StoreAsync(entity); + + await session.SaveChangesAsync(cancellationToken); + } + + return entity; + } + + public virtual async Task> CreateMany(IEnumerable entities, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + foreach (var entity in entities) + { + cancellationToken.ThrowIfCancellationRequested(); + + await session.StoreAsync(entity); + } + + await session.SaveChangesAsync(cancellationToken); + } + + return entities; + } + + public virtual async Task Update(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); + if (dbEntity == null) + { + await session.StoreAsync(entity); + + dbEntity = entity; + } + else + { + entity.CopyProperties(dbEntity); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity; + } + } + + public virtual async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entity = await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + + session.Delete(entity); + + await session.SaveChangesAsync(cancellationToken); + + return entity; + } + } + + public virtual async Task> DeleteMany(IEnumerable ids, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entities = await session.Query().Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); + + foreach (var entity in entities) + { + cancellationToken.ThrowIfCancellationRequested(); + + session.Delete(entity); + } + + await session.SaveChangesAsync(cancellationToken); + + return entities; + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FileRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FileRepository.cs new file mode 100644 index 000000000..dacccb75f --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FileRepository.cs @@ -0,0 +1,9 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class FileRepository : AuditableEntityRepository, IFileRepository +{ + public FileRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } +} + diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FluentCMS.Repositories.RavenDB.csproj b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FluentCMS.Repositories.RavenDB.csproj new file mode 100644 index 000000000..a1b351130 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FluentCMS.Repositories.RavenDB.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + disable + enable + + + + + + + + + + + diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FolderRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FolderRepository.cs new file mode 100644 index 000000000..9ab6e8c15 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/FolderRepository.cs @@ -0,0 +1,8 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class FolderRepository : AuditableEntityRepository, IFolderRepository +{ + public FolderRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs new file mode 100644 index 000000000..4ef2d3e83 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs @@ -0,0 +1,93 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class GlobalSettingsRepository : IGlobalSettingsRepository +{ + private readonly IApiExecutionContext _apiExecutionContext; + private readonly IDocumentStore _store; + + public GlobalSettingsRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) + { + _apiExecutionContext = apiExecutionContext; + _store = RavenDbContext.Store; + } + + public async Task Get(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = _store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(cancellationToken); + } + } + + public async Task Update(GlobalSettings settings, CancellationToken cancellationToken = default) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = _store.OpenAsyncSession()) + { + var dbEntity = await session.Query().SingleOrDefaultAsync(cancellationToken); + if (dbEntity == null) + { + SetAuditableFieldsForCreate(settings); + + await session.StoreAsync(settings); + + dbEntity = settings; + } + else + { + settings.CopyProperties(dbEntity); + + SetAuditableFieldsForUpdate(dbEntity); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity; + } + } + + public async Task Initialized(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var existing = await Get(cancellationToken); + + return existing?.Initialized ?? false; + } + + private async Task Create(GlobalSettings settings, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + SetAuditableFieldsForCreate(settings); + using (var session = _store.OpenAsyncSession()) + { + await session.StoreAsync(settings); + + await session.SaveChangesAsync(cancellationToken); + } + + return settings; + } + + private void SetAuditableFieldsForCreate(GlobalSettings settings) + { + settings.Id = Guid.NewGuid(); + settings.CreatedAt = DateTime.UtcNow; + settings.CreatedBy = _apiExecutionContext.Username; + } + + private void SetAuditableFieldsForUpdate(GlobalSettings settings) + { + settings.ModifiedAt = DateTime.UtcNow; + settings.ModifiedBy = _apiExecutionContext.Username; + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/LayoutRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/LayoutRepository.cs new file mode 100644 index 000000000..c92cd21d4 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/LayoutRepository.cs @@ -0,0 +1,8 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class LayoutRepository : SiteAssociatedRepository, ILayoutRepository +{ + public LayoutRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PageRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PageRepository.cs new file mode 100644 index 000000000..e396c1892 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PageRepository.cs @@ -0,0 +1,8 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class PageRepository : SiteAssociatedRepository, IPageRepository +{ + public PageRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs new file mode 100644 index 000000000..a695161dc --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs @@ -0,0 +1,39 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class PermissionRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : SiteAssociatedRepository(RavenDbContext, apiExecutionContext), IPermissionRepository +{ + public async Task> Set(Guid siteId, Guid entityId, string entityTypeName, string action, IEnumerable roleIds, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + // Delete existing permissions + var existingPermissions = await session.Query().Where(x => x.EntityId == entityId && x.EntityType == entityTypeName && x.Action == action).ToListAsync(cancellationToken); + foreach (var existingPermission in existingPermissions) + { + session.Delete(existingPermission); + } + + // Create new permissions + var permissions = roleIds.Select(x => new Permission + { + EntityType = entityTypeName, + Action = action, + EntityId = entityId, + RoleId = x, + SiteId = siteId + }); + + // Save the new permissions + foreach (var permission in permissions) + { + await session.StoreAsync(permission, cancellationToken); + } + + await session.SaveChangesAsync(cancellationToken); + + return permissions; + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs new file mode 100644 index 000000000..7c1db5541 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs @@ -0,0 +1,20 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class PluginContentRepository : SiteAssociatedRepository, IPluginContentRepository +{ + public PluginContentRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } + + public async Task> GetByPluginId(Guid pluginId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var qres = await session.Query().Where(x => x.PluginId == pluginId).ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginDefinitionRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginDefinitionRepository.cs new file mode 100644 index 000000000..64ed7fd84 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginDefinitionRepository.cs @@ -0,0 +1,9 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class PluginDefinitionRepository : AuditableEntityRepository, IPluginDefinitionRepository +{ + public PluginDefinitionRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } + +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs new file mode 100644 index 000000000..605bd681e --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs @@ -0,0 +1,62 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class PluginRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : SiteAssociatedRepository(RavenDbContext, apiExecutionContext), IPluginRepository +{ + public async Task> GetByPageId(Guid pageId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var qres = await session.Query().Where(x => x.PageId == pageId).ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } + + public async Task UpdateOrder(Guid pluginId, string section, int order, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var plugin = await GetById(pluginId, cancellationToken); + + if (plugin != null) + { + plugin.Order = order; + plugin.Section = section; + + return await Update(plugin, cancellationToken); + } + return default; + } + + public async Task UpdateCols(Guid pluginId, int cols, int colsMd, int colsLg, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var plugin = await GetById(pluginId, cancellationToken); + + if (plugin != null) + { + plugin.Cols = cols; + plugin.ColsMd = colsMd; + plugin.ColsLg = colsLg; + return await Update(plugin, cancellationToken); + } + return default; + } + + public async Task UpdateSettings(Guid pluginId, Dictionary settings, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var plugin = await GetById(pluginId, cancellationToken); + + if (plugin != null) + { + plugin.Settings = settings; + return await Update(plugin, cancellationToken); + } + return default; + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs new file mode 100644 index 000000000..f909be952 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs @@ -0,0 +1,41 @@ +using System.Security.Cryptography.X509Certificates; + +namespace FluentCMS.Repositories.RavenDB; + +public interface IRavenDBContext +{ + IDocumentStore Store { get; } +} + +public class RavenDBContext : IRavenDBContext +{ + public RavenDBContext(RavenDBOptions options) + { + // Validate input arguments to ensure they are not null. + ArgumentNullException.ThrowIfNull(options, nameof(options)); + ArgumentNullException.ThrowIfNull(options.URL, nameof(options.URL)); + ArgumentNullException.ThrowIfNull(options.DatabaseName, nameof(options.DatabaseName)); + ArgumentNullException.ThrowIfNull(options.CertificatePath, nameof(options.CertificatePath)); + + if (System.IO.File.Exists(options.CertificatePath) == false) + throw new ArgumentException($"Certificate '{options.CertificatePath}' not found!"); + + Store = new DocumentStore() + { + Urls = [options.URL], + Database = options.DatabaseName, + Certificate = new X509Certificate2(options.CertificatePath), + + // Set conventions as necessary (optional) + Conventions = + { + MaxNumberOfRequestsPerSession = 10, + UseOptimisticConcurrency = true, + AddIdFieldToDynamicObjects = false, + FindIdentityProperty = memberInfo => memberInfo.Name == "RavenId", + }, + }.Initialize(); + } + + public IDocumentStore Store { get; private set; } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBExtensions.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBExtensions.cs new file mode 100644 index 000000000..3d1ea786e --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBExtensions.cs @@ -0,0 +1,35 @@ +using System.Reflection; + +namespace FluentCMS.Repositories.RavenDB; + +public static class RavenDBExtensions +{ + /// + /// Extension for 'IEntity' that copies the properties to a destination object. + /// + /// The source. + /// The destination. + public static void CopyProperties(this IEntity source, IEntity destination) + { + // If any this null throw an exception + if (source == null || destination == null) + throw new Exception("Source or/and Destination Objects are null"); + // Getting the Types of the objects + Type typeDest = destination.GetType(); + Type typeSrc = source.GetType(); + // Collect all the valid properties to map + var results = from srcProp in typeSrc.GetProperties() + let targetProperty = typeDest.GetProperty(srcProp.Name) + where srcProp.CanRead + && targetProperty != null + && targetProperty.GetSetMethod(true) != null && !targetProperty.GetSetMethod(true).IsPrivate + && (targetProperty.GetSetMethod().Attributes & MethodAttributes.Static) == 0 + && targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType) + select new { sourceProperty = srcProp, targetProperty = targetProperty }; + //map the properties + foreach (var props in results) + { + props.targetProperty.SetValue(destination, props.sourceProperty.GetValue(source, null), null); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBOptions.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBOptions.cs new file mode 100644 index 000000000..4b89ee7a7 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBOptions.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; + +namespace FluentCMS.Repositories.RavenDB; + +public class RavenDBOptions where TContext : IRavenDBContext +{ + public RavenDBOptions(string connectionString) + { + // Guard clause to ensure the connection string is not null or empty + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString)); + + // Split connection string by ; and = to get name value pairs + var value = connectionString.Split(';') + .Select(x => x.Split('=')) + .ToDictionary(s => s.First(), s => s.Last()); + + if (value.ContainsKey("URL") == false || string.IsNullOrWhiteSpace(value["URL"])) + throw new ArgumentException("URL is missing or empty", nameof(URL)); + + if (value.ContainsKey("DatabaseName") == false || string.IsNullOrWhiteSpace(value["DatabaseName"])) + throw new ArgumentException("DatabaseName is missing or empty", nameof(DatabaseName)); + + if (value.ContainsKey("CertificatePath") == false || string.IsNullOrWhiteSpace(value["CertificatePath"])) + throw new ArgumentException("CertificatePath is missing or empty", nameof(CertificatePath)); + + URL = value["URL"]; + DatabaseName = value["DatabaseName"]; + CertificatePath = value["CertificatePath"]; + } + + public string URL { get; } + + /// + /// Name of database to use + /// + public string DatabaseName { get; } + + /// + /// Path to pfx certificate + /// + public string CertificatePath { get; } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs new file mode 100644 index 000000000..bab18ba0f --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs @@ -0,0 +1,45 @@ +using System; +using FluentCMS.Repositories.RavenDB; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class RavenDBServiceExtension +{ + public static IServiceCollection AddRavenDbRepositories(this IServiceCollection services, string connectionString) + { + // Register MongoDB context and options + services.AddSingleton(provider => CreateRavenDBOptions(provider, connectionString)); + services.AddSingleton(); + + // Register repositories + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + + private static RavenDBOptions CreateRavenDBOptions(IServiceProvider provider, string connectionString) + { + var configuration = provider.GetService() ?? throw new InvalidOperationException("IConfiguration is not registered."); + var connString = configuration.GetConnectionString(connectionString); + return connString is not null + ? new RavenDBOptions(connString) + : throw new InvalidOperationException($"Connection string '{connectionString}' not found."); + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RoleRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RoleRepository.cs new file mode 100644 index 000000000..90a5cf493 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RoleRepository.cs @@ -0,0 +1,5 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class RoleRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : SiteAssociatedRepository(RavenDbContext, apiExecutionContext), IRoleRepository +{ +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs new file mode 100644 index 000000000..d9c9c7ac5 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs @@ -0,0 +1,41 @@ +namespace FluentCMS.Repositories.RavenDB; + +public abstract class SiteAssociatedRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(RavenDbContext, apiExecutionContext), ISiteAssociatedRepository where TEntity : ISiteAssociatedEntity +{ + public override async Task Update(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var existing = await GetById(entity.Id, cancellationToken); + if (existing is null) + return default; + + SetAuditableFieldsForUpdate(entity, existing); + + entity.SiteId = existing.SiteId; + + return await base.Update(entity, cancellationToken); + } + + public async Task> GetAllForSite(Guid siteId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var qres = await session.Query().Where(x => x.SiteId == siteId).ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } + + public async Task GetByIdForSite(Guid id, Guid siteId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Id == id && x.SiteId == siteId, cancellationToken); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs new file mode 100644 index 000000000..9a72d2f97 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs @@ -0,0 +1,17 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class SiteRepository : AuditableEntityRepository, ISiteRepository +{ + public SiteRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : base(RavenDbContext, apiExecutionContext) + { + } + public async Task GetByUrl(string url, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Urls.Contains(url), cancellationToken); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs new file mode 100644 index 000000000..dfe809c3d --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs @@ -0,0 +1,97 @@ +using System.Security.Claims; + +namespace FluentCMS.Repositories.RavenDB; + +public class UserRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(RavenDbContext, apiExecutionContext), IUserRepository +{ + override public async Task GetById(Guid id, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + } + } + + override public async Task Update(User entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); + if (dbEntity == null) + { + SetAuditableFieldsForCreate(entity); + + await session.StoreAsync(entity); + + dbEntity = entity; + } + else + { + entity.CopyProperties(dbEntity); + + SetAuditableFieldsForUpdate(entity, dbEntity); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity; + } + } + + public async Task> GetUsersForClaim(Claim claim, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var users = session.Query().Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)).ToListAsync(); + + return await users; + } + } + + public async Task FindByEmail(string normalizedEmail, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().SingleOrDefaultAsync(x => x.NormalizedEmail == normalizedEmail); + } + } + + public async Task FindByLogin(string loginProvider, string providerKey, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().FirstOrDefaultAsync(user => user.Logins.Any(x => x.LoginProvider == loginProvider && x.ProviderKey == providerKey)); + } + } + + public async Task FindByName(string normalizedUserName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + return await session.Query().FirstOrDefaultAsync(x => x.NormalizedUserName == normalizedUserName); + } + } + + public IQueryable AsQueryable() + { + using (var session = Store.OpenSession()) + { + // TODO: Not good to load all user to list and the query them. But difficult to use sessions otherwise. + var entities = session.Query().ToList(); + return entities.AsQueryable(); + } + } + +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs new file mode 100644 index 000000000..e94dbac2e --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs @@ -0,0 +1,40 @@ +namespace FluentCMS.Repositories.RavenDB; + +public class UserRoleRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : SiteAssociatedRepository(RavenDbContext, apiExecutionContext), IUserRoleRepository +{ + public async Task> GetUserRoles(Guid userId, Guid siteId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var qres = await session.Query().Where(x => x.SiteId == siteId && x.UserId == userId).ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } + + public async Task> GetByRoleId(Guid roleId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var qres = await session.Query().Where(x => x.RoleId == roleId).ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } + + public async Task> GetByUserId(Guid userId, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var qres = await session.Query().Where(x => x.UserId == userId).ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/_GlobalUsings.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/_GlobalUsings.cs new file mode 100644 index 000000000..65bc19a0f --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/_GlobalUsings.cs @@ -0,0 +1,8 @@ +global using FluentCMS.Entities; +global using FluentCMS.Repositories.Abstractions; +global using Raven.Client.Documents; +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/src/FluentCMS.sln b/src/FluentCMS.sln index c4eb1e849..10151ed56 100644 --- a/src/FluentCMS.sln +++ b/src/FluentCMS.sln @@ -86,6 +86,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Providers.CachePr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Providers.CacheProviders.Abstractions", "Providers\CacheProviders\FluentCMS.Providers.CacheProviders.Abstractions\FluentCMS.Providers.CacheProviders.Abstractions.csproj", "{97999608-B9AD-46B4-AF10-311B109A90BE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Repositories.RavenDB", "Backend\Repositories\FluentCMS.Repositories.RavenDB\FluentCMS.Repositories.RavenDB.csproj", "{0D9836A4-5C0B-48F8-BF02-40028F836584}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -216,6 +218,10 @@ Global {97999608-B9AD-46B4-AF10-311B109A90BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {97999608-B9AD-46B4-AF10-311B109A90BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {97999608-B9AD-46B4-AF10-311B109A90BE}.Release|Any CPU.Build.0 = Release|Any CPU + {0D9836A4-5C0B-48F8-BF02-40028F836584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D9836A4-5C0B-48F8-BF02-40028F836584}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D9836A4-5C0B-48F8-BF02-40028F836584}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D9836A4-5C0B-48F8-BF02-40028F836584}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -236,7 +242,6 @@ Global {70B5742E-C249-4B51-985C-2B4B7BDAB489} = {BB5FD6A0-0840-4381-AB68-AD61499368ED} {818F8CD0-5218-4480-A541-BD9D59416CDE} = {BB5FD6A0-0840-4381-AB68-AD61499368ED} {9D8B6051-BE99-42E4-B569-43ABA3A9510D} = {BB5FD6A0-0840-4381-AB68-AD61499368ED} - {54DBC633-2A8D-4531-AA22-23824C6CFCD9} = {BB5FD6A0-0840-4381-AB68-AD61499368ED} {0A4945C1-1089-4331-B9F4-2AB98BCB4D39} = {CA060C96-322D-4410-8D32-0AF5DA8A975C} {6BF48DBC-42A7-412E-8894-39E61BA7EF8A} = {CA060C96-322D-4410-8D32-0AF5DA8A975C} {ACD2C7B7-2227-4E25-88F8-8B45BCE89584} = {CA060C96-322D-4410-8D32-0AF5DA8A975C} @@ -258,6 +263,7 @@ Global {016945AB-5C4D-42A1-81B9-8C12FC6E40E6} = {CA060C96-322D-4410-8D32-0AF5DA8A975C} {6FEFA4A7-CF69-44CD-BD03-AC5C702B7BA0} = {016945AB-5C4D-42A1-81B9-8C12FC6E40E6} {97999608-B9AD-46B4-AF10-311B109A90BE} = {016945AB-5C4D-42A1-81B9-8C12FC6E40E6} + {0D9836A4-5C0B-48F8-BF02-40028F836584} = {1CC73AD2-CFC9-4FE7-96C0-0E4FEFBB66F1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E9F4217-7A58-48A4-9850-84CD0CDA31DA} diff --git a/src/FluentCMS/FluentCMS.csproj b/src/FluentCMS/FluentCMS.csproj index ab548e33f..dcb8eebcb 100644 --- a/src/FluentCMS/FluentCMS.csproj +++ b/src/FluentCMS/FluentCMS.csproj @@ -25,6 +25,7 @@ + diff --git a/src/FluentCMS/Program.cs b/src/FluentCMS/Program.cs index d4c117c8d..12a4f3b8b 100644 --- a/src/FluentCMS/Program.cs +++ b/src/FluentCMS/Program.cs @@ -10,11 +10,14 @@ services.AddCmsServices(configuration); // Use LiteDB as database -services.AddLiteDbRepositories("LiteDb"); +//services.AddLiteDbRepositories("LiteDb"); // Use MongoDB as database //services.AddMongoDbRepositories("MongoDb"); +// Use RavenDB as database +services.AddRavenDbRepositories("RavenDB"); + // Enable caching for repository layer services.AddCachedRepositories(); diff --git a/src/Shared/DictionaryJsonConverter.cs b/src/Shared/DictionaryJsonConverter.cs index aa1e25d1d..3b3bfb505 100644 --- a/src/Shared/DictionaryJsonConverter.cs +++ b/src/Shared/DictionaryJsonConverter.cs @@ -118,6 +118,9 @@ private void WriteValue(Utf8JsonWriter writer, object? value, JsonSerializerOpti case double doubleValue: writer.WriteNumberValue(doubleValue); break; + case decimal decimalValue: + writer.WriteNumberValue(decimalValue); + break; case int intValue: writer.WriteNumberValue(intValue); break; From 86e7d3667aa84952a8bdced62020ea54ad64fe90 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Fri, 11 Oct 2024 22:31:10 +0200 Subject: [PATCH 02/11] Set Id when empty --- .gitignore | 1 + src/Backend/FluentCMS.Entities/Entity.cs | 5 +++ .../AuditableEntityRepository.cs | 17 ++++++++-- .../EntityRepository.cs | 31 ++++++++++--------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index e9150297a..eecd24dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ ################################################################################ /.vs +.DS_Store diff --git a/src/Backend/FluentCMS.Entities/Entity.cs b/src/Backend/FluentCMS.Entities/Entity.cs index dd28878a2..667b6b598 100644 --- a/src/Backend/FluentCMS.Entities/Entity.cs +++ b/src/Backend/FluentCMS.Entities/Entity.cs @@ -2,6 +2,11 @@ public interface IEntity { + /// + /// RavenDB cannot use Guid as Id and will automatically try to set an internal id for all documents. + /// To override that we need another property to store the internal RavenId. + /// This Id is not used elsewhere in the program. + /// string RavenId { get; set; } Guid Id { get; set; } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs index 3b47a2b64..e45b20708 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs @@ -26,7 +26,8 @@ public override async Task> CreateMany(IEnumerable using (var session = Store.OpenAsyncSession()) { - var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); + var id = entity.Id; + var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); if (dbEntity == null) { SetAuditableFieldsForCreate(entity); @@ -38,24 +39,34 @@ public override async Task> CreateMany(IEnumerable else { entity.CopyProperties(dbEntity); - + SetAuditableFieldsForUpdate(entity, dbEntity); } await session.SaveChangesAsync(cancellationToken); - + return dbEntity; } } protected void SetAuditableFieldsForCreate(TEntity entity) { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + entity.CreatedAt = DateTime.UtcNow; entity.CreatedBy = ApiExecutionContext.Username; } protected void SetAuditableFieldsForUpdate(TEntity entity, TEntity oldEntity) { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + entity.CreatedAt = oldEntity.CreatedAt; entity.CreatedBy = oldEntity.CreatedBy; entity.ModifiedAt = DateTime.UtcNow; diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs index 7ed841d3e..e9dc56d41 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs @@ -4,30 +4,17 @@ namespace FluentCMS.Repositories.RavenDB; public abstract class EntityRepository : IEntityRepository where TEntity : IEntity { - //protected readonly IRavenCollection Collection; - //protected readonly IRavenDatabase RavenDatabase; protected readonly IDocumentStore Store; public EntityRepository(IRavenDBContext RavenDbContext) { - //RavenDatabase = RavenDbContext.Database; - //Collection = RavenDbContext.Database.GetCollection(EntityRepository.GetCollectionName()); - Store = RavenDbContext.Store; // Ensure index on Id field - - // var indexKeysDefinition = Builders.IndexKeys.Ascending(x => x.Id); // Collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); } - // private static string GetCollectionName() - // { - // var entityTypeName = typeof(TEntity).Name; - // return entityTypeName.Pluralize().ToLowerInvariant(); - // } - public virtual async Task> GetAll(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -66,6 +53,11 @@ public virtual async Task> GetByIds(IEnumerable ids, using (var session = Store.OpenAsyncSession()) { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + await session.StoreAsync(entity); await session.SaveChangesAsync(cancellationToken); @@ -84,6 +76,11 @@ public virtual async Task> CreateMany(IEnumerable { cancellationToken.ThrowIfCancellationRequested(); + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + await session.StoreAsync(entity); } @@ -99,9 +96,15 @@ public virtual async Task> CreateMany(IEnumerable using (var session = Store.OpenAsyncSession()) { - var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); + var id = entity.Id; + var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); if (dbEntity == null) { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + await session.StoreAsync(entity); dbEntity = entity; From fa2f36e0d366400addb2f21ed85a61c2b337bd94 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Sat, 12 Oct 2024 17:32:58 +0200 Subject: [PATCH 03/11] Reset to default --- src/FluentCMS/Program.cs | 4 ++-- src/FluentCMS/appsettings.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/FluentCMS/Program.cs b/src/FluentCMS/Program.cs index 12a4f3b8b..d74c04d89 100644 --- a/src/FluentCMS/Program.cs +++ b/src/FluentCMS/Program.cs @@ -10,13 +10,13 @@ services.AddCmsServices(configuration); // Use LiteDB as database -//services.AddLiteDbRepositories("LiteDb"); +services.AddLiteDbRepositories("LiteDb"); // Use MongoDB as database //services.AddMongoDbRepositories("MongoDb"); // Use RavenDB as database -services.AddRavenDbRepositories("RavenDB"); +//services.AddRavenDbRepositories("RavenDB"); // Enable caching for repository layer services.AddCachedRepositories(); diff --git a/src/FluentCMS/appsettings.json b/src/FluentCMS/appsettings.json index 8a1a2b80a..36e460a65 100644 --- a/src/FluentCMS/appsettings.json +++ b/src/FluentCMS/appsettings.json @@ -12,7 +12,8 @@ }, "ConnectionStrings": { "LiteDb": "./LiteDb.db", - "MongoDb": "mongodb://localhost:27017/FluentCMS" + "MongoDb": "mongodb://localhost:27017/FluentCMS", + "RavenDB": "URL=https://localhost:1234;DatabaseName=FluentCMS;CertificatePath=cert.pfx" }, "Providers": { "Email": { From 5351d167716989e41c4a2540013dffd326d3f2b7 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Sat, 12 Oct 2024 17:55:18 +0200 Subject: [PATCH 04/11] Remove UpdateSettings --- .../PluginRepository.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs index 605bd681e..d692c115e 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs @@ -45,18 +45,4 @@ public async Task> GetByPageId(Guid pageId, CancellationToke } return default; } - - public async Task UpdateSettings(Guid pluginId, Dictionary settings, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - var plugin = await GetById(pluginId, cancellationToken); - - if (plugin != null) - { - plugin.Settings = settings; - return await Update(plugin, cancellationToken); - } - return default; - } } From 224dc376eac1d9f7646f54ba34271e54e1884cff Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Sun, 13 Oct 2024 17:38:31 +0200 Subject: [PATCH 05/11] Added repository for Settings --- .../RavenDBServiceExtension.cs | 1 + .../SettingsRepository.cs | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs index bab18ba0f..7663285dc 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs @@ -28,6 +28,7 @@ public static IServiceCollection AddRavenDbRepositories(this IServiceCollection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs new file mode 100644 index 000000000..b63cc0db6 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs @@ -0,0 +1,48 @@ +using System; + +namespace FluentCMS.Repositories.RavenDB; + +public class SettingsRepository : AuditableEntityRepository, ISettingsRepository +{ + public SettingsRepository(IRavenDBContext dbContext, IApiExecutionContext apiExecutionContext) : base(dbContext, apiExecutionContext) + { + } + + public async Task Update(Guid entityId, Dictionary settings, CancellationToken cancellationToken = default) + { + using (var session = Store.OpenAsyncSession()) + { + var existing = await session.Query().SingleOrDefaultAsync(x => x.Id == entityId, cancellationToken); + + if (existing == null) + { + if (entityId == Guid.Empty) + { + entityId = Guid.NewGuid(); + } + + existing = new Settings() + { + Id = entityId, + Values = settings + }; + + SetAuditableFieldsForCreate(existing); + + await session.StoreAsync(existing); + } + else + { + // add new settings to existing settings or update existing settings + foreach (var setting in settings) + existing.Values[setting.Key] = setting.Value; + + SetAuditableFieldsForUpdate(existing, existing); + } + + await session.SaveChangesAsync(); + + return existing; + } + } +} From c1757b22348e51bf4474a3ca239320bd6b3d626a Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Tue, 15 Oct 2024 20:44:47 +0200 Subject: [PATCH 06/11] Use wrapper for RavenId --- src/Backend/FluentCMS.Entities/Entity.cs | 8 -- src/Backend/FluentCMS.Entities/User.cs | 1 - .../PluginDefinitionCreateRequest.cs | 4 +- .../ApiTokenRepository.cs | 12 ++- .../AuditableEntityRepository.cs | 12 +-- .../ContentRepository.cs | 5 +- .../ContentTypeRepository.cs | 4 +- .../EntityRepository.cs | 39 +++++--- .../GlobalSettingsRepository.cs | 38 ++++---- .../PermissionRepository.cs | 10 +- .../PluginContentRepository.cs | 5 +- .../PluginRepository.cs | 44 ++++++--- .../RavenEntity.cs | 15 +++ .../SettingsRepository.cs | 16 ++-- .../SiteAssociatedRepository.cs | 36 ++++--- .../SiteRepository.cs | 4 +- .../UserRepository.cs | 94 +++++++++++-------- .../UserRoleRepository.cs | 15 ++- 18 files changed, 225 insertions(+), 137 deletions(-) create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenEntity.cs diff --git a/src/Backend/FluentCMS.Entities/Entity.cs b/src/Backend/FluentCMS.Entities/Entity.cs index 667b6b598..b420af6c2 100644 --- a/src/Backend/FluentCMS.Entities/Entity.cs +++ b/src/Backend/FluentCMS.Entities/Entity.cs @@ -2,18 +2,10 @@ public interface IEntity { - /// - /// RavenDB cannot use Guid as Id and will automatically try to set an internal id for all documents. - /// To override that we need another property to store the internal RavenId. - /// This Id is not used elsewhere in the program. - /// - string RavenId { get; set; } - Guid Id { get; set; } } public abstract class Entity : IEntity { - public string RavenId { get; set; } public Guid Id { get; set; } } diff --git a/src/Backend/FluentCMS.Entities/User.cs b/src/Backend/FluentCMS.Entities/User.cs index 04bb8425d..f1c204260 100644 --- a/src/Backend/FluentCMS.Entities/User.cs +++ b/src/Backend/FluentCMS.Entities/User.cs @@ -4,7 +4,6 @@ namespace FluentCMS.Entities; public class User : IdentityUser, IAuditableEntity { - public string RavenId { get; set; } public DateTime? LoginAt { get; set; } public int LoginCount { get; set; } public DateTime? PasswordChangedAt { get; set; } diff --git a/src/Backend/FluentCMS.Web.Api/Models/PluginDefinition/PluginDefinitionCreateRequest.cs b/src/Backend/FluentCMS.Web.Api/Models/PluginDefinition/PluginDefinitionCreateRequest.cs index 25fe91a4e..232fd6cf3 100644 --- a/src/Backend/FluentCMS.Web.Api/Models/PluginDefinition/PluginDefinitionCreateRequest.cs +++ b/src/Backend/FluentCMS.Web.Api/Models/PluginDefinition/PluginDefinitionCreateRequest.cs @@ -4,6 +4,8 @@ public class PluginDefinitionCreateRequest { public string Name { get; set; } = default!; public string Category { get; set; } = default!; + public string Assembly { get; set; } = default!; public string? Description { get; set; } - public string Type { get; set; } = default!; + public IEnumerable Types { get; set; } = []; + public bool Locked { get; set; } = false; } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs index 2060617dd..21884edc2 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs @@ -6,9 +6,11 @@ public class ApiTokenRepository(IRavenDBContext dbContext, IApiExecutionContext { cancellationToken.ThrowIfCancellationRequested(); - using (var session = dbContext.Store.OpenAsyncSession()) + using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.Key == apiKey, cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Key == apiKey, cancellationToken); + + return entity?.Data; } } @@ -16,9 +18,11 @@ public class ApiTokenRepository(IRavenDBContext dbContext, IApiExecutionContext { cancellationToken.ThrowIfCancellationRequested(); - using (var session = dbContext.Store.OpenAsyncSession()) + using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.Name == name, cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Name == name, cancellationToken); + + return entity?.Data; } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs index e45b20708..c27e3ae4e 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs @@ -27,25 +27,25 @@ public override async Task> CreateMany(IEnumerable using (var session = Store.OpenAsyncSession()) { var id = entity.Id; - var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); if (dbEntity == null) { SetAuditableFieldsForCreate(entity); - await session.StoreAsync(entity); + dbEntity = new RavenEntity(entity); - dbEntity = entity; + await session.StoreAsync(dbEntity, cancellationToken); } else { - entity.CopyProperties(dbEntity); + entity.CopyProperties(dbEntity.Data); - SetAuditableFieldsForUpdate(entity, dbEntity); + SetAuditableFieldsForUpdate(entity, dbEntity.Data); } await session.SaveChangesAsync(cancellationToken); - return dbEntity; + return dbEntity.Data; } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs index 55d60cd7d..905d30f39 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs @@ -8,8 +8,9 @@ public async Task> GetAll(Guid contentTypeId, CancellationT using (var session = Store.OpenAsyncSession()) { - var entities = await session.Query() - .Where(x => x.TypeId == contentTypeId) + var entities = await session.Query>() + .Where(x => x.Data.TypeId == contentTypeId) + .Select(x => x.Data) .ToListAsync(cancellationToken); return entities.AsEnumerable(); diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs index 444ad0a7d..cddf3327b 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs @@ -8,7 +8,9 @@ public class ContentTypeRepository(IRavenDBContext dbContext, IApiExecutionConte using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.Slug == contentTypeSlug); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Slug == contentTypeSlug, cancellationToken); + + return entity?.Data; } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs index e9dc56d41..80f5e4012 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs @@ -21,7 +21,10 @@ public virtual async Task> GetAll(CancellationToken cancell using (var session = Store.OpenAsyncSession()) { - var entities = await session.Query().ToListAsync(cancellationToken); + var entities = await session.Query>() + .Select(x => x.Data) + .ToListAsync(cancellationToken); + return entities.AsEnumerable(); } } @@ -32,7 +35,9 @@ public virtual async Task> GetAll(CancellationToken cancell using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); + + return entity.Data; } } @@ -42,7 +47,10 @@ public virtual async Task> GetByIds(IEnumerable ids, using (var session = Store.OpenAsyncSession()) { - var entities = await session.Query().Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); + var entities = await session.Query>().Where(x => ids.Contains(x.Data.Id)) + .Select(x => x.Data) + .ToListAsync(cancellationToken); + return entities.AsEnumerable(); } } @@ -58,7 +66,7 @@ public virtual async Task> GetByIds(IEnumerable ids, entity.Id = Guid.NewGuid(); } - await session.StoreAsync(entity); + await session.StoreAsync(new RavenEntity(entity), cancellationToken); await session.SaveChangesAsync(cancellationToken); } @@ -81,7 +89,7 @@ public virtual async Task> CreateMany(IEnumerable entity.Id = Guid.NewGuid(); } - await session.StoreAsync(entity); + await session.StoreAsync(new RavenEntity(entity), cancellationToken); } await session.SaveChangesAsync(cancellationToken); @@ -96,8 +104,9 @@ public virtual async Task> CreateMany(IEnumerable using (var session = Store.OpenAsyncSession()) { - var id = entity.Id; - var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + var id = entity.Id; // Needs to be extracted to guid to avoid type casts in query. + + var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); if (dbEntity == null) { if (entity.Id == Guid.Empty) @@ -105,18 +114,18 @@ public virtual async Task> CreateMany(IEnumerable entity.Id = Guid.NewGuid(); } - await session.StoreAsync(entity); + dbEntity = new RavenEntity(entity); - dbEntity = entity; + await session.StoreAsync(dbEntity, cancellationToken); } else { - entity.CopyProperties(dbEntity); + entity.CopyProperties(dbEntity.Data); } await session.SaveChangesAsync(cancellationToken); - return dbEntity; + return dbEntity.Data; } } @@ -126,13 +135,13 @@ public virtual async Task> CreateMany(IEnumerable using (var session = Store.OpenAsyncSession()) { - var entity = await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); session.Delete(entity); await session.SaveChangesAsync(cancellationToken); - return entity; + return entity.Data; // A bit strange to return to object we just deleted. } } @@ -142,7 +151,7 @@ public virtual async Task> DeleteMany(IEnumerable ids using (var session = Store.OpenAsyncSession()) { - var entities = await session.Query().Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); + var entities = await session.Query>().Where(x => ids.Contains(x.Data.Id)).ToListAsync(cancellationToken); foreach (var entity in entities) { @@ -153,7 +162,7 @@ public virtual async Task> DeleteMany(IEnumerable ids await session.SaveChangesAsync(cancellationToken); - return entities; + return entities.Select(x => x.Data); } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs index 4ef2d3e83..fda207102 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs @@ -17,7 +17,9 @@ public GlobalSettingsRepository(IRavenDBContext RavenDbContext, IApiExecutionCon using (var session = _store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(cancellationToken); + + return entity?.Data; } } @@ -32,25 +34,25 @@ public GlobalSettingsRepository(IRavenDBContext RavenDbContext, IApiExecutionCon using (var session = _store.OpenAsyncSession()) { - var dbEntity = await session.Query().SingleOrDefaultAsync(cancellationToken); + var dbEntity = await session.Query>().SingleOrDefaultAsync(cancellationToken); if (dbEntity == null) { SetAuditableFieldsForCreate(settings); - await session.StoreAsync(settings); + dbEntity = new RavenEntity(settings); - dbEntity = settings; + await session.StoreAsync(dbEntity, cancellationToken); } else { - settings.CopyProperties(dbEntity); + settings.CopyProperties(dbEntity.Data); - SetAuditableFieldsForUpdate(dbEntity); + SetAuditableFieldsForUpdate(dbEntity.Data); } await session.SaveChangesAsync(cancellationToken); - return dbEntity; + return dbEntity?.Data; } } @@ -63,20 +65,20 @@ public async Task Initialized(CancellationToken cancellationToken = defaul return existing?.Initialized ?? false; } - private async Task Create(GlobalSettings settings, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); + // private async Task Create(GlobalSettings settings, CancellationToken cancellationToken = default) + // { + // cancellationToken.ThrowIfCancellationRequested(); - SetAuditableFieldsForCreate(settings); - using (var session = _store.OpenAsyncSession()) - { - await session.StoreAsync(settings); + // SetAuditableFieldsForCreate(settings); + // using (var session = _store.OpenAsyncSession()) + // { + // await session.StoreAsync(settings, settings.Id.ToString(), cancellationToken); - await session.SaveChangesAsync(cancellationToken); - } + // await session.SaveChangesAsync(cancellationToken); + // } - return settings; - } + // return settings; + // } private void SetAuditableFieldsForCreate(GlobalSettings settings) { diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs index a695161dc..d7786fd13 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs @@ -9,7 +9,10 @@ public async Task> Set(Guid siteId, Guid entityId, strin using (var session = Store.OpenAsyncSession()) { // Delete existing permissions - var existingPermissions = await session.Query().Where(x => x.EntityId == entityId && x.EntityType == entityTypeName && x.Action == action).ToListAsync(cancellationToken); + var existingPermissions = await session.Query>() + .Where(x => x.Data.EntityId == entityId && x.Data.EntityType == entityTypeName && x.Data.Action == action) + .ToListAsync(cancellationToken); + foreach (var existingPermission in existingPermissions) { session.Delete(existingPermission); @@ -18,6 +21,7 @@ public async Task> Set(Guid siteId, Guid entityId, strin // Create new permissions var permissions = roleIds.Select(x => new Permission { + Id = Guid.NewGuid(), EntityType = entityTypeName, Action = action, EntityId = entityId, @@ -28,7 +32,9 @@ public async Task> Set(Guid siteId, Guid entityId, strin // Save the new permissions foreach (var permission in permissions) { - await session.StoreAsync(permission, cancellationToken); + SetAuditableFieldsForCreate(permission); + + await session.StoreAsync(new RavenEntity(permission), cancellationToken); } await session.SaveChangesAsync(cancellationToken); diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs index 7c1db5541..e995a5296 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs @@ -12,7 +12,10 @@ public async Task> GetByPluginId(Guid pluginId, Cance using (var session = Store.OpenAsyncSession()) { - var qres = await session.Query().Where(x => x.PluginId == pluginId).ToListAsync(cancellationToken); + var qres = await session.Query>() + .Where(x => x.Data.PluginId == pluginId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); return qres.AsEnumerable(); } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs index d692c115e..f591faed2 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs @@ -8,7 +8,10 @@ public async Task> GetByPageId(Guid pageId, CancellationToke using (var session = Store.OpenAsyncSession()) { - var qres = await session.Query().Where(x => x.PageId == pageId).ToListAsync(cancellationToken); + var qres = await session.Query>() + .Where(x => x.Data.PageId == pageId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); return qres.AsEnumerable(); } @@ -18,15 +21,21 @@ public async Task> GetByPageId(Guid pageId, CancellationToke { cancellationToken.ThrowIfCancellationRequested(); - var plugin = await GetById(pluginId, cancellationToken); - - if (plugin != null) + using (var session = Store.OpenAsyncSession()) { - plugin.Order = order; - plugin.Section = section; + var plugin = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == pluginId, cancellationToken); + + if (plugin != null) + { + plugin.Data.Order = order; + plugin.Data.Section = section; - return await Update(plugin, cancellationToken); + await session.SaveChangesAsync(); + + return plugin.Data; + } } + return default; } @@ -34,15 +43,22 @@ public async Task> GetByPageId(Guid pageId, CancellationToke { cancellationToken.ThrowIfCancellationRequested(); - var plugin = await GetById(pluginId, cancellationToken); - - if (plugin != null) + using (var session = Store.OpenAsyncSession()) { - plugin.Cols = cols; - plugin.ColsMd = colsMd; - plugin.ColsLg = colsLg; - return await Update(plugin, cancellationToken); + var plugin = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == pluginId, cancellationToken); + + if (plugin != null) + { + plugin.Data.Cols = cols; + plugin.Data.ColsMd = colsMd; + plugin.Data.ColsLg = colsLg; + + await session.SaveChangesAsync(); + + return plugin.Data; + } } + return default; } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenEntity.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenEntity.cs new file mode 100644 index 000000000..031998d39 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenEntity.cs @@ -0,0 +1,15 @@ +using System; + +namespace FluentCMS.Repositories.RavenDB; + +public class RavenEntity where TEntity : IEntity +{ + public RavenEntity(TEntity entity) + { + Data = entity; + } + + public string RavenId { get; set; } = default!; + + public TEntity Data { get; set; } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs index b63cc0db6..9bdf294ca 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs @@ -12,7 +12,7 @@ public SettingsRepository(IRavenDBContext dbContext, IApiExecutionContext apiExe { using (var session = Store.OpenAsyncSession()) { - var existing = await session.Query().SingleOrDefaultAsync(x => x.Id == entityId, cancellationToken); + var existing = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == entityId, cancellationToken); if (existing == null) { @@ -21,28 +21,30 @@ public SettingsRepository(IRavenDBContext dbContext, IApiExecutionContext apiExe entityId = Guid.NewGuid(); } - existing = new Settings() + var setting = new Settings() { Id = entityId, Values = settings }; - SetAuditableFieldsForCreate(existing); + SetAuditableFieldsForCreate(setting); - await session.StoreAsync(existing); + existing = new RavenEntity(setting); + + await session.StoreAsync(existing, cancellationToken); } else { // add new settings to existing settings or update existing settings foreach (var setting in settings) - existing.Values[setting.Key] = setting.Value; + existing.Data.Values[setting.Key] = setting.Value; - SetAuditableFieldsForUpdate(existing, existing); + SetAuditableFieldsForUpdate(existing.Data, existing.Data); } await session.SaveChangesAsync(); - return existing; + return existing.Data; } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs index d9c9c7ac5..61c3dd47a 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs @@ -2,20 +2,25 @@ public abstract class SiteAssociatedRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(RavenDbContext, apiExecutionContext), ISiteAssociatedRepository where TEntity : ISiteAssociatedEntity { - public override async Task Update(TEntity entity, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); + // public override async Task Update(TEntity entity, CancellationToken cancellationToken = default) + // { + // cancellationToken.ThrowIfCancellationRequested(); - var existing = await GetById(entity.Id, cancellationToken); - if (existing is null) - return default; + // using (var session = Store.OpenAsyncSession()) + // { + // var existing = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == entity.Id, cancellationToken); + // if (existing == null) + // return default; - SetAuditableFieldsForUpdate(entity, existing); + // SetAuditableFieldsForUpdate(entity, existing.Data); - entity.SiteId = existing.SiteId; + // existing.Data.SiteId = entity.SiteId; - return await base.Update(entity, cancellationToken); - } + // await session.SaveChangesAsync(); + + // return existing.Data; + // } + // } public async Task> GetAllForSite(Guid siteId, CancellationToken cancellationToken = default) { @@ -23,19 +28,24 @@ public async Task> GetAllForSite(Guid siteId, CancellationT using (var session = Store.OpenAsyncSession()) { - var qres = await session.Query().Where(x => x.SiteId == siteId).ToListAsync(cancellationToken); + var qres = await session.Query>() + .Where(x => x.Data.SiteId == siteId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); return qres.AsEnumerable(); } } - public async Task GetByIdForSite(Guid id, Guid siteId, CancellationToken cancellationToken = default) + public async Task GetByIdForSite(Guid id, Guid siteId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.Id == id && x.SiteId == siteId, cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id && x.Data.SiteId == siteId, cancellationToken); + + return entity?.Data; } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs index 9a72d2f97..643e01428 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs @@ -11,7 +11,9 @@ public SiteRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiEx using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.Urls.Contains(url), cancellationToken); + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Urls.Contains(url), cancellationToken); + + return entity?.Data; } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs index dfe809c3d..13096947e 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs @@ -1,46 +1,49 @@ -using System.Security.Claims; +using System.IO.Compression; +using System.Security.Claims; namespace FluentCMS.Repositories.RavenDB; public class UserRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(RavenDbContext, apiExecutionContext), IUserRepository { - override public async Task GetById(Guid id, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var session = Store.OpenAsyncSession()) - { - return await session.Query().SingleOrDefaultAsync(x => x.Id == id, cancellationToken); - } - } - - override public async Task Update(User entity, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var session = Store.OpenAsyncSession()) - { - var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); - if (dbEntity == null) - { - SetAuditableFieldsForCreate(entity); - - await session.StoreAsync(entity); - - dbEntity = entity; - } - else - { - entity.CopyProperties(dbEntity); + // override public async Task GetById(Guid id, CancellationToken cancellationToken = default) + // { + // cancellationToken.ThrowIfCancellationRequested(); + + // using (var session = Store.OpenAsyncSession()) + // { + // var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); + + // return dbEntity?.Data; + // } + // } + + // override public async Task Update(User entity, CancellationToken cancellationToken = default) + // { + // cancellationToken.ThrowIfCancellationRequested(); + + // using (var session = Store.OpenAsyncSession()) + // { + // var dbEntity = await session.Query().SingleOrDefaultAsync(x => x.Id == entity.Id, cancellationToken); + // if (dbEntity == null) + // { + // SetAuditableFieldsForCreate(entity); + + // await session.StoreAsync(entity, entity.Id.ToString(), cancellationToken); + + // dbEntity = entity; + // } + // else + // { + // entity.CopyProperties(dbEntity); - SetAuditableFieldsForUpdate(entity, dbEntity); - } + // SetAuditableFieldsForUpdate(entity, dbEntity); + // } - await session.SaveChangesAsync(cancellationToken); + // await session.SaveChangesAsync(cancellationToken); - return dbEntity; - } - } + // return dbEntity; + // } + // } public async Task> GetUsersForClaim(Claim claim, CancellationToken cancellationToken = default) { @@ -48,7 +51,10 @@ public async Task> GetUsersForClaim(Claim claim, CancellationToken c using (var session = Store.OpenAsyncSession()) { - var users = session.Query().Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)).ToListAsync(); + var users = session.Query>() + .Where(u => u.Data.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)) + .Select(x => x.Data) + .ToListAsync(); return await users; } @@ -60,7 +66,9 @@ public async Task> GetUsersForClaim(Claim claim, CancellationToken c using (var session = Store.OpenAsyncSession()) { - return await session.Query().SingleOrDefaultAsync(x => x.NormalizedEmail == normalizedEmail); + var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.NormalizedEmail == normalizedEmail); + + return dbEntity?.Data; } } @@ -70,7 +78,10 @@ public async Task> GetUsersForClaim(Claim claim, CancellationToken c using (var session = Store.OpenAsyncSession()) { - return await session.Query().FirstOrDefaultAsync(user => user.Logins.Any(x => x.LoginProvider == loginProvider && x.ProviderKey == providerKey)); + var dbEntity = await session.Query>() + .FirstOrDefaultAsync(user => user.Data.Logins.Any(x => x.LoginProvider == loginProvider && x.ProviderKey == providerKey)); + + return dbEntity?.Data; } } @@ -80,7 +91,9 @@ public async Task> GetUsersForClaim(Claim claim, CancellationToken c using (var session = Store.OpenAsyncSession()) { - return await session.Query().FirstOrDefaultAsync(x => x.NormalizedUserName == normalizedUserName); + var dbEntity = await session.Query>().FirstOrDefaultAsync(x => x.Data.NormalizedUserName == normalizedUserName); + + return dbEntity?.Data; } } @@ -89,7 +102,8 @@ public IQueryable AsQueryable() using (var session = Store.OpenSession()) { // TODO: Not good to load all user to list and the query them. But difficult to use sessions otherwise. - var entities = session.Query().ToList(); + var entities = session.Query>().Select(x => x.Data).ToList(); + return entities.AsQueryable(); } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs index e94dbac2e..83cf53c6a 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs @@ -8,7 +8,10 @@ public async Task> GetUserRoles(Guid userId, Guid siteId, using (var session = Store.OpenAsyncSession()) { - var qres = await session.Query().Where(x => x.SiteId == siteId && x.UserId == userId).ToListAsync(cancellationToken); + var qres = await session.Query>() + .Where(x => x.Data.SiteId == siteId && x.Data.UserId == userId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); return qres.AsEnumerable(); } @@ -20,7 +23,10 @@ public async Task> GetByRoleId(Guid roleId, CancellationTo using (var session = Store.OpenAsyncSession()) { - var qres = await session.Query().Where(x => x.RoleId == roleId).ToListAsync(cancellationToken); + var qres = await session.Query>() + .Where(x => x.Data.RoleId == roleId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); return qres.AsEnumerable(); } @@ -32,7 +38,10 @@ public async Task> GetByUserId(Guid userId, CancellationTo using (var session = Store.OpenAsyncSession()) { - var qres = await session.Query().Where(x => x.UserId == userId).ToListAsync(cancellationToken); + var qres = await session.Query>() + .Where(x => x.Data.UserId == userId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); return qres.AsEnumerable(); } From 322d37140399b3cb6f5ed2b18ec666df3670cba0 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Wed, 16 Oct 2024 08:13:29 +0200 Subject: [PATCH 07/11] Bug fix. --- .../FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs index c27e3ae4e..ddaed544f 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs @@ -40,7 +40,7 @@ public override async Task> CreateMany(IEnumerable { entity.CopyProperties(dbEntity.Data); - SetAuditableFieldsForUpdate(entity, dbEntity.Data); + SetAuditableFieldsForUpdate(dbEntity.Data, dbEntity.Data); } await session.SaveChangesAsync(cancellationToken); From 5629b04cf6159321b9b7cbe2dab738aa0e986224 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Sun, 27 Oct 2024 21:37:47 +0100 Subject: [PATCH 08/11] Implement new Get method. --- .../PermissionRepository.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs index d7786fd13..a4254a26e 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs @@ -2,6 +2,20 @@ public class PermissionRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : SiteAssociatedRepository(RavenDbContext, apiExecutionContext), IPermissionRepository { + public async Task> Get(Guid siteId, Guid entityId, string entityTypeName, string action, CancellationToken cancellationToken = default) + { + using (var session = Store.OpenAsyncSession()) + { + var permissions = await session.Query>() + .Where(x => x.Data.EntityId == entityId && x.Data.EntityType == entityTypeName && x.Data.Action == action) + .Select(x => x.Data) + .ToListAsync(cancellationToken); + + return permissions; + } + } + + public async Task> Set(Guid siteId, Guid entityId, string entityTypeName, string action, IEnumerable roleIds, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); From d52cd6848131c0e1fdefbc4d9b37f7048982e869 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Mon, 4 Nov 2024 19:16:24 +0100 Subject: [PATCH 09/11] Icon corrections --- .../Components/Icon/IconResource.resx | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Frontend/FluentCMS.Web.UI.Components/Components/Icon/IconResource.resx b/src/Frontend/FluentCMS.Web.UI.Components/Components/Icon/IconResource.resx index b655ae75d..5827c5bc5 100644 --- a/src/Frontend/FluentCMS.Web.UI.Components/Components/Icon/IconResource.resx +++ b/src/Frontend/FluentCMS.Web.UI.Components/Components/Icon/IconResource.resx @@ -118,31 +118,40 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M8 3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1h2a2 2 0 0 1 2 2v15a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h2Zm6 1h-4v2H9a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2h-1V4Zm-3 8a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1Zm-2-1a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H9Zm2 5a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2h-3a1 1 0 0 1-1-1Zm-2-1a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H9Z" clip-rule="evenodd"/> </svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 4h3a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3m0 3h6m-3 5h3m-6 0h.01M12 16h3m-6 0h.01M10 3v4h4V3h-4Z"/> +</svg> - <svg fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">   <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"/> </svg> - <svg fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"></path></svg> + <svg class="w-6 h-6 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/> </svg> - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/> </svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 13V8m0 8h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/> +</svg> - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 14"> <path d="M10 0C4.612 0 0 5.336 0 7c0 1.742 3.546 7 10 7 6.454 0 10-5.258 10-7 0-1.664-4.612-7-10-7Zm0 10a3 3 0 1 1 0-6 3 3 0 0 1 0 6Z"/> </svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-width="2" d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6Z"/> +  <path stroke="currentColor" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/> +</svg> - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/> </svg> + <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/> </svg> - <svg fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"></path></svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7 7V5"/> +</svg> @@ -162,11 +171,13 @@ - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M2 12a10 10 0 1 1 20 0 10 10 0 0 1-20 0Zm11-4a1 1 0 1 0-2 0v4c0 .3.1.5.3.7l3 3a1 1 0 0 0 1.4-1.4L13 11.6V8Z" clip-rule="evenodd"/> </svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/> +</svg> - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8 8-4 4 4 4m8 0 4-4-4-4m-2-3-4 14"/> </svg> + <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8 8-4 4 4 4m8 0 4-4-4-4m-2-3-4 14"/> </svg> @@ -202,15 +213,19 @@ - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/></svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/> +</svg> - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/></svg> + <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> +  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/> +</svg> - <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m5 15 7-7 7 7"/></svg> + <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m5 15 7-7 7 7"/></svg> @@ -249,6 +264,4 @@ <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="m4 15.6 3.055-3.056A4.913 4.913 0 0 1 7 12.012a5.006 5.006 0 0 1 5-5c.178.009.356.027.532.054l1.744-1.744A8.973 8.973 0 0 0 12 5.012c-5.388 0-10 5.336-10 7A6.49 6.49 0 0 0 4 15.6Z"/><path d="m14.7 10.726 4.995-5.007A.998.998 0 0 0 18.99 4a1 1 0 0 0-.71.305l-4.995 5.007a2.98 2.98 0 0 0-.588-.21l-.035-.01a2.981 2.981 0 0 0-3.584 3.583c0 .012.008.022.01.033.05.204.12.402.211.59l-4.995 4.983a1 1 0 1 0 1.414 1.414l4.995-4.983c.189.091.386.162.59.211.011 0 .021.007.033.01a2.982 2.982 0 0 0 3.584-3.584c0-.012-.008-.023-.011-.035a3.05 3.05 0 0 0-.21-.588Z"/><path d="m19.821 8.605-2.857 2.857a4.952 4.952 0 0 1-5.514 5.514l-1.785 1.785c.767.166 1.55.25 2.335.251 6.453 0 10-5.258 10-7 0-1.166-1.637-2.874-2.179-3.407Z"/></svg> - - \ No newline at end of file From bdb0114cef3dd4f1faa510fdec6d2db2e684fe88 Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Fri, 20 Dec 2024 17:05:13 +0100 Subject: [PATCH 10/11] Fix for SiteAssociatedRepository Only set created and modified when they are. --- .../AuditableEntityRepository.cs | 8 ++-- .../RavenDBContext.cs | 1 + .../SettingsRepository.cs | 2 +- .../SiteAssociatedRepository.cs | 44 +++++++++++++------ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs index ddaed544f..0a8201c5a 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs @@ -40,7 +40,7 @@ public override async Task> CreateMany(IEnumerable { entity.CopyProperties(dbEntity.Data); - SetAuditableFieldsForUpdate(dbEntity.Data, dbEntity.Data); + SetAuditableFieldsForUpdate(dbEntity.Data); } await session.SaveChangesAsync(cancellationToken); @@ -58,17 +58,17 @@ protected void SetAuditableFieldsForCreate(TEntity entity) entity.CreatedAt = DateTime.UtcNow; entity.CreatedBy = ApiExecutionContext.Username; + entity.ModifiedAt = entity.CreatedAt; + entity.ModifiedBy = entity.CreatedBy; } - protected void SetAuditableFieldsForUpdate(TEntity entity, TEntity oldEntity) + protected void SetAuditableFieldsForUpdate(TEntity entity) { if (entity.Id == Guid.Empty) { entity.Id = Guid.NewGuid(); } - entity.CreatedAt = oldEntity.CreatedAt; - entity.CreatedBy = oldEntity.CreatedBy; entity.ModifiedAt = DateTime.UtcNow; entity.ModifiedBy = ApiExecutionContext.Username; } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs index f909be952..2bf24f893 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs @@ -29,6 +29,7 @@ public RavenDBContext(RavenDBOptions options) // Set conventions as necessary (optional) Conventions = { + DisposeCertificate = false, MaxNumberOfRequestsPerSession = 10, UseOptimisticConcurrency = true, AddIdFieldToDynamicObjects = false, diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs index 9bdf294ca..6375ae1ed 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs @@ -39,7 +39,7 @@ public SettingsRepository(IRavenDBContext dbContext, IApiExecutionContext apiExe foreach (var setting in settings) existing.Data.Values[setting.Key] = setting.Value; - SetAuditableFieldsForUpdate(existing.Data, existing.Data); + SetAuditableFieldsForUpdate(existing.Data); } await session.SaveChangesAsync(); diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs index 61c3dd47a..717aeb3d4 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs @@ -2,25 +2,41 @@ public abstract class SiteAssociatedRepository(IRavenDBContext RavenDbContext, IApiExecutionContext apiExecutionContext) : AuditableEntityRepository(RavenDbContext, apiExecutionContext), ISiteAssociatedRepository where TEntity : ISiteAssociatedEntity { - // public override async Task Update(TEntity entity, CancellationToken cancellationToken = default) - // { - // cancellationToken.ThrowIfCancellationRequested(); + public override async Task Update(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + Guid id = entity.Id; + var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); + if (dbEntity == null) + { + SetAuditableFieldsForCreate(entity); - // using (var session = Store.OpenAsyncSession()) - // { - // var existing = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == entity.Id, cancellationToken); - // if (existing == null) - // return default; + dbEntity = new RavenEntity(entity); - // SetAuditableFieldsForUpdate(entity, existing.Data); + await session.StoreAsync(dbEntity, cancellationToken); + } + else + { + Guid siteId = dbEntity.Data.SiteId; - // existing.Data.SiteId = entity.SiteId; + entity.CopyProperties(dbEntity.Data); - // await session.SaveChangesAsync(); + dbEntity.Data.SiteId = siteId; - // return existing.Data; - // } - // } + SetAuditableFieldsForUpdate(dbEntity.Data); + } + + if (entity.SiteId != Guid.Empty) + dbEntity.Data.SiteId = entity.SiteId; + + await session.SaveChangesAsync(); + + return dbEntity.Data; + } + } public async Task> GetAllForSite(Guid siteId, CancellationToken cancellationToken = default) { From d8a6f1287fe8d902ff34f5dbb5506cb34dcd26cf Mon Sep 17 00:00:00 2001 From: Stefan Ericsson Date: Fri, 20 Dec 2024 17:05:43 +0100 Subject: [PATCH 11/11] Missed commit --- .../FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs index 717aeb3d4..c33222197 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs @@ -20,10 +20,13 @@ public abstract class SiteAssociatedRepository(IRavenDBContext RavenDbC } else { + // Preserve the site id since it is not part of the entity in the request Guid siteId = dbEntity.Data.SiteId; entity.CopyProperties(dbEntity.Data); + // Restore the site id + // This means that the site id cannot be changed dbEntity.Data.SiteId = siteId; SetAuditableFieldsForUpdate(dbEntity.Data);