diff --git a/old/zero.Core/Collections/CollectionBase.cs b/old/zero.Core/Collections/CollectionBase.cs deleted file mode 100644 index 50a4ada2..00000000 --- a/old/zero.Core/Collections/CollectionBase.cs +++ /dev/null @@ -1,498 +0,0 @@ -using FluentValidation; -using FluentValidation.Results; -using Raven.Client; -using Raven.Client.Documents.Indexes; -using Raven.Client.Documents.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using zero.Core.Api; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Utils; - -namespace zero.Core.Collections -{ - public abstract class CollectionBase : ICollectionBase, ICollectionBase where T : ZeroEntity - { - private IRevisionsApi _revisions; - private string _database; - - protected virtual Action PreSave { get; set; } - - protected bool OnlyActive { get; set; } = false; // TODO do we really need this? - - - public CollectionBase(ICollectionContext collectionContext, IValidator validator = null) - { - Context = collectionContext.Context; - Store = collectionContext.Store; - Validator = validator; - Database = Store.ResolvedDatabase; - } - - - /// - /// Zero context - /// - protected readonly IZeroContext Context; - - /// - /// Document store - /// - protected readonly IZeroStore Store; - - /// - /// The validator - /// - protected readonly IValidator Validator; - - /// - /// Manage revisions - /// - protected IRevisionsApi Revisions - { - get - { - if (_revisions != null) - { - return _revisions; - } - - _revisions = new RevisionsApi(Session); - return _revisions; - } - } - - /// - /// Create an an async document session - /// - public IZeroDocumentSession Session => Store.Session(Database == Context.Options.Raven.Database); - - /// - public string Database - { - get => _database; - set - { - if (value != _database) - { - _database = value; - } - } - } - - /// - public Guid Guid { get; private set; } = Guid.NewGuid(); - - /// - public IRavenQueryable Query => Session.Query(); - - - /// - public virtual void ApplyScope(string scope) - { - Database = scope is "shared" or "core" ? Context.Options.Raven.Database : Store.ResolvedDatabase; - } - - - /// - public virtual void WithInactive(bool includeInactive = true) - { - OnlyActive = !includeInactive; - } - - - /// - public virtual async Task GetById(string id, string changeVector = null) - { - if (id.IsNullOrWhiteSpace()) - { - return default; - } - if (!changeVector.IsNullOrEmpty()) - { - return WhenActive(await GetRevision(changeVector)); - } - - return WhenActive(await Session.LoadAsync(id)); - } - - - /// - public virtual async Task> GetByIds(params string[] ids) - { - ids = ids.Distinct().ToArray(); - - Dictionary models = await Session.LoadAsync(ids); - Dictionary result = new Dictionary(); - - foreach (string id in ids) - { - models.TryGetValue(id, out T model); - result.Add(id, WhenActive(model)); - } - - return result; - } - - - /// - public virtual async Task> GetByQuery(ListQuery query) - { - if (query.SearchSelector == null && !query.SearchSelectors.Any()) - { - query.SearchSelector = x => x.Name; - } - return await Session.Query().WhereIf(x => x.IsActive, OnlyActive).ToQueriedListAsync(query); - } - - - /// - public virtual async Task> GetByQuery(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new() - { - if (query.SearchSelector == null && !query.SearchSelectors.Any()) - { - query.SearchSelector = x => x.Name; - } - return await Session.Query().WhereIf(x => x.IsActive, OnlyActive).ToQueriedListAsync(query); - } - - - /// - public virtual async Task> GetAll() - { - List items = new(); - - await foreach (T item in Stream()) - { - items.Add(item); - } - - return items; - } - - - /// - public virtual async Task> GetRevisions(string id, int page = 1, int pageSize = 10) - { - if (id.IsNullOrWhiteSpace()) - { - return default; - } - - return await Revisions.GetPaged(id, page, pageSize); - } - - - /// - public virtual async Task GetRevision(string changeVector) - { - return await Session.Advanced.Revisions.GetAsync(changeVector); - } - - - /// - public virtual IAsyncEnumerable Stream() => Stream(null); - - - /// - public virtual async IAsyncEnumerable Stream(Func, IRavenQueryable> expression) - { - IRavenQueryable query = Session.Query().WhereIf(x => x.IsActive, OnlyActive); - - if (expression != null) - { - query = expression(query); - } - - var stream = await Session.Advanced.StreamAsync(query); - - while (await stream.MoveNextAsync()) - { - yield return stream.Current.Document; - } - } - - - /// - public virtual async Task> Save(T model) - { - if (model == null) - { - return EntityResult.Fail("@errors.onsave.empty"); - } - - PreSave?.Invoke(model); - - bool isUpdate = model.Id.IsNullOrEmpty() ? false : await Session.Advanced.ExistsAsync(model.Id); - - // set IDs - model.AutoSetIds(); - - // get current user - string userId = Context.BackofficeUser.FindFirstValue(Constants.Auth.Claims.UserId); - - // set default properties - if (!isUpdate) - { - model.CreatedDate = DateTimeOffset.Now; - model.CreatedById = userId; - model.LanguageId ??= "languages.1-A"; // TODO correct language id - } - - // update name alias and last modified - model.Alias = Safenames.Alias(model.Name); - model.LastModifiedById = userId; - model.LastModifiedDate = DateTimeOffset.Now; - model.CreatedById ??= userId; - model.Hash ??= IdGenerator.Classic(); - - // run validator - if (Validator != null) - { - ValidationResult validation = await Validator.ValidateAsync(model); - - if (!validation.IsValid) - { - return EntityResult.Fail(validation); - } - } - - if (!isUpdate) - { - return await Create(model); - } - - return await Update(model); - } - - - /// - async Task> Create(T model) - { - // create ID before-hand so interceptors can use it - model.Id = await Session.Advanced.DocumentStore.Conventions.GenerateDocumentIdAsync(Session.Advanced.DocumentStore.Database, model); - - await Session.StoreAsync(model); - - await Session.SaveChangesAsync(); - - return EntityResult.Success(model); - } - - - /// - async Task> Update(T model) - { - - await Session.StoreAsync(model); - - await Session.SaveChangesAsync(); - - return EntityResult.Success(model); - } - - - /// - public virtual async Task> Delete(T model) => await DeleteById(model?.Id); - - - /// - public virtual async Task> DeleteById(string id) - { - if (String.IsNullOrEmpty(id)) - { - return EntityResult.Fail("@errors.ondelete.idnotfound"); - } - - T entity = await Session.LoadAsync(id); - - if (entity == null) - { - return EntityResult.Fail("@errors.ondelete.idnotfound"); - } - - Session.Delete(entity); - - await Session.SaveChangesAsync(); - - return EntityResult.Success(); - } - - - /// - public virtual async Task Delete(params T[] models) => await DeleteByIds(models.Select(x => x.Id).ToArray()); - - - /// - public virtual async Task DeleteByIds(params string[] ids) - { - int successCount = 0; - - foreach (string id in ids) - { - EntityResult result = await DeleteById(id); - successCount += result.IsSuccess ? 1 : 0; - } - - return successCount; - } - - - /// - public virtual async Task> Purge(string querySuffix = null, Parameters parameters = null) - { - await Store.Raven.PurgeAsync(Database, querySuffix, parameters); - - return EntityResult.Success(); - } - - - /// - /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() - /// - protected TRes WhenActive(TRes model) where TRes : T - { - return model != null && (!OnlyActive || model.IsActive) ? model : default; - } - } - - - public interface ICollectionBase - { - /// - /// Guid for this instance - /// - Guid Guid { get; } - - /// - /// The database to operate on. - /// Is null by default, which uses the database from the resolved application. - /// - string Database { get; set; } - - /// - /// Create an async document session - /// - IZeroDocumentSession Session { get; } - - /// - /// Applies the scope to the service instance - /// - void ApplyScope(string scope); - - /// - /// Include entities with IsActive=false for GET queries - /// - void WithInactive(bool include = true); - } - - - public interface ICollectionBase : ICollectionBase where T : ZeroEntity - { - /// - /// Returns a new document queryable - /// - IRavenQueryable Query { get; } - - /// - /// Get an entity by Id - /// - Task GetById(string id, string changeVector = null); - - /// - /// Get entities by ids - /// - Task> GetByIds(params string[] ids); - - /// - /// Get entities by query - /// - Task> GetByQuery(ListQuery query); - - /// - /// Get entities by query (by using the specified index) - /// - Task> GetByQuery(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new(); - - /// - /// Get all entities from this collection. - /// Warning: Don't use this method for large collections. Stream the results instead. - /// - Task> GetAll(); - - /// - /// Get page revisions for the specified entity - /// - Task> GetRevisions(string id, int page = 1, int pageSize = 10); - - /// - /// Get a revision by change vector - /// - Task GetRevision(string changeVector); - - /// - /// Stream the collection - /// - IAsyncEnumerable Stream(); - - /// - /// Stream the collection - /// - IAsyncEnumerable Stream(Func, IRavenQueryable> expression); - - /// - /// Updates or creates an entity with an optional validator - /// - Task> Save(T model); - - /// - /// Deletes an entity - /// - Task> Delete(T model); - - /// - /// Deletes entities - /// - Task Delete(params T[] models); - - /// - /// Deletes an entity by Id - /// - Task> DeleteById(string id); - - /// - /// Deletes entities by Id - /// - Task DeleteByIds(params string[] ids); - - /// - /// Delete a whole collection (with an optional query suffix, i.e. a where statement) - /// - Task> Purge(string querySuffix = null, Parameters parameters = null); - } - - - public interface ICollectionSession - { - /// - /// Guid for this instance - /// - Guid Guid { get; } - - /// - /// The database to operate on. - /// Is null by default, which uses the database from the resolved application. - /// - string Database { get; set; } - - /// - /// Applies the scope to the service instance - /// - void ApplyScope(string scope); - } -} diff --git a/old/zero.Core/Collections/CollectionContext.cs b/old/zero.Core/Collections/CollectionContext.cs deleted file mode 100644 index 314f176b..00000000 --- a/old/zero.Core/Collections/CollectionContext.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace zero.Core.Collections -{ - public class CollectionContext : CollectionContext, ICollectionContext where T : ZeroIdEntity, new() - { - public IInterceptorRunner Interceptors { get; private set; } - - public CollectionContext(IZeroContext context, IInterceptorRunner interceptors) : base(context) - { - Interceptors = interceptors; - } - } - - - public class CollectionContext : ICollectionContext - { - public IZeroStore Store { get; private set; } - - public IZeroContext Context { get; private set; } - - public IZeroOptions Options { get; private set; } - - - public CollectionContext(IZeroContext context) - { - Store = context.Store; - Options = context.Options; - Context = context; - } - } - - - public interface ICollectionContext : ICollectionContext where T : ZeroIdEntity, new() - { - IInterceptorRunner Interceptors { get; } - } - - - public interface ICollectionContext - { - IZeroStore Store { get; } - - IZeroContext Context { get; } - - IZeroOptions Options { get; } - } -} \ No newline at end of file diff --git a/old/zero.Core/Collections/CollectionScope.cs b/old/zero.Core/Collections/CollectionScope.cs deleted file mode 100644 index 7d234127..00000000 --- a/old/zero.Core/Collections/CollectionScope.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace zero.Core.Collections -{ - public sealed class CollectionScope : IDisposable - { - ICollectionBase _collection; - - - internal CollectionScope(ICollectionBase collection, string scope) - { - _collection = collection; - _collection.ApplyScope(scope); - } - - public void Dispose() - { - _collection?.ApplyScope(null); - } - } -} diff --git a/old/zero.Core/Collections/Countries/CountriesCollection.cs b/old/zero.Core/Collections/Countries/CountriesCollection.cs deleted file mode 100644 index 8762c37b..00000000 --- a/old/zero.Core/Collections/Countries/CountriesCollection.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FluentValidation; - -namespace zero.Core.Collections -{ - public class CountriesCollection : EntityCollection, ICountriesCollection - { - public CountriesCollection(ICollectionContext context) : base(context) { } - - /// - protected override void ValidationRules(ZeroValidator validator) - { - validator.RuleFor(x => x.Code).Length(2).Unique(Session); - validator.RuleFor(x => x.Name).Length(2, 120); - } - } - - - public interface ICountriesCollection : IEntityCollection { } -} diff --git a/old/zero.Core/Collections/EntityCollection/EntityCollection.Delete.cs b/old/zero.Core/Collections/EntityCollection/EntityCollection.Delete.cs deleted file mode 100644 index 7f9c2b3a..00000000 --- a/old/zero.Core/Collections/EntityCollection/EntityCollection.Delete.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace zero.Core; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Entities; -using zero.Core.Extensions; - -public abstract partial class EntityCollection -{ - /// - public virtual async Task> Delete(string id) - { - if (id.IsNullOrEmpty()) - { - return EntityResult.Fail("@errors.ondelete.idnotfound"); - } - - T entity = await Session.LoadAsync(id); - - if (entity == null) - { - return EntityResult.Fail("@errors.ondelete.idnotfound"); - } - - InterceptorInstruction instruction = Interceptors.CreateInstruction(this, InterceptorType.Delete, entity); - - if (!await instruction.Run()) - { - return instruction.Result; - } - - Session.Delete(entity); - - await instruction.Complete(); - - await Session.SaveChangesAsync(); - - return EntityResult.Success(); - } - - - /// - public virtual async Task> Delete(T model) => await Delete(model?.Id); - - - /// - public virtual async Task Delete(IEnumerable models) => await Delete(models.Select(x => x.Id)); - - - /// - public virtual async Task Delete(IEnumerable ids) - { - int successCount = 0; - - foreach (string id in ids) - { - EntityResult result = await Delete(id); - successCount += result.IsSuccess ? 1 : 0; - } - - return successCount; - } -} \ No newline at end of file diff --git a/old/zero.Core/Collections/EntityCollection/EntityCollection.Write.cs b/old/zero.Core/Collections/EntityCollection/EntityCollection.Write.cs deleted file mode 100644 index 5721b080..00000000 --- a/old/zero.Core/Collections/EntityCollection/EntityCollection.Write.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace zero.Core; - -using FluentValidation.Results; -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using zero.Core.Api; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Utils; - -public abstract partial class EntityCollection -{ - /// - public virtual async Task> Save(T model) - { - if (model == null) - { - return EntityResult.Fail("@errors.onsave.empty"); - } - - bool isUpdate = model.Id.IsNullOrEmpty() ? false : await Session.Advanced.ExistsAsync(model.Id); - - // set IDs - model.AutoSetIds(); - - // get current user - string userId = Context.BackofficeUser.FindFirstValue(Constants.Auth.Claims.UserId); - - if (model is ZeroEntity) - { - ZeroEntity zeroModel = model as ZeroEntity; - - // set default properties - if (!isUpdate) - { - zeroModel.CreatedDate = DateTimeOffset.Now; - zeroModel.CreatedById = userId; - zeroModel.LanguageId ??= "languages.1-A"; // TODO correct language id - } - - // update name alias and last modified - zeroModel.Alias = Safenames.Alias(zeroModel.Name); - zeroModel.LastModifiedById = userId; - zeroModel.LastModifiedDate = DateTimeOffset.Now; - zeroModel.CreatedById ??= userId; - zeroModel.Hash ??= IdGenerator.Classic(); - } - - // run validator - ValidationResult validation = await Validate(model); - if (!validation.IsValid) - { - return EntityResult.Fail(validation); - } - - // create ID before-hand so interceptors can use it - if (!isUpdate) - { - model.Id = await Session.Advanced.DocumentStore.Conventions.GenerateDocumentIdAsync(Session.Advanced.DocumentStore.Database, model); - } - - // run interceptor - InterceptorInstruction saveInstruction = Interceptors.CreateInstruction(this, InterceptorType.Save, model); - InterceptorInstruction instruction = Interceptors.CreateInstruction(this, isUpdate ? InterceptorType.Update : InterceptorType.Create, model); - - if (!await saveInstruction.Run()) - { - return saveInstruction.Result; - } - if (!await instruction.Run()) - { - return instruction.Result; - } - - // store our model - await Session.StoreAsync(model); - - // run after-handlers for interceptors - await saveInstruction.Complete(); - await instruction.Complete(); - - await Session.SaveChangesAsync(); - - return EntityResult.Success(model); - } -} \ No newline at end of file diff --git a/old/zero.Core/Collections/EntityCollection/EntityCollectionProxy.cs b/old/zero.Core/Collections/EntityCollection/EntityCollectionProxy.cs deleted file mode 100644 index 364c9a25..00000000 --- a/old/zero.Core/Collections/EntityCollection/EntityCollectionProxy.cs +++ /dev/null @@ -1,64 +0,0 @@ -//namespace zero.Core; -//using FluentValidation.Results; -//using Raven.Client.Documents.Indexes; -//using Raven.Client.Documents.Linq; -//using System; -//using System.Collections.Generic; -//using System.Threading.Tasks; -//using zero.Core.Database; -//using zero.Core.Entities; -//using zero.Core.Validation; - -//public abstract partial class EntityCollectionProxy -//{ -// EntityCollection Collection; - -// public async Task Load(string id, string changeVector = null) => await Collection.Load(id, changeVector); -//} - - - -//public interface IEntityCollection where T : ZeroIdEntity -//{ -// /// -// /// Get an entity by Id -// /// -// Task Load(string id, string changeVector = null); - -// /// -// /// Get entities by ids -// /// -// Task> Load(params string[] ids); - -// /// -// /// Get entities by query -// /// -// Task> Load(ListQuery query); - -// /// -// /// Get entities by query (by using the specified index) -// /// -// Task> Load(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new(); - -// /// -// /// Get all entities from this collection. -// /// Warning: Don't use this method for large collections. Stream the results instead. -// /// -// Task> LoadAll(); - -// /// -// /// Stream the collection -// /// -// IAsyncEnumerable Stream(); - -// /// -// /// Stream the collection -// /// -// IAsyncEnumerable Stream(Func, IRavenQueryable> expression); - - -// /// -// /// Validates an entity in this collection -// /// -// Task Validate(T model); -//} \ No newline at end of file diff --git a/old/zero.Core/Collections/Languages/LanguagesCollection.cs b/old/zero.Core/Collections/Languages/LanguagesCollection.cs deleted file mode 100644 index 96c187af..00000000 --- a/old/zero.Core/Collections/Languages/LanguagesCollection.cs +++ /dev/null @@ -1,55 +0,0 @@ -using FluentValidation; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace zero.Core.Collections -{ - public class LanguagesCollection : EntityCollection, ILanguagesCollection - { - public LanguagesCollection(ICollectionContext context) : base(context) { } - - - /// - public List GetAllCultures(params string[] codes) - { - return CultureInfo.GetCultures(CultureTypes.AllCultures) - .Where(x => !x.Name.IsNullOrWhiteSpace()) - .Select(x => new CultureInfo(x.Name)) - .Where(x => codes.Length > 0 ? codes.Contains(x.Name, StringComparer.InvariantCultureIgnoreCase) : true) - .OrderBy(x => x.DisplayName) - .Select(x => new Culture() - { - Code = x.Name, - Name = x.DisplayName - }) - .ToList(); - } - - - /// - protected override void ValidationRules(ZeroValidator validator) - { - validator.RuleFor(x => x.Name).Length(2, 60); - validator.RuleFor(x => x.Code).Length(2, 10).Culture(); - validator.RuleFor(x => x.IsDefault).Unique(Context.Store).When(x => x.IsDefault).WithMessage("@language.errors.default_unique"); - validator.RuleFor(x => x.IsDefault).ExpectAnyUnique(Context.Store, expectedValue: true).When(x => !x.IsDefault).WithMessage("@language.errors.needs_default"); - validator.RuleFor(x => x.InheritedLanguageId).Must((entity, value) => !entity.Id.Equals(value, StringComparison.InvariantCultureIgnoreCase)).When(x => !x.Id.IsNullOrEmpty()).WithMessage("@language.errors.fallback_invalid"); - validator.RuleFor(x => x.InheritedLanguageId).Equal((string)null).When(x => x.IsDefault).WithMessage("@language.errors.default_no_fallback"); - validator.RuleFor(x => x.InheritedLanguageId).Exists(Context.Store); - validator.RuleFor(x => x.IsOptional).Equal(false).When(x => x.IsDefault).WithMessage("@language.errors.default_not_optional"); - } - } - - - public interface ILanguagesCollection : IEntityCollection - { - /// - /// Get all available cultures - /// - List GetAllCultures(params string[] codes); - } -} diff --git a/old/zero.Core/Collections/MailTemplates/MailTemplatesCollection.cs b/old/zero.Core/Collections/MailTemplates/MailTemplatesCollection.cs deleted file mode 100644 index fc6affd8..00000000 --- a/old/zero.Core/Collections/MailTemplates/MailTemplatesCollection.cs +++ /dev/null @@ -1,49 +0,0 @@ -using FluentValidation; -using Raven.Client.Documents; -using System.Threading.Tasks; -using zero.Core.Mails; - -namespace zero.Core.Collections -{ - public class MailTemplatesCollection : EntityCollection, IMailTemplatesCollection - { - protected IMailDispatcher Dispatcher { get; set; } - - - public MailTemplatesCollection(ICollectionContext context, IMailDispatcher dispatcher = null) : base(context) - { - Dispatcher = dispatcher; - } - - - /// - public async Task GetByKey(string key) - { - return await Session.Query().FirstOrDefaultAsync(x => x.Key == key); - } - - - /// - protected override void ValidationRules(ZeroValidator validator) - { - validator.RuleFor(x => x.SenderEmail).Email(); - - if (Dispatcher != null) - { - validator.RuleFor(x => x.SenderEmail).MustAsync(async (value, ct) => - { - return await Dispatcher.IsSenderSupported(value); - }).WithMessage("@mailTemplate.errors.senderNotAllowed"); - } - } - } - - - public interface IMailTemplatesCollection : IEntityCollection - { - /// - /// Get mail template by associated key - /// - Task GetByKey(string key); - } -} diff --git a/old/zero.Web/Defaults/SectionPermissions.cs b/old/zero.Web/Defaults/SectionPermissions.cs deleted file mode 100644 index e07a6806..00000000 --- a/old/zero.Web/Defaults/SectionPermissions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using zero.Core; -using zero.Core.Identity; - -namespace zero.Web.Defaults -{ - public class SectionPermissions : PermissionCollection - { - public SectionPermissions() - { - Alias = Constants.PermissionCollections.Sections; - Label = "@permission.collections.sections"; - Description = "@permission.collections.sections_description"; - - Items.Add(new Permission(Permissions.Sections.Dashboard, "@sections.item.dashboard", null, PermissionValueType.Boolean)); - Items.Add(new Permission(Permissions.Sections.Pages, "@sections.item.pages", null, PermissionValueType.Boolean)); - Items.Add(new Permission(Permissions.Sections.Spaces, "@sections.item.spaces", null, PermissionValueType.Boolean)); - Items.Add(new Permission(Permissions.Sections.Media, "@sections.item.media", null, PermissionValueType.Boolean)); - Items.Add(new Permission(Permissions.Sections.Settings, "@sections.item.settings", null, PermissionValueType.Boolean)); - } - } -} diff --git a/old/zero.Web/Defaults/ZeroBackofficePlugin.cs b/old/zero.Web/Defaults/ZeroBackofficePlugin.cs index 00f43f93..35b98896 100644 --- a/old/zero.Web/Defaults/ZeroBackofficePlugin.cs +++ b/old/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using zero.Web.Sections; using zero.Web.ViewHelpers; namespace zero.Web.Defaults @@ -10,16 +8,9 @@ namespace zero.Web.Defaults { public override void Configure(IZeroOptions zero) { - zero.Sections.Add(); - zero.Sections.Add(); - zero.Sections.Add(); - zero.Sections.Add(); - zero.Sections.Add(); - zero.Settings.AddGroup(); zero.Settings.AddGroup(); - zero.Permissions.AddCollection(); zero.Permissions.AddCollection(); zero.Permissions.AddCollection(); zero.Permissions.AddCollection(); @@ -62,7 +53,6 @@ namespace zero.Web.Defaults services.AddScoped(); services.AddScoped(typeof(ICollectionContext<>), typeof(CollectionContext<>)); - services.AddScoped(typeof(IInterceptorRunner<>), typeof(InterceptorRunner<>)); services.AddScoped(); } diff --git a/zero.Backoffice/Endpoints/ZeroBackofficeCollectionController.cs b/zero.Backoffice/Endpoints/ZeroBackofficeCollectionController.cs index dce5a86c..722c0396 100644 --- a/zero.Backoffice/Endpoints/ZeroBackofficeCollectionController.cs +++ b/zero.Backoffice/Endpoints/ZeroBackofficeCollectionController.cs @@ -11,7 +11,7 @@ namespace zero.Backoffice; public abstract class ZeroBackofficeCollectionController : ZeroBackofficeController where TEntity : ZeroIdEntity, new() - where TCollection : IEntityCollection + where TCollection : ICollectionOperations { protected TCollection Collection { get; private set; } diff --git a/zero.Backoffice/Registrations.cs b/zero.Backoffice/Registrations.cs index b03565f0..e34a3f58 100644 --- a/zero.Backoffice/Registrations.cs +++ b/zero.Backoffice/Registrations.cs @@ -5,6 +5,7 @@ internal class Registrations public static List Modules { get; } = new() { new CountriesModule(), - new SearchModule() + new SearchModule(), + new BackofficeSectionModule() }; } \ No newline at end of file diff --git a/zero.Backoffice/Sections/Section.cs b/zero.Backoffice/Sections/BackofficeSection.cs similarity index 64% rename from zero.Backoffice/Sections/Section.cs rename to zero.Backoffice/Sections/BackofficeSection.cs index d5c356bc..00ea1047 100644 --- a/zero.Backoffice/Sections/Section.cs +++ b/zero.Backoffice/Sections/BackofficeSection.cs @@ -3,7 +3,7 @@ /// /// A section is a main part of the backoffice application /// -public class Section : ISection, IChildSection +public class BackofficeSection : IBackofficeSection, IChildBackofficeSection { /// public string Alias { get; set; } @@ -20,12 +20,12 @@ public class Section : ISection, IChildSection public string Color { get; set; } /// - public IList Children { get; } = new List(); + public IList Children { get; } = new List(); - public Section() { } + public BackofficeSection() { } - public Section(string alias, string name, string icon = null, string color = null) + public BackofficeSection(string alias, string name, string icon = null, string color = null) { Alias = alias; Name = name; diff --git a/zero.Backoffice/Sections/BackofficeSectionPermissions.cs b/zero.Backoffice/Sections/BackofficeSectionPermissions.cs new file mode 100644 index 00000000..967980d1 --- /dev/null +++ b/zero.Backoffice/Sections/BackofficeSectionPermissions.cs @@ -0,0 +1,18 @@ +namespace zero.Backoffice.Modules; + +public class BackofficeSectionPermissions : PermissionProvider +{ + public BackofficeSectionPermissions() : base("@permission.collections.sections") { } + + static string Prefix = "sections."; + + public static readonly Permission Dashboard = new(Prefix + "dashboard", "@sections.item.dashboard"); + public static readonly Permission Pages = new(Prefix + "pages", "@sections.item.pages"); + public static readonly Permission Spaces = new(Prefix + "spaces", "@sections.item.spaces"); + public static readonly Permission Media = new(Prefix + "media", "@sections.item.media"); + public static readonly Permission Settings = new(Prefix + "settings", "@sections.item.settings"); + + + /// + public override IEnumerable GetPermissions() => new[] { Dashboard, Pages, Spaces, Media, Settings }; +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/DashboardSection.cs b/zero.Backoffice/Sections/DashboardSection.cs index 3974da5e..d0229929 100644 --- a/zero.Backoffice/Sections/DashboardSection.cs +++ b/zero.Backoffice/Sections/DashboardSection.cs @@ -3,7 +3,7 @@ /// /// The dashboard aggregates data from all sections /// -public class DashboardSection : IInternalSection +public class DashboardSection : IInternalBackofficeSection { /// public string Alias => Constants.Sections.Dashboard; @@ -18,5 +18,5 @@ public class DashboardSection : IInternalSection public string Color => null; /// - public IList Children => new List(); + public IList Children => new List(); } \ No newline at end of file diff --git a/zero.Backoffice/Sections/ISection.cs b/zero.Backoffice/Sections/IBackofficeSection.cs similarity index 82% rename from zero.Backoffice/Sections/ISection.cs rename to zero.Backoffice/Sections/IBackofficeSection.cs index db87e4e3..6128763d 100644 --- a/zero.Backoffice/Sections/ISection.cs +++ b/zero.Backoffice/Sections/IBackofficeSection.cs @@ -3,12 +3,12 @@ /// /// Internal section /// -public interface IInternalSection : ISection { } +internal interface IInternalBackofficeSection : IBackofficeSection { } /// /// A section is a main part of the backoffice application /// -public interface ISection +public interface IBackofficeSection { /// /// The section alias which acts as the url slug for navigation @@ -33,5 +33,5 @@ public interface ISection /// /// Children are displayed as a sub-navigation in the main nav area /// - IList Children { get; } + IList Children { get; } } \ No newline at end of file diff --git a/zero.Backoffice/Sections/IChildSection.cs b/zero.Backoffice/Sections/IChildBackofficeSection.cs similarity index 90% rename from zero.Backoffice/Sections/IChildSection.cs rename to zero.Backoffice/Sections/IChildBackofficeSection.cs index 643957e1..e683d708 100644 --- a/zero.Backoffice/Sections/IChildSection.cs +++ b/zero.Backoffice/Sections/IChildBackofficeSection.cs @@ -3,7 +3,7 @@ /// /// A child section is a sub-navigation item of a section /// -public interface IChildSection +public interface IChildBackofficeSection { /// /// The section alias which acts as the url slug for navigation diff --git a/zero.Backoffice/Sections/MediaSection.cs b/zero.Backoffice/Sections/MediaSection.cs index c220f34f..32038580 100644 --- a/zero.Backoffice/Sections/MediaSection.cs +++ b/zero.Backoffice/Sections/MediaSection.cs @@ -3,7 +3,7 @@ /// /// Media items (images, videos, documents) grouped in folders /// -public class MediaSection : IInternalSection +public class MediaSection : IInternalBackofficeSection { /// public string Alias => Constants.Sections.Media; @@ -18,5 +18,5 @@ public class MediaSection : IInternalSection public string Color => "#d82853"; /// - public IList Children => new List(); + public IList Children => new List(); } \ No newline at end of file diff --git a/zero.Backoffice/Sections/Module.cs b/zero.Backoffice/Sections/Module.cs new file mode 100644 index 00000000..9ff474b5 --- /dev/null +++ b/zero.Backoffice/Sections/Module.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Backoffice.Sections; + +internal class BackofficeSectionModule : ZeroModule +{ + /// + public override void Register(IZeroModuleConfiguration config) + { + config.Services.AddSingleton(); + config.Services.AddSingleton(); + config.Services.AddSingleton(); + config.Services.AddSingleton(); + config.Services.AddSingleton(); + config.Services.AddScoped(); + } + + + /// + public override void Configure(IZeroOptions options) + { + + } +} \ No newline at end of file diff --git a/zero.Backoffice/Sections/PagesSection.cs b/zero.Backoffice/Sections/PagesSection.cs index c7cab504..5d4f2a38 100644 --- a/zero.Backoffice/Sections/PagesSection.cs +++ b/zero.Backoffice/Sections/PagesSection.cs @@ -3,7 +3,7 @@ /// /// Manage the page tree in this section /// -public class PagesSection : IInternalSection +public class PagesSection : IInternalBackofficeSection { /// public string Alias => Constants.Sections.Pages; @@ -18,5 +18,5 @@ public class PagesSection : IInternalSection public string Color => "#0cb0f5"; /// - public IList Children => new List(); + public IList Children => new List(); } \ No newline at end of file diff --git a/zero.Backoffice/Sections/SettingsSection.cs b/zero.Backoffice/Sections/SettingsSection.cs index ccc506f1..608ade93 100644 --- a/zero.Backoffice/Sections/SettingsSection.cs +++ b/zero.Backoffice/Sections/SettingsSection.cs @@ -3,7 +3,7 @@ /// /// Website and backoffice settings /// -public class SettingsSection : IInternalSection +public class SettingsSection : IInternalBackofficeSection { /// public string Alias => Constants.Sections.Settings; @@ -18,5 +18,5 @@ public class SettingsSection : IInternalSection public string Color => null; /// - public IList Children => new List(); + public IList Children => new List(); } diff --git a/zero.Backoffice/Sections/SpacesSection.cs b/zero.Backoffice/Sections/SpacesSection.cs index e038c7bb..1bde0485 100644 --- a/zero.Backoffice/Sections/SpacesSection.cs +++ b/zero.Backoffice/Sections/SpacesSection.cs @@ -3,7 +3,7 @@ /// /// Global list entities /// -public class SpacesSection : IInternalSection +public class SpacesSection : IInternalBackofficeSection { /// public string Alias => Constants.Sections.Spaces; @@ -18,5 +18,5 @@ public class SpacesSection : IInternalSection public string Color => "#f9c202"; /// - public IList Children => new List(); + public IList Children => new List(); } \ No newline at end of file diff --git a/zero.Backoffice/Usings.cs b/zero.Backoffice/Usings.cs index 3f1b03cb..308b7b5a 100644 --- a/zero.Backoffice/Usings.cs +++ b/zero.Backoffice/Usings.cs @@ -25,4 +25,5 @@ global using zero.Applications; global using zero.Backoffice; global using zero.Backoffice.Models; global using zero.Backoffice.Modules; -global using zero.Backoffice.Configuration; \ No newline at end of file +global using zero.Backoffice.Configuration; +global using zero.Backoffice.Sections; \ No newline at end of file diff --git a/zero.Core/BaseEntities/Queries/ListQuery.cs b/zero.Core/BaseEntities/Queries/ListQuery.cs new file mode 100644 index 00000000..70e41c48 --- /dev/null +++ b/zero.Core/BaseEntities/Queries/ListQuery.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json; +using Raven.Client.Documents.Session; +using System.Linq.Expressions; + +namespace zero; + +public class ListQuery +{ + public string Search { get; set; } = null; + + public Expression> SearchSelector { get; set; } = null; + + public Expression>[] SearchSelectors { get; private set; } = Array.Empty>>(); + + public string OrderBy { get; set; } = "createdDate"; + + public ListQueryOrderType OrderType { get; set; } = ListQueryOrderType.String; + + public bool OrderIsDescending { get; set; } = true; + + public Func, IQueryable> OrderQuery = null; + + public int Page { get; set; } = 1; + + public int PageSize { get; set; } = 30; + + public bool IncludeInactive { get; set; } = false; + + public QueryStatistics Statistics { get; internal set; } + + public void SearchFor(params Expression>[] selectors) + { + SearchSelectors = selectors; + } +} + + +public class ListQuery : ListQuery where TFilter : IListSpecificQuery +{ + public TFilter Filter { get; set; } +} + + +public static class ListQueryExtensions +{ + public static ListQuery AsList(this string options) where TFilter : IListSpecificQuery + { + return JsonConvert.DeserializeObject>(options); + } + + public static ListQuery AsList(this string options) where T : IListSpecificQuery + { + return JsonConvert.DeserializeObject>(options); + } +} + + +public interface IListSpecificQuery { } + +public class EmptyListSpecificQuery : IListSpecificQuery { } + + +public class ListQueryDateRange +{ + public DateTimeOffset? From { get; set; } + + public DateTimeOffset? To { get; set; } +} + +public class ListQueryRange +{ + public decimal? From { get; set; } + + public decimal? To { get; set; } +} + +public enum ListQueryOrderType +{ + String, + Number +} \ No newline at end of file diff --git a/zero.Core/Persistence/EntityResult.cs b/zero.Core/BaseEntities/Results/EntityResult.cs similarity index 99% rename from zero.Core/Persistence/EntityResult.cs rename to zero.Core/BaseEntities/Results/EntityResult.cs index a70f2e45..b683457e 100644 --- a/zero.Core/Persistence/EntityResult.cs +++ b/zero.Core/BaseEntities/Results/EntityResult.cs @@ -1,7 +1,7 @@ using FluentValidation.Results; using System.Runtime.Serialization; -namespace zero.Persistence; +namespace zero; [DataContract(Name = "result", Namespace = "")] public class EntityResult diff --git a/zero.Core/BaseEntities/Results/ListResult.cs b/zero.Core/BaseEntities/Results/ListResult.cs new file mode 100644 index 00000000..c9cd23da --- /dev/null +++ b/zero.Core/BaseEntities/Results/ListResult.cs @@ -0,0 +1,67 @@ +namespace zero; + +/// +/// Represents a paged result for a model collection +/// +public class ListResult : ListResult +{ + public ListResult(long totalItems, long page, long pageSize) + { + TotalItems = totalItems; + Page = page; + PageSize = pageSize; + + if (pageSize > 0) + { + TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize); + } + else + { + TotalPages = 1; + } + + HasMore = TotalPages > Page; + } + + public ListResult(IList items, long totalItems, long pageNumber, long pageSize) : this(totalItems, pageNumber, pageSize) + { + Items = items; + } + + public ListResult MapTo(Func convertItem) + { + return new ListResult(Items.Select(x => convertItem(x)).Where(x => x != null).ToList(), TotalItems, Page, PageSize); + } + + public IList Items { get; set; } = new List(); + + + /// + /// Calculates the skip size based on the paged parameters specified + /// + /// + /// Returns 0 if the page number or page size is zero + /// + public int GetSkipSize() + { + if (Page > 0 && PageSize > 0) + { + return Convert.ToInt32((Page - 1) * PageSize); + } + return 0; + } +} + + +public class ListResult +{ + public long Page { get; protected set; } + + public long PageSize { get; protected set; } + + public long TotalPages { get; protected set; } + + public long TotalItems { get; protected set; } + + public bool HasMore { get; protected set; } +} \ No newline at end of file diff --git a/zero.Core/Collections/CollectionContext.cs b/zero.Core/Collections/CollectionContext.cs new file mode 100644 index 00000000..0e05180c --- /dev/null +++ b/zero.Core/Collections/CollectionContext.cs @@ -0,0 +1,38 @@ +namespace zero.Collections; + +public class CollectionContext : ICollectionContext +{ + public IZeroStore Store { get; private set; } + + public IZeroContext Context { get; private set; } + + public IZeroOptions Options { get; private set; } + + public IInterceptors Interceptors { get; private set; } + + public ICollectionOperations Operations { get; private set; } + + + public CollectionContext(IZeroContext context, IInterceptors interceptors, ICollectionOperations operations) + { + Store = context.Store; + Options = context.Options; + Context = context; + Interceptors = interceptors; + Operations = operations; + } +} + + +public interface ICollectionContext +{ + IZeroStore Store { get; } + + IZeroContext Context { get; } + + IZeroOptions Options { get; } + + IInterceptors Interceptors { get; } + + ICollectionOperations Operations { get; } +} \ No newline at end of file diff --git a/zero.Core/Collections/CollectionOperations.Delete.cs b/zero.Core/Collections/CollectionOperations.Delete.cs new file mode 100644 index 00000000..5d1df1f0 --- /dev/null +++ b/zero.Core/Collections/CollectionOperations.Delete.cs @@ -0,0 +1,56 @@ +namespace zero.Collections; + +public abstract partial class CollectionOperations +{ + /// + public virtual async Task> Delete(string id) where T : ZeroIdEntity, new() + { + if (id.IsNullOrEmpty()) + { + return EntityResult.Fail("@errors.ondelete.idnotfound"); + } + + T entity = await Session.LoadAsync(id); + + if (entity == null) + { + return EntityResult.Fail("@errors.ondelete.idnotfound"); + } + + InterceptorInstruction instruction = Interceptors.ForDelete(entity); + + if (!await instruction.Start()) + { + return instruction.Result; + } + + Session.Delete(entity); + await instruction.Complete(); + await Session.SaveChangesAsync(); + + return EntityResult.Success(); + } + + + /// + public virtual async Task> Delete(T model) where T : ZeroIdEntity, new() => await Delete(model?.Id); + + + /// + public virtual async Task Delete(IEnumerable models) where T : ZeroIdEntity, new() => await Delete(models.Select(x => x.Id)); + + + /// + public virtual async Task Delete(IEnumerable ids) where T : ZeroIdEntity, new() + { + int successCount = 0; + + foreach (string id in ids) + { + EntityResult result = await Delete(id); + successCount += result.IsSuccess ? 1 : 0; + } + + return successCount; + } +} \ No newline at end of file diff --git a/old/zero.Core/Collections/EntityCollection/EntityCollection.Read.cs b/zero.Core/Collections/CollectionOperations.Read.cs similarity index 53% rename from old/zero.Core/Collections/EntityCollection/EntityCollection.Read.cs rename to zero.Core/Collections/CollectionOperations.Read.cs index 61da1ce8..7e4ec485 100644 --- a/old/zero.Core/Collections/EntityCollection/EntityCollection.Read.cs +++ b/zero.Core/Collections/CollectionOperations.Read.cs @@ -1,17 +1,12 @@ -namespace zero.Core; -using Raven.Client.Documents.Indexes; +using Raven.Client.Documents.Indexes; using Raven.Client.Documents.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Entities; -using zero.Core.Extensions; -public abstract partial class EntityCollection +namespace zero.Collections; + +public abstract partial class CollectionOperations { /// - public virtual async Task Load(string id, string changeVector = null) + public virtual async Task Load(string id, string changeVector = null) where T : ZeroIdEntity, new() { if (id.IsNullOrWhiteSpace()) { @@ -27,7 +22,7 @@ public abstract partial class EntityCollection /// - public virtual async Task> Load(IEnumerable ids) + public virtual async Task> Load(IEnumerable ids) where T : ZeroIdEntity, new() { ids = ids.Distinct().ToArray(); @@ -45,25 +40,27 @@ public abstract partial class EntityCollection /// - public virtual async Task> Load(ListQuery query) + public virtual async Task> Load(ListQuery query) where T : ZeroIdEntity, new() { - return await Session.Query().ToQueriedListAsyncX(query); // TODO whenActive + AsyncX update for ZeroEntity + return await Session.Query().FilterAsync(query); } /// - public virtual async Task> Load(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new() + public virtual async Task> Load(ListQuery query) + where T : ZeroIdEntity, new() + where TIndex : AbstractCommonApiForIndexes, new() { - return await Session.Query().ToQueriedListAsyncX(query); + return await Session.Query().FilterAsync(query); } /// - public virtual async Task> LoadAll() + public virtual async Task> LoadAll() where T : ZeroIdEntity, new() { List items = new(); - await foreach (T item in Stream()) + await foreach (T item in Stream()) { items.Add(item); } @@ -73,11 +70,11 @@ public abstract partial class EntityCollection /// - public virtual IAsyncEnumerable Stream() => Stream(null); + public virtual IAsyncEnumerable Stream() where T : ZeroIdEntity, new() => Stream(null); /// - public virtual async IAsyncEnumerable Stream(Func, IRavenQueryable> expression) + public virtual async IAsyncEnumerable Stream(Func, IRavenQueryable> expression) where T : ZeroIdEntity, new() { IRavenQueryable query = Session.Query(); diff --git a/zero.Core/Collections/CollectionOperations.Write.cs b/zero.Core/Collections/CollectionOperations.Write.cs new file mode 100644 index 00000000..c02177c3 --- /dev/null +++ b/zero.Core/Collections/CollectionOperations.Write.cs @@ -0,0 +1,51 @@ +using FluentValidation.Results; + +namespace zero.Collections; + +public abstract partial class CollectionOperations +{ + /// + public virtual async Task> Save(T model, Func> validate = null) where T : ZeroIdEntity, new() + { + if (model == null) + { + return EntityResult.Fail("@errors.onsave.empty"); + } + + bool isUpdate = model.Id.IsNullOrEmpty() ? false : await Session.Advanced.ExistsAsync(model.Id); + + // prepare model + PrepareForSave(model); + + // run validator + if (validate != null) + { + ValidationResult validation = await validate(model); + if (!validation.IsValid) + { + return EntityResult.Fail(validation); + } + } + + // create ID before-hand so interceptors can use it + if (!isUpdate) + { + model.Id = await GenerateId(model); + } + + // run interceptor + InterceptorInstruction instruction = isUpdate ? Interceptors.ForUpdate(model) : Interceptors.ForCreate(model); + + if (!await instruction.Start()) + { + return instruction.Result; + } + + // store our model + await Session.StoreAsync(model); + await instruction.Complete(); + await Session.SaveChangesAsync(); + + return EntityResult.Success(model); + } +} \ No newline at end of file diff --git a/zero.Core/Collections/CollectionOperations.cs b/zero.Core/Collections/CollectionOperations.cs new file mode 100644 index 00000000..c8f35e9f --- /dev/null +++ b/zero.Core/Collections/CollectionOperations.cs @@ -0,0 +1,200 @@ +using FluentValidation.Results; +using Raven.Client.Documents.Indexes; +using Raven.Client.Documents.Linq; +using System.Security.Claims; + +namespace zero.Collections; + +public abstract partial class CollectionOperations : ICollectionOperations +{ + /// + public IZeroDocumentSession Session => Context.Store.Session(); + + protected record EntityCollectionOptions(bool IncludeInactive); + + protected IZeroContext Context { get; private set; } + + protected EntityCollectionOptions Options { get; set; } + + protected IInterceptors Interceptors { get; private set; } + + + public CollectionOperations(ICollectionContext collectionContext) + { + Context = collectionContext.Context; + Interceptors = collectionContext.Interceptors; + Options = new(true); + } + + + /// + /// Get new instance of an entity + /// + public virtual Task Empty() where T : ZeroIdEntity, new() + { + return Task.FromResult(new T()); + } + + + /// + public async Task GenerateId(T model) where T : ZeroIdEntity + { + IZeroDocumentSession session = Context.Store.Session(); + return await session.Advanced.DocumentStore.Conventions.GenerateDocumentIdAsync(session.Advanced.DocumentStore.Database, model); + } + + + /// + public T AutoSetIds(T model) + { + // find all Raven Ids + List> ravenIds = ObjectTraverser.FindAttribute(model); + + // set unset Raven Ids + foreach (ObjectTraverser.Result item in ravenIds) + { + string id = item.Property.GetValue(item.Parent, null) as string; + if (id.IsNullOrWhiteSpace()) + { + id = item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create(); + item.Property.SetValue(item.Parent, id); + } + } + + return model; + } + + + /// + public T PrepareForSave(T model) where T : ZeroIdEntity + { + // set IDs + AutoSetIds(model); + + if (model is ZeroEntity) + { + ZeroEntity zeroModel = model as ZeroEntity; + + // get current user + string userId = Context.BackofficeUser.FindFirstValue(Constants.Auth.Claims.UserId); + + // set default properties + if (zeroModel.CreatedDate == default) + { + zeroModel.CreatedDate = DateTimeOffset.Now; + } + if (zeroModel.CreatedById.IsNullOrEmpty()) + { + zeroModel.CreatedById = userId; + } + if (zeroModel.LanguageId.IsNullOrEmpty()) + { + zeroModel.LanguageId ??= "languages.1-A"; // TODO correct language id + } + + // update name alias and last modified + zeroModel.Alias = Safenames.Alias(zeroModel.Name); + zeroModel.LastModifiedById = userId; + zeroModel.LastModifiedDate = DateTimeOffset.Now; + zeroModel.CreatedById ??= userId; + zeroModel.Hash ??= IdGenerator.Create(); + } + + return model; + } + + + /// + /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() + /// + protected virtual T WhenActive(T model) where T : ZeroIdEntity, new() + { + return model != null && (Options.IncludeInactive || (model is ZeroEntity ? (model as ZeroEntity).IsActive : true)) ? model : default; + } +} + + + +public interface ICollectionOperations +{ + /// + /// Get new instance of an entity + /// + Task Empty() where T : ZeroIdEntity, new(); + + /// + /// Generate model Id by using configured document store conventions + /// + Task GenerateId(T model) where T : ZeroIdEntity; + + /// + /// Generate values for all properties (incl. nested) which contain the [GenerateId] attribute + /// + T AutoSetIds(T model); + + /// + /// Automatically fill base properties of a ZeroEntity + /// + T PrepareForSave(T model) where T : ZeroIdEntity; + + /// + /// Get an entity by Id + /// + Task Load(string id, string changeVector = null) where T : ZeroIdEntity, new(); + + /// + /// Get entities by ids + /// + Task> Load(IEnumerable ids) where T : ZeroIdEntity, new(); + + /// + /// Get entities by query + /// + Task> Load(ListQuery query) where T : ZeroIdEntity, new(); + + /// + /// Get entities by query (by using the specified index) + /// + Task> Load(ListQuery query) where T : ZeroIdEntity, new() where TIndex : AbstractCommonApiForIndexes, new(); + + /// + /// Get all entities from this collection. + /// Warning: Don't use this method for large collections. Stream the results instead. + /// + Task> LoadAll() where T : ZeroIdEntity, new(); + + /// + /// Stream the collection + /// + IAsyncEnumerable Stream() where T : ZeroIdEntity, new(); + + /// + /// Stream the collection + /// + IAsyncEnumerable Stream(Func, IRavenQueryable> expression) where T : ZeroIdEntity, new(); + + /// + /// Updates or creates an entity with an optional validator + /// + Task> Save(T model, Func> validate = null) where T : ZeroIdEntity, new(); + + /// + /// Deletes an entity + /// + Task> Delete(T model) where T : ZeroIdEntity, new(); + + /// + /// Deletes entities + /// + Task Delete(IEnumerable models) where T : ZeroIdEntity, new(); + + /// + /// Deletes an entity by Id + /// + Task> Delete(string id) where T : ZeroIdEntity, new(); + + /// + /// Deletes entities by Id + /// + Task Delete(IEnumerable ids) where T : ZeroIdEntity, new(); +} \ No newline at end of file diff --git a/zero.Core/Collections/CountriesCollection.cs b/zero.Core/Collections/CountriesCollection.cs new file mode 100644 index 00000000..7592297f --- /dev/null +++ b/zero.Core/Collections/CountriesCollection.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace zero.Collections; + +public class CountriesCollection : EntityCollection, ICountriesCollection +{ + public CountriesCollection(ICollectionContext context) : base(context) { } + + /// + protected override void ValidationRules(ZeroValidator validator) + { + validator.RuleFor(x => x.Code).Length(2).Unique(Session); + validator.RuleFor(x => x.Name).Length(2, 120); + } +} + + +public interface ICountriesCollection : IEntityCollection { } \ No newline at end of file diff --git a/old/zero.Core/Collections/EntityCollection/EntityCollection.cs b/zero.Core/Collections/EntityCollection.cs similarity index 59% rename from old/zero.Core/Collections/EntityCollection/EntityCollection.cs rename to zero.Core/Collections/EntityCollection.cs index 2f69f27c..55fbc7cd 100644 --- a/old/zero.Core/Collections/EntityCollection/EntityCollection.cs +++ b/zero.Core/Collections/EntityCollection.cs @@ -1,14 +1,8 @@ -namespace zero.Core; -using FluentValidation.Results; +using FluentValidation.Results; using Raven.Client.Documents.Indexes; using Raven.Client.Documents.Linq; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Validation; +namespace zero.Collections; public abstract partial class EntityCollection : IEntityCollection where T : ZeroIdEntity, new() { @@ -25,25 +19,58 @@ public abstract partial class EntityCollection : IEntityCollection where T protected EntityCollectionOptions Options { get; set; } - protected IInterceptorRunner Interceptors { get; private set; } + protected IInterceptors Interceptors { get; private set; } + + protected ICollectionOperations Operations { get; private set; } - public EntityCollection(ICollectionContext collectionContext) + public EntityCollection(ICollectionContext collectionContext) { + Operations = collectionContext.Operations; Context = collectionContext.Context; Interceptors = collectionContext.Interceptors; Options = new(true); } - /// - /// Get new instance of an entity - /// - public virtual Task Empty() - { - return Task.FromResult(new T()); - } + /// + public virtual Task Empty() => Operations.Empty(); + /// + public virtual Task Load(string id, string changeVector = null) => Operations.Load(id, changeVector); + + /// + public virtual Task> Load(IEnumerable ids) => Operations.Load(ids); + + /// + public virtual Task> Load(ListQuery query) => Operations.Load(query); + + /// + public virtual Task> Load(ListQuery query) where TIndex : AbstractCommonApiForIndexes, new() => Operations.Load(query); + + /// + public virtual Task> LoadAll() => Operations.LoadAll(); + + /// + public virtual IAsyncEnumerable Stream() => Operations.Stream(); + + /// + public virtual IAsyncEnumerable Stream(Func, IRavenQueryable> expression) => Operations.Stream(expression); + + /// + public virtual Task> Save(T model) => Operations.Save(model, async m => await Validate(m)); + + /// + public virtual Task> Delete(T model) => Operations.Delete(model); + + /// + public virtual Task Delete(IEnumerable models) => Operations.Delete(models); + + /// + public virtual Task> Delete(string id) => Operations.Delete(id); + + /// + public virtual Task Delete(IEnumerable ids) => Operations.Delete(ids); /// public virtual async Task Validate(T model) @@ -63,10 +90,7 @@ public abstract partial class EntityCollection : IEntityCollection where T /// /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() /// - protected virtual T WhenActive(T model) - { - return model != null && (Options.IncludeInactive || (model is ZeroEntity ? (model as ZeroEntity).IsActive : true)) ? model : default; - } + protected virtual T WhenActive(T model) => model != null && (Options.IncludeInactive || (model is ZeroEntity ? (model as ZeroEntity).IsActive : true)) ? model : default; } diff --git a/zero.Core/Collections/LanguagesCollection.cs b/zero.Core/Collections/LanguagesCollection.cs new file mode 100644 index 00000000..8cc5dc73 --- /dev/null +++ b/zero.Core/Collections/LanguagesCollection.cs @@ -0,0 +1,51 @@ +using FluentValidation; +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using System.Globalization; + +namespace zero.Collections; + +public class LanguagesCollection : EntityCollection, ILanguagesCollection +{ + public LanguagesCollection(ICollectionContext context) : base(context) { } + + + /// + public List GetAllCultures(params string[] codes) + { + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Where(x => !x.Name.IsNullOrWhiteSpace()) + .Select(x => new CultureInfo(x.Name)) + .Where(x => codes.Length > 0 ? codes.Contains(x.Name, StringComparer.InvariantCultureIgnoreCase) : true) + .OrderBy(x => x.DisplayName) + .Select(x => new Culture() + { + Code = x.Name, + Name = x.DisplayName + }) + .ToList(); + } + + + /// + protected override void ValidationRules(ZeroValidator validator) + { + validator.RuleFor(x => x.Name).Length(2, 60); + validator.RuleFor(x => x.Code).Length(2, 10).Culture(); + validator.RuleFor(x => x.IsDefault).Unique(Context.Store).When(x => x.IsDefault).WithMessage("@language.errors.default_unique"); + validator.RuleFor(x => x.IsDefault).ExpectAnyUnique(Context.Store, expectedValue: true).When(x => !x.IsDefault).WithMessage("@language.errors.needs_default"); + validator.RuleFor(x => x.InheritedLanguageId).Must((entity, value) => !entity.Id.Equals(value, StringComparison.InvariantCultureIgnoreCase)).When(x => !x.Id.IsNullOrEmpty()).WithMessage("@language.errors.fallback_invalid"); + validator.RuleFor(x => x.InheritedLanguageId).Equal((string)null).When(x => x.IsDefault).WithMessage("@language.errors.default_no_fallback"); + validator.RuleFor(x => x.InheritedLanguageId).Exists(Context.Store); + validator.RuleFor(x => x.IsOptional).Equal(false).When(x => x.IsDefault).WithMessage("@language.errors.default_not_optional"); + } +} + + +public interface ILanguagesCollection : IEntityCollection +{ + /// + /// Get all available cultures + /// + List GetAllCultures(params string[] codes); +} \ No newline at end of file diff --git a/zero.Core/Collections/MailTemplatesCollection.cs b/zero.Core/Collections/MailTemplatesCollection.cs new file mode 100644 index 00000000..5cc5c0a3 --- /dev/null +++ b/zero.Core/Collections/MailTemplatesCollection.cs @@ -0,0 +1,46 @@ +using FluentValidation; +using Raven.Client.Documents; + +namespace zero.Collections; + +public class MailTemplatesCollection : EntityCollection, IMailTemplatesCollection +{ + protected IMailDispatcher Dispatcher { get; set; } + + + public MailTemplatesCollection(ICollectionContext context, IMailDispatcher dispatcher = null) : base(context) + { + Dispatcher = dispatcher; + } + + + /// + public async Task GetByKey(string key) + { + return await Session.Query().FirstOrDefaultAsync(x => x.Key == key); + } + + + /// + protected override void ValidationRules(ZeroValidator validator) + { + validator.RuleFor(x => x.SenderEmail).Email(); + + if (Dispatcher != null) + { + validator.RuleFor(x => x.SenderEmail).MustAsync(async (value, ct) => + { + return await Dispatcher.IsSenderSupported(value); + }).WithMessage("@mailTemplate.errors.senderNotAllowed"); + } + } +} + + +public interface IMailTemplatesCollection : IEntityCollection +{ + /// + /// Get mail template by associated key + /// + Task GetByKey(string key); +} \ No newline at end of file diff --git a/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs b/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs deleted file mode 100644 index 021f61ec..00000000 --- a/zero.Core/Configuration/ConfigurationParts/SectionOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using zero.Core.Entities; - -namespace zero.Configuration; - -public class SectionOptions : OptionsEnumerable, IOptionsEnumerable -{ - public SectionOptions() - { - - } - - - public void Add(int index = -1) where T : ISection, new() - { - if (index > -1 && index < Items.Count) - { - Items.Insert(index, new T()); - } - else - { - Items.Add(new T()); - } - } - - public void Add(string alias, string name, string icon) - { - Items.Add(new Section(alias, name, icon)); - } -} diff --git a/zero.Core/Configuration/ZeroOptions.cs b/zero.Core/Configuration/ZeroOptions.cs index e23545be..cfa5d7a5 100644 --- a/zero.Core/Configuration/ZeroOptions.cs +++ b/zero.Core/Configuration/ZeroOptions.cs @@ -66,8 +66,6 @@ public class ZeroOptions : IZeroOptions ///// //public ApplicationOptions Applications { get; set; } - ///// - //public SectionOptions Sections { get; private set; } ///// //public FeatureOptions Features { get; private set; } diff --git a/zero.Core/Extensions/ObjectExtensions.cs b/zero.Core/Extensions/ObjectExtensions.cs index 25ef9f9a..4f10bbf0 100644 --- a/zero.Core/Extensions/ObjectExtensions.cs +++ b/zero.Core/Extensions/ObjectExtensions.cs @@ -1,48 +1,30 @@ -using Newtonsoft.Json; +//using Newtonsoft.Json; -namespace zero.Extensions; +//namespace zero.Extensions; -[Obsolete("we don't want this for every object (use a Utils class instead)")] -public static class ObjectExtensions -{ - public static bool Is(this Type type) - { - return type.IsAssignableFrom(typeof(T)); - } +//[Obsolete("we don't want this for every object (use a Utils class instead)")] +//public static class ObjectExtensions +//{ +// public static bool Is(this Type type) +// { +// return type.IsAssignableFrom(typeof(T)); +// } - public static bool Is(this object obj) - { - return obj.GetType().IsAssignableFrom(typeof(T)); - } +// public static bool Is(this object obj) +// { +// return obj.GetType().IsAssignableFrom(typeof(T)); +// } - public static T Clone(this T obj) - { - Type type = obj.GetType(); - return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), type, new RefJsonConverter()); - } +// public static T Clone(this T obj) +// { +// Type type = obj.GetType(); +// return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj), type); +// } - public static T CloneLax(this object obj) - { - return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), new RefJsonConverter()); - } - - public static T AutoSetIds(this T obj) - { - // find all Raven Ids - List> ravenIds = ObjectTraverser.FindAttribute(obj); - - // set unset Raven Ids - foreach (ObjectTraverser.Result item in ravenIds) - { - string id = item.Property.GetValue(item.Parent, null) as string; - if (String.IsNullOrWhiteSpace(id)) - { - item.Property.SetValue(item.Parent, item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create()); - } - } - - return obj; - } -} +// public static T CloneLax(this object obj) +// { +// return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj)); +// } +//} diff --git a/zero.Core/Extensions/QueryableExtensions.cs b/zero.Core/Extensions/QueryableExtensions.cs deleted file mode 100644 index 249933dd..00000000 --- a/zero.Core/Extensions/QueryableExtensions.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using Raven.Client.Documents.Queries; -using Raven.Client.Documents.Session; -using System.Linq.Expressions; - -namespace zero.Extensions; - -public static class QueryableExtensions -{ - public static IOrderedQueryable OrderBy(this IQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) - { - if (!String.IsNullOrEmpty(path)) - { - char[] a = path.ToCharArray(); - a[0] = char.ToUpper(a[0]); - path = new string(a); - } - - if (isDescending) - { - return source.OrderByDescending(path, type); - } - return source.OrderBy(path, type); - } - - - public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) - { - if (!String.IsNullOrEmpty(path)) - { - char[] a = path.ToCharArray(); - a[0] = char.ToUpper(a[0]); - path = new string(a); - } - - if (isDescending) - { - return source.ThenByDescending(path, type); - } - return source.ThenBy(path, type); - } - - - public static IQueryable Paging(this IQueryable source, int pageNumber, int pageSize) - { - pageNumber = pageNumber.Limit(1, 10_000_000); - pageSize = pageSize.Limit(1, 1_000); - - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } - - return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); - } - - - public static IRavenQueryable WhereIf(this IRavenQueryable source, Expression> predicate, bool condition, Expression> elsePredicate = null) - { - if (!condition) - { - if (elsePredicate != null) - { - return source.Where(elsePredicate); - } - return source; - } - - return source.Where(predicate); - } - - - public static IQueryable SearchIf(this IQueryable source, Expression> fieldSelector, string searchTerms, string suffix = null, string prefix = null, SearchOperator @operator = SearchOperator.Or) - { - if (String.IsNullOrWhiteSpace(searchTerms)) - { - return source; - } - - searchTerms = searchTerms.Trim(); - - string[] searchParts = searchTerms.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => - { - if (suffix != null) - { - x += suffix; - } - if (prefix != null) - { - x = prefix + x; - } - return x; - }).ToArray(); - - if (searchTerms.StartsWith('"') && searchTerms.EndsWith('"')) - { - searchParts = new[] { searchTerms }; - } - - return source.Search(fieldSelector, searchParts, @operator: @operator); - } -} diff --git a/zero.Core/Persistence/Module.cs b/zero.Core/Persistence/Module.cs index 493d02ae..aa4e00a7 100644 --- a/zero.Core/Persistence/Module.cs +++ b/zero.Core/Persistence/Module.cs @@ -13,6 +13,8 @@ internal class PersistenceModule : ZeroModule config.Services.AddSingleton(CreateRavenStore); config.Services.AddScoped(); config.Services.AddScoped(); + config.Services.AddScoped(); + config.Services.AddScoped(); } @@ -34,14 +36,14 @@ internal class PersistenceModule : ZeroModule IZeroDocumentStore store = new ZeroDocumentStore(options) { Urls = new string[1] { options.For().Url }, - Conventions = // TODO activate and test this + Conventions = + { + AggressiveCache = { - AggressiveCache = - { - Duration = TimeSpan.FromHours(1), - Mode = AggressiveCacheMode.TrackChanges - } + Duration = TimeSpan.FromHours(1), + Mode = AggressiveCacheMode.TrackChanges } + } }; conventionsBuilder.Run(store.Conventions); diff --git a/zero.Core/Persistence/RavenQueryableExtensions.cs b/zero.Core/Persistence/RavenQueryableExtensions.cs index 73cfe399..112bcd70 100644 --- a/zero.Core/Persistence/RavenQueryableExtensions.cs +++ b/zero.Core/Persistence/RavenQueryableExtensions.cs @@ -1,194 +1,175 @@ -//using Raven.Client.Documents; -//using Raven.Client.Documents.Linq; -//using Raven.Client.Documents.Session; -//using System.Collections.Generic; -//using System.Linq; -//using System.Threading.Tasks; +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Queries; +using Raven.Client.Documents.Session; +using System.Linq.Expressions; -//namespace zero; +namespace zero.Persistence; -//public static class RavenQueryableExtensions -//{ -// // TODO we need to simplify these extensions methods. -// // ToQueriedListAsyncX is used in MediaCollection for the Media_ByParent index, which produces MediaListItem (which is no ZeroEntity) -// /// -// /// -// /// -// public static async Task> ToQueriedListAsyncX(this IRavenQueryable queryable, ListQuery query) -// { -// queryable = queryable.Statistics(out QueryStatistics stats); - -// IQueryable rawQuery = queryable; - -// if (query != null) -// { -// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null) -// { -// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); -// } -// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0) -// { -// foreach (var selector in query.SearchSelectors) -// { -// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); -// } -// } - -// if (query.OrderQuery != null) -// { -// rawQuery = query.OrderQuery(rawQuery); -// } -// else if (!query.OrderBy.IsNullOrEmpty()) -// { -// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double); -// } - -// if (query.PageSize > 0) -// { -// rawQuery = rawQuery.Paging(query.Page, query.PageSize); -// } -// } - -// List items = await rawQuery.ToListAsync(); - -// return new ListResult(items, stats.TotalResults, query.Page, query.PageSize); -// } +public static class RavenQueryableExtensions +{ + /// + /// + /// + public static async Task> FilterAsync(this IRavenQueryable source, ListQuery listQuery) + { + return await source.WithFilter(listQuery).ToListResultAsync(listQuery); + } -// /// -// /// -// /// -// public static async Task> ToQueriedListAsync(this IRavenQueryable queryable, ListQuery query) where T : ZeroEntity -// { -// queryable = queryable.Statistics(out QueryStatistics stats); + /// + /// + /// + public static IQueryable WithFilter(this IRavenQueryable source, ListQuery listQuery) + { + Type collectionType = typeof(T); + Type zeroType = typeof(ZeroEntity); + bool isZeroType = zeroType.IsAssignableFrom(collectionType); -// IQueryable rawQuery = queryable; + source.Statistics(out QueryStatistics stats); + listQuery.Statistics = stats; -// if (query != null) -// { -// if (!query.IncludeInactive) -// { -// rawQuery = rawQuery.Where(x => x.IsActive); -// } + IQueryable queryable = source; -// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0) -// { -// foreach (var selector in query.SearchSelectors) -// { -// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); -// } -// } -// else if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null) -// { -// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); -// } + if (!listQuery.IncludeInactive && isZeroType) + { + queryable = queryable.Where(x => (x as ZeroEntity).IsActive); + } -// if (query.OrderQuery != null) -// { -// rawQuery = query.OrderQuery(rawQuery); -// } -// else if (!query.OrderBy.IsNullOrEmpty()) -// { -// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double); -// } -// else -// { -// rawQuery = rawQuery.OrderByDescending(x => x.CreatedDate); -// } + if (!listQuery.Search.IsNullOrEmpty() && listQuery.SearchSelectors.Length > 0) + { + foreach (var selector in listQuery.SearchSelectors) + { + queryable = queryable.SearchIf(selector, listQuery.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); + } + } + else if (!listQuery.Search.IsNullOrEmpty() && listQuery.SearchSelector != null) + { + queryable = queryable.SearchIf(listQuery.SearchSelector, listQuery.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And); + } -// if (query.PageSize > 0) -// { -// rawQuery = rawQuery.Paging(query.Page, query.PageSize); -// } -// } + if (listQuery.OrderQuery != null) + { + queryable = listQuery.OrderQuery(queryable); + } + else if (!listQuery.OrderBy.IsNullOrEmpty()) + { + queryable = queryable.OrderBy(listQuery.OrderBy, listQuery.OrderIsDescending, listQuery.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double); + } + else if (isZeroType) + { + queryable = queryable.OrderByDescending(x => (x as ZeroEntity).CreatedDate); + } -// List items = await rawQuery.ToListAsync(); + if (listQuery.PageSize > 0) + { + queryable = queryable.Paging(listQuery.Page, listQuery.PageSize); + } -// return new ListResult(items, stats.TotalResults, query.Page, query.PageSize); -// } + return queryable; + } -// /// -// /// -// /// -// public static async Task> ToQueriedListAsyncX(this IRavenQueryable queryable, ListQuery query) where TFilter : IListSpecificQuery -// { -// queryable = queryable.Statistics(out QueryStatistics stats); - -// IQueryable rawQuery = queryable; - -// if (query != null) -// { -// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null) -// { -// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*"); -// } -// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0) -// { -// foreach (var selector in query.SearchSelectors) -// { -// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.Or); -// } -// } - -// if (!query.OrderBy.IsNullOrEmpty()) -// { -// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double); -// } - -// if (query.PageSize > 0) -// { -// rawQuery = rawQuery.Paging(query.Page, query.PageSize); -// } -// } - -// List items = await rawQuery.ToListAsync(); - -// return new ListResult(items, stats.TotalResults, query.Page, query.PageSize); -// } + /// + /// + /// + public static async Task> ToListResultAsync(this IQueryable source, ListQuery listQuery, CancellationToken token = default) + { + List results = await source.ToListAsync(token); + return new ListResult(results, listQuery.Statistics.TotalResults, listQuery.Page, listQuery.PageSize); + } -// /// -// /// -// /// -// public static async Task> ToQueriedListAsync(this IRavenQueryable queryable, ListQuery query) where T : ZeroEntity where TFilter : IListSpecificQuery -// { -// queryable = queryable.Statistics(out QueryStatistics stats); + public static IOrderedQueryable OrderBy(this IQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) + { + if (!String.IsNullOrEmpty(path)) + { + char[] a = path.ToCharArray(); + a[0] = char.ToUpper(a[0]); + path = new string(a); + } -// IQueryable rawQuery = queryable; + if (isDescending) + { + return source.OrderByDescending(path, type); + } + return source.OrderBy(path, type); + } -// if (query != null) -// { -// if (!query.IncludeInactive) -// { -// rawQuery = rawQuery.Where(x => x.IsActive); -// } -// if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null) -// { -// rawQuery = rawQuery.SearchIf(query.SearchSelector, query.Search, "*", "*"); -// } -// if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0) -// { -// foreach (var selector in query.SearchSelectors) -// { -// rawQuery = rawQuery.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.Or); -// } -// } + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string path, bool isDescending, OrderingType type = OrderingType.String) + { + if (!String.IsNullOrEmpty(path)) + { + char[] a = path.ToCharArray(); + a[0] = char.ToUpper(a[0]); + path = new string(a); + } -// if (!query.OrderBy.IsNullOrEmpty()) -// { -// rawQuery = rawQuery.OrderBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double); -// } + if (isDescending) + { + return source.ThenByDescending(path, type); + } + return source.ThenBy(path, type); + } -// if (query.PageSize > 0) -// { -// rawQuery = rawQuery.Paging(query.Page, query.PageSize); -// } -// } -// List items = await rawQuery.ToListAsync(); + public static IQueryable Paging(this IQueryable source, int pageNumber, int pageSize) + { + pageNumber = pageNumber.Limit(1, 10_000_000); + pageSize = pageSize.Limit(1, 1_000); -// return new ListResult(items, stats.TotalResults, query.Page, query.PageSize); -// } -//} + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + return source.Skip((pageNumber - 1) * pageSize).Take(pageSize); + } + + + public static IRavenQueryable WhereIf(this IRavenQueryable source, Expression> predicate, bool condition, Expression> elsePredicate = null) + { + if (!condition) + { + if (elsePredicate != null) + { + return source.Where(elsePredicate); + } + return source; + } + + return source.Where(predicate); + } + + + public static IQueryable SearchIf(this IQueryable source, Expression> fieldSelector, string searchTerms, string suffix = null, string prefix = null, SearchOperator @operator = SearchOperator.Or) + { + if (String.IsNullOrWhiteSpace(searchTerms)) + { + return source; + } + + searchTerms = searchTerms.Trim(); + + string[] searchParts = searchTerms.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => + { + if (suffix != null) + { + x += suffix; + } + if (prefix != null) + { + x = prefix + x; + } + return x; + }).ToArray(); + + if (searchTerms.StartsWith('"') && searchTerms.EndsWith('"')) + { + searchParts = new[] { searchTerms }; + } + + return source.Search(fieldSelector, searchParts, @operator: @operator); + } +} diff --git a/zero.Core/Usings.cs b/zero.Core/Usings.cs index 512a1975..d45620ca 100644 --- a/zero.Core/Usings.cs +++ b/zero.Core/Usings.cs @@ -22,3 +22,4 @@ global using zero.Rendering; global using zero.Routing; global using zero.Utils; global using zero.Validation; +global using zero.Collections; \ No newline at end of file