using FluentValidation; using FluentValidation.Results; using Raven.Client; using Raven.Client.Documents.Indexes; 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.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 ICollectionInterceptorHandler InterceptorHandler { get; private set; } 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; InterceptorHandler = collectionContext.InterceptorHandler; 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); // run interceptor var instruction = CreateInstruction.CreateParameters>("create", args => args.Model = model); await instruction.HandleBefore(x => x.Creating(instruction.Parameters)); if (instruction.Return) { return instruction.EntityResult; } // run generic interceptor var instruction2 = CreateInstruction.SaveParameters>("save", args => { args.Model = model; args.Id = model.Id; args.IsUpdate = false; }); await instruction2.HandleBefore(x => x.Saving(instruction2.Parameters)); if (instruction2.Return) { return instruction2.EntityResult; } await Session.StoreAsync(model); await instruction.HandleAfter(x => x.Created(instruction.Parameters)); await instruction2.HandleAfter(x => x.Saved(instruction2.Parameters)); await Session.SaveChangesAsync(); return EntityResult.Success(model); } /// async Task> Update(T model) { // run interceptor var instruction = CreateInstruction.UpdateParameters>("update", args => { args.Model = model; args.Id = model.Id; }); await instruction.HandleBefore(x => x.Updating(instruction.Parameters)); if (instruction.Return) { return instruction.EntityResult; } // run generic interceptor var instruction2 = CreateInstruction.SaveParameters>("save", args => { args.Model = model; args.Id = model.Id; args.IsUpdate = true; }); await instruction2.HandleBefore(x => x.Saving(instruction2.Parameters)); if (instruction2.Return) { return instruction2.EntityResult; } await Session.StoreAsync(model); await instruction.HandleAfter(x => x.Updated(instruction.Parameters)); await instruction2.HandleAfter(x => x.Saved(instruction2.Parameters)); 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"); } var instruction = CreateInstruction.DeleteParameters>("delete", args => { args.Model = entity; args.Id = entity.Id; }); await instruction.HandleBefore(x => x.Deleting(instruction.Parameters)); if (instruction.Return) { return instruction.EntityResult; } Session.Delete(entity); await instruction.HandleAfter(x => x.Deleted(instruction.Parameters)); 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) { var instruction = CreateInstruction.PurgeParameters>("purge"); await instruction.HandleBefore(x => x.Purging(instruction.Parameters)); if (instruction.Return) { return instruction.EntityResult; } await Store.Raven.PurgeAsync(Database, querySuffix, parameters); await instruction.HandleAfter(x => x.Purged(instruction.Parameters)); return EntityResult.Success(); } /// /// Get interceptor parameters /// protected InterceptorInstruction CreateInstruction(string operationName, Action configure = null) where TParams : CollectionInterceptor.Parameters, new() { TParams parameters = new TParams() { Context = Context, Store = Store, Validator = Validator, Session = Session }; configure?.Invoke(parameters); return InterceptorHandler?.Create(operationName, parameters) ?? new(); } /// /// 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); } }