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/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.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 new file mode 100644 index 000000000..21884edc2 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ApiTokenRepository.cs @@ -0,0 +1,28 @@ +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 = Store.OpenAsyncSession()) + { + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Key == apiKey, cancellationToken); + + return entity?.Data; + } + } + + public async Task GetByName(string name, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + 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 new file mode 100644 index 000000000..0a8201c5a --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/AuditableEntityRepository.cs @@ -0,0 +1,75 @@ +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 id = entity.Id; + var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); + if (dbEntity == null) + { + SetAuditableFieldsForCreate(entity); + + dbEntity = new RavenEntity(entity); + + await session.StoreAsync(dbEntity, cancellationToken); + } + else + { + entity.CopyProperties(dbEntity.Data); + + SetAuditableFieldsForUpdate(dbEntity.Data); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity.Data; + } + } + + protected void SetAuditableFieldsForCreate(TEntity entity) + { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + + entity.CreatedAt = DateTime.UtcNow; + entity.CreatedBy = ApiExecutionContext.Username; + entity.ModifiedAt = entity.CreatedAt; + entity.ModifiedBy = entity.CreatedBy; + } + + protected void SetAuditableFieldsForUpdate(TEntity entity) + { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + + 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..905d30f39 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentRepository.cs @@ -0,0 +1,19 @@ +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.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 new file mode 100644 index 000000000..cddf3327b --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/ContentTypeRepository.cs @@ -0,0 +1,16 @@ +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()) + { + 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 new file mode 100644 index 000000000..80f5e4012 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/EntityRepository.cs @@ -0,0 +1,168 @@ +using Raven.Client.Documents.Linq; + +namespace FluentCMS.Repositories.RavenDB; + +public abstract class EntityRepository : IEntityRepository where TEntity : IEntity +{ + protected readonly IDocumentStore Store; + + public EntityRepository(IRavenDBContext RavenDbContext) + { + Store = RavenDbContext.Store; + + // Ensure index on Id field + // var indexKeysDefinition = Builders.IndexKeys.Ascending(x => x.Id); + // Collection.Indexes.CreateOne(new CreateIndexModel(indexKeysDefinition)); + } + + public virtual async Task> GetAll(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entities = await session.Query>() + .Select(x => x.Data) + .ToListAsync(cancellationToken); + + return entities.AsEnumerable(); + } + } + + public virtual async Task GetById(Guid id, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var entity = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == id, cancellationToken); + + return entity.Data; + } + } + + 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.Data.Id)) + .Select(x => x.Data) + .ToListAsync(cancellationToken); + + return entities.AsEnumerable(); + } + } + + public virtual async Task Create(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + + await session.StoreAsync(new RavenEntity(entity), cancellationToken); + + 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(); + + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + } + + await session.StoreAsync(new RavenEntity(entity), cancellationToken); + } + + await session.SaveChangesAsync(cancellationToken); + } + + return entities; + } + + public virtual async Task Update(TEntity entity, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + 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) + { + entity.Id = Guid.NewGuid(); + } + + dbEntity = new RavenEntity(entity); + + await session.StoreAsync(dbEntity, cancellationToken); + } + else + { + entity.CopyProperties(dbEntity.Data); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity.Data; + } + } + + 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.Data.Id == id, cancellationToken); + + session.Delete(entity); + + await session.SaveChangesAsync(cancellationToken); + + return entity.Data; // A bit strange to return to object we just deleted. + } + } + + 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.Data.Id)).ToListAsync(cancellationToken); + + foreach (var entity in entities) + { + cancellationToken.ThrowIfCancellationRequested(); + + session.Delete(entity); + } + + await session.SaveChangesAsync(cancellationToken); + + return entities.Select(x => x.Data); + } + } +} 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..fda207102 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/GlobalSettingsRepository.cs @@ -0,0 +1,95 @@ +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()) + { + var entity = await session.Query>().SingleOrDefaultAsync(cancellationToken); + + return entity?.Data; + } + } + + 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); + + dbEntity = new RavenEntity(settings); + + await session.StoreAsync(dbEntity, cancellationToken); + } + else + { + settings.CopyProperties(dbEntity.Data); + + SetAuditableFieldsForUpdate(dbEntity.Data); + } + + await session.SaveChangesAsync(cancellationToken); + + return dbEntity?.Data; + } + } + + 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, settings.Id.ToString(), cancellationToken); + + // 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..a4254a26e --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PermissionRepository.cs @@ -0,0 +1,59 @@ +namespace FluentCMS.Repositories.RavenDB; + +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(); + + using (var session = Store.OpenAsyncSession()) + { + // Delete existing permissions + 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); + } + + // Create new permissions + var permissions = roleIds.Select(x => new Permission + { + Id = Guid.NewGuid(), + EntityType = entityTypeName, + Action = action, + EntityId = entityId, + RoleId = x, + SiteId = siteId + }); + + // Save the new permissions + foreach (var permission in permissions) + { + SetAuditableFieldsForCreate(permission); + + await session.StoreAsync(new RavenEntity(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..e995a5296 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginContentRepository.cs @@ -0,0 +1,23 @@ +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.Data.PluginId == pluginId) + .Select(x => x.Data) + .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..f591faed2 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/PluginRepository.cs @@ -0,0 +1,64 @@ +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.Data.PageId == pageId) + .Select(x => x.Data) + .ToListAsync(cancellationToken); + + return qres.AsEnumerable(); + } + } + + public async Task UpdateOrder(Guid pluginId, string section, int order, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var plugin = await session.Query>().SingleOrDefaultAsync(x => x.Data.Id == pluginId, cancellationToken); + + if (plugin != null) + { + plugin.Data.Order = order; + plugin.Data.Section = section; + + await session.SaveChangesAsync(); + + return plugin.Data; + } + } + + return default; + } + + public async Task UpdateCols(Guid pluginId, int cols, int colsMd, int colsLg, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + 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/RavenDBContext.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs new file mode 100644 index 000000000..2bf24f893 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBContext.cs @@ -0,0 +1,42 @@ +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 = + { + DisposeCertificate = false, + 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..7663285dc --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/RavenDBServiceExtension.cs @@ -0,0 +1,46 @@ +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(); + 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/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/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/SettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs new file mode 100644 index 000000000..6375ae1ed --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SettingsRepository.cs @@ -0,0 +1,50 @@ +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.Data.Id == entityId, cancellationToken); + + if (existing == null) + { + if (entityId == Guid.Empty) + { + entityId = Guid.NewGuid(); + } + + var setting = new Settings() + { + Id = entityId, + Values = settings + }; + + SetAuditableFieldsForCreate(setting); + + 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.Data.Values[setting.Key] = setting.Value; + + SetAuditableFieldsForUpdate(existing.Data); + } + + await session.SaveChangesAsync(); + + return existing.Data; + } + } +} 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..c33222197 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteAssociatedRepository.cs @@ -0,0 +1,70 @@ +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(); + + 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); + + dbEntity = new RavenEntity(entity); + + await session.StoreAsync(dbEntity, cancellationToken); + } + 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); + } + + 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) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + 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) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + 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 new file mode 100644 index 000000000..643e01428 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/SiteRepository.cs @@ -0,0 +1,19 @@ +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()) + { + 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 new file mode 100644 index 000000000..13096947e --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRepository.cs @@ -0,0 +1,111 @@ +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()) + // { + // 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); + // } + + // 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.Data.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)) + .Select(x => x.Data) + .ToListAsync(); + + return await users; + } + } + + public async Task FindByEmail(string normalizedEmail, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var dbEntity = await session.Query>().SingleOrDefaultAsync(x => x.Data.NormalizedEmail == normalizedEmail); + + return dbEntity?.Data; + } + } + + public async Task FindByLogin(string loginProvider, string providerKey, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var dbEntity = await session.Query>() + .FirstOrDefaultAsync(user => user.Data.Logins.Any(x => x.LoginProvider == loginProvider && x.ProviderKey == providerKey)); + + return dbEntity?.Data; + } + } + + public async Task FindByName(string normalizedUserName, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var session = Store.OpenAsyncSession()) + { + var dbEntity = await session.Query>().FirstOrDefaultAsync(x => x.Data.NormalizedUserName == normalizedUserName); + + return dbEntity?.Data; + } + } + + 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>().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 new file mode 100644 index 000000000..83cf53c6a --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.RavenDB/UserRoleRepository.cs @@ -0,0 +1,49 @@ +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.Data.SiteId == siteId && x.Data.UserId == userId) + .Select(x => x.Data) + .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.Data.RoleId == roleId) + .Select(x => x.Data) + .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.Data.UserId == userId) + .Select(x => x.Data) + .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 8c4f81910..c0f20d630 100644 --- a/src/FluentCMS.sln +++ b/src/FluentCMS.sln @@ -85,6 +85,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 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Web.Plugins.Contents.IFrame", "Frontend\Plugins\FluentCMS.Web.Plugins.Contents.IFrame\FluentCMS.Web.Plugins.Contents.IFrame.csproj", "{73B6E291-89AA-4EB7-BE38-C5884BAAB248}" EndProject Global @@ -217,6 +219,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 {73B6E291-89AA-4EB7-BE38-C5884BAAB248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73B6E291-89AA-4EB7-BE38-C5884BAAB248}.Debug|Any CPU.Build.0 = Debug|Any CPU {73B6E291-89AA-4EB7-BE38-C5884BAAB248}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -241,7 +247,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} @@ -263,6 +268,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} {73B6E291-89AA-4EB7-BE38-C5884BAAB248} = {BB5FD6A0-0840-4381-AB68-AD61499368ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/FluentCMS/FluentCMS.csproj b/src/FluentCMS/FluentCMS.csproj index 1de94a960..5cb552884 100644 --- a/src/FluentCMS/FluentCMS.csproj +++ b/src/FluentCMS/FluentCMS.csproj @@ -26,6 +26,7 @@ + diff --git a/src/FluentCMS/Program.cs b/src/FluentCMS/Program.cs index d4c117c8d..d74c04d89 100644 --- a/src/FluentCMS/Program.cs +++ b/src/FluentCMS/Program.cs @@ -15,6 +15,9 @@ // 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/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": { 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 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;