diff --git a/zero.Core/Backoffice/BackofficeServiceBase.cs b/zero.Core/Backoffice/BackofficeServiceBase.cs new file mode 100644 index 00000000..20f1af1f --- /dev/null +++ b/zero.Core/Backoffice/BackofficeServiceBase.cs @@ -0,0 +1,309 @@ +using FluentValidation; +using FluentValidation.Results; +using Raven.Client; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Session; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using zero.Core.Api; +using zero.Core.Attributes; +using zero.Core.Database; +using zero.Core.Entities; +using zero.Core.Extensions; +using zero.Core.Utils; + +namespace zero.Core.Backoffice +{ + public abstract class BackofficeService : IBackofficeService where T : IZeroEntity + { + /// + public string Database { get; set; } + + /// + /// Zero context + /// + protected readonly IZeroContext Context; + + /// + /// Document store + /// + protected readonly IZeroStore Store; + + /// + /// The validator + /// + protected readonly IValidator Validator; + + + public BackofficeService(IZeroContext context, IValidator validator = null) + { + Context = context; + Store = context.Store; + Validator = validator; + } + + + /// + /// Create an an async document session + /// + protected virtual IAsyncDocumentSession Session() + { + return Database.IsNullOrWhiteSpace() ? Store.OpenAsyncSession() : Store.OpenAsyncSession(Database); + } + + + /// + public virtual async Task GetById(string id) + { + if (id.IsNullOrWhiteSpace()) + { + return default; + } + + using IAsyncDocumentSession session = Session(); + return await session.LoadAsync(id); + } + + + /// + public virtual async Task> GetByIds(params string[] ids) + { + using IAsyncDocumentSession session = Session(); + Dictionary models = await session.LoadAsync(ids); + Dictionary result = new Dictionary(); + + foreach (string id in ids) + { + models.TryGetValue(id, out T model); + result.Add(id, model); + } + + return result; + } + + + /// + public virtual async Task> GetByQuery(ListQuery query) + { + using IAsyncDocumentSession session = Session(); + return await session.Query().ToQueriedListAsync(query); + } + + + /// + public virtual IAsyncEnumerable Stream() => Stream(null); + + + /// + public virtual async IAsyncEnumerable Stream(Func, IRavenQueryable> expression) + { + using IAsyncDocumentSession session = Session(); + IRavenQueryable query = session.Query(); + + if (expression != null) + { + query = expression(query); + } + + var stream = await session.Advanced.StreamAsync(query); + + while (await stream.MoveNextAsync()) + { + yield return stream.Current.Document; + } + } + + + /// + public async Task> Save(T model) + { + bool isCreate = false; + + // run validator + if (Validator != null) + { + ValidationResult validation = await Validator.ValidateAsync(model); + + if (!validation.IsValid) + { + return EntityResult.Fail(validation); + } + } + + // 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 (String.IsNullOrWhiteSpace(id)) + { + item.Property.SetValue(item.Parent, item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create()); + } + } + + // get current user + string userId = Context.BackofficeUser.FindFirstValue(Constants.Auth.Claims.UserId); + + // set default properties + if (model.Id.IsNullOrEmpty()) + { + isCreate = true; + + model.CreatedDate = DateTimeOffset.Now; + model.CreatedById = userId; + + if (model is ILanguageAwareEntity) + { + (model as ILanguageAwareEntity).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.Create(); + + using IAsyncDocumentSession session = Session(); + session.Advanced.WaitForIndexesAfterSaveChanges(throwOnTimeout: false); + + await session.StoreAsync(model); + + //await Backoffice.Messages.Publish(new EntitySavedMessage() + //{ + // Id = model.Id, + // IsCreate = isCreate, + // IsDelete = false, + // Model = model, + // Session = session + //}); + + 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) + { + using IAsyncDocumentSession session = Session(); + session.Advanced.WaitForIndexesAfterSaveChanges(throwOnTimeout: false); + + 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.PurgeAsync(Database, querySuffix, parameters); + return EntityResult.Success(); + } + } + + + public interface IBackofficeService where T : IZeroEntity + { + /// + /// The database to operate on. + /// Is null by default, which uses the database from the resolved application. + /// + string Database { get; set; } + + /// + /// Get an entity by Id + /// + Task GetById(string id); + + /// + /// Get entities by ids + /// + Task> GetByIds(params string[] ids); + + /// + /// Get entities by query + /// + Task> GetByQuery(ListQuery query); + + /// + /// 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); + } +} diff --git a/zero.Core/Backoffice/Countries/CountriesService.cs b/zero.Core/Backoffice/Countries/CountriesService.cs new file mode 100644 index 00000000..49afa027 --- /dev/null +++ b/zero.Core/Backoffice/Countries/CountriesService.cs @@ -0,0 +1,40 @@ +using FluentValidation; +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using System.Collections.Generic; +using zero.Core.Entities; +using zero.Core.Extensions; + +namespace zero.Core.Backoffice +{ + public class CountriesBackofficeService : BackofficeService, ICountriesBackofficeService + { + public CountriesBackofficeService(IZeroContext context, IValidator validator) : base(context, validator) { } + + + /// + public override IAsyncEnumerable Stream() + { + return base.Stream(q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name)); + } + + + /// + //public async Task> GetByQuery(string languageId, ListQuery query) + //{ + // query.SearchSelector = country => country.Name; + + // using IAsyncDocumentSession session = Store.OpenAsyncSession(); + // return await session.Query() + // .OrderByDescending(x => x.IsPreferred) + // .ThenBy(x => x.Name) + // .ToQueriedListAsync(query); + //} + } + + + public interface ICountriesBackofficeService : IBackofficeService + { + + } +} diff --git a/zero.Core/ZeroContext.cs b/zero.Core/ZeroContext.cs index fe8a7751..2efbbc5d 100644 --- a/zero.Core/ZeroContext.cs +++ b/zero.Core/ZeroContext.cs @@ -34,13 +34,14 @@ namespace zero.Core /// public IResolvedRoute ResolvedRoute { get; private set; } + /// + public IZeroStore Store { get; private set; } + protected IApplicationResolver AppResolver { get; private set; } protected ILogger Logger { get; private set; } - protected IZeroStore Store { get; private set; } - private bool _resolved = false; @@ -124,6 +125,11 @@ namespace zero.Core /// IZeroOptions Options { get; } + /// + /// Document store + /// + IZeroStore Store { get; } + /// /// Matching (frontend) path route /// diff --git a/zero.Web/Controllers/BackofficeController.cs b/zero.Web/Controllers/BackofficeController.cs index c837e19f..51c55378 100644 --- a/zero.Web/Controllers/BackofficeController.cs +++ b/zero.Web/Controllers/BackofficeController.cs @@ -112,6 +112,25 @@ namespace zero.Web.Controllers + public async Task> SelectList(IAsyncEnumerable enumerable) where T : IZeroEntity + { + List items = new List(); + + await foreach (T item in enumerable) + { + items.Add(new SelectModel() + { + Id = item.Id, + Name = item.Name, + IsActive = item.IsActive + }); + } + + return items; + } + + + public async Task> Previews(Dictionary items, Func> transform) { IList previews = new List(); diff --git a/zero.Web/Controllers/CountriesController.cs b/zero.Web/Controllers/CountriesController.cs index 260a0d0d..dac4c29a 100644 --- a/zero.Web/Controllers/CountriesController.cs +++ b/zero.Web/Controllers/CountriesController.cs @@ -1,12 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using zero.Core; -using zero.Core.Api; +using zero.Core.Backoffice; using zero.Core.Entities; -using zero.Core.Extensions; using zero.Core.Identity; using zero.Web.Models; @@ -15,34 +11,25 @@ namespace zero.Web.Controllers [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)] public class CountriesController : BackofficeController { - ICountriesApi Api; + ICountriesBackofficeService Service; - public CountriesController(ICountriesApi api) + public CountriesController(ICountriesBackofficeService service) { - Api = api; + Service = service; } - public async Task> GetById([FromQuery] string id) => Edit(await Api.GetById(id)); - + public async Task> GetById([FromQuery] string id) => Edit(await Service.GetById(id)); public EditModel GetEmpty([FromServices] ICountry blueprint) => Edit(blueprint); + public async Task> GetAll([FromQuery] ListQuery query) => await Service.GetByQuery(query); - public async Task> GetAll([FromQuery] ListQuery query) => await Api.GetByQuery("languages.1-A", query); // TODO correct language - - - public async Task> GetForPicker() => (await Api.GetAll("languages.1-A")).Select(x => new SelectModel() - { - Id = x.Id, - Name = x.Name, - IsActive = x.IsActive - }); - + public async Task> GetForPicker() => await SelectList(Service.Stream()); public async Task> GetPreviews([FromQuery] List ids) { - return Previews(await Api.GetByIds(ids.ToArray()), item => new PreviewModel() + return Previews(await Service.GetByIds(ids.ToArray()), item => new PreviewModel() { Id = item.Id, Icon = "flag flag-" + item.Code.ToLowerInvariant(), @@ -52,10 +39,10 @@ namespace zero.Web.Controllers [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Update)] - public async Task> Save([FromBody] ICountry model) => await Api.Save(model); + public async Task> Save([FromBody] ICountry model) => await Service.Save(model); [ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Update)] - public async Task> Delete([FromQuery] string id) => await Api.Delete(id); + public async Task> Delete([FromQuery] string id) => await Service.DeleteById(id); } } diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs index 6e54b109..30bfee95 100644 --- a/zero.Web/Defaults/ZeroBackofficePlugin.cs +++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using zero.Core.Api; +using zero.Core.Backoffice; using zero.Core.Entities; using zero.Core.Messages; using zero.Core.Options; @@ -71,6 +72,7 @@ namespace zero.Web.Defaults services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient();