using FluentValidation; using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using zero.Core.Database.Indexes; using zero.Core.Entities; using zero.Core.Extensions; using zero.Core.Handlers; using zero.Core.Options; namespace zero.Core.Api { public class PagesApi : BackofficeApi, IPagesApi { const string RECYCLE_BIN_GROUP = "pages"; protected IZeroOptions Options { get; private set; } protected IRecycleBinApi RecycleBinApi { get; private set; } protected ILogger Logger { get; private set; } protected IHandlerHolder Handler { get; private set; } public PagesApi(IZeroOptions options, IBackofficeStore store, IRecycleBinApi recycleBinApi, ILogger logger, IHandlerHolder handler) : base(store) { Options = options; RecycleBinApi = recycleBinApi; Logger = logger; Handler = handler; } /// public Task GetEmpty(string pageType, string parentId = null) { PageType type = GetPageType(pageType); if (type == null) { return Task.FromResult(null); } try { IPage model = Activator.CreateInstance(type.ContentType) as IPage; model.PageTypeAlias = type.Alias; model.ParentId = parentId; // TODO validate if type is allowed and if parentid is allowed Handler.Get()?.OnCreate(model); return Task.FromResult(model); } catch { Logger.LogWarning("Could not create page with type {type}", type); } return Task.FromResult(null); } /// public async Task GetById(string id) { return await GetById(id); } /// public async Task> GetByIds(params string[] ids) { return await GetByIds(ids); } /// public IList GetPageTypes() { return Options.Pages.GetAllItems().ToList(); } /// public async Task> GetAllowedPageTypes(string parentId = null) { IEnumerable types = Options.Pages.GetAllItems(); if (parentId.IsNullOrEmpty()) { return types.Where(x => x.AllowAsRoot || x.OnlyAtRoot).ToList(); } Page page = await GetById(parentId); PageType pageType = page != null ? types.FirstOrDefault(x => x.Alias == page.PageTypeAlias) : null; if (pageType == null) { return new List(); } if (pageType.AllowAllChildrenTypes) { return types.Where(x => !x.OnlyAtRoot).ToList(); } return types.Where(x => !x.OnlyAtRoot && pageType.AllowedChildrenTypes.Contains(x.Alias)).ToList(); } /// public PageType GetPageType(string alias) { return Options.Pages.GetAllItems().FirstOrDefault(x => x.Alias == alias); } /// public async Task> Save(IPage model) { return await SaveModel(model, null); } /// public async Task>> SaveSorting(string[] sortedIds) { Dictionary items = await GetByIds(sortedIds); uint index = 0; // contains multiple parents, therefore fail if (items.Select(x => x.Value?.ParentId).Distinct().Count() > 1) { return EntityResult>.Fail("@errors.page.sortingmultipleparents"); } using (IAsyncDocumentSession session = Store.OpenAsyncSession()) { session.Advanced.WaitForIndexesAfterSaveChanges(throwOnTimeout: false); foreach (var item in items) { item.Value.Sort = index; index += 10; await session.StoreAsync(item.Value); //session.Advanced.Patch(item.Value.Id, x => x.Sort, index++); } await session.SaveChangesAsync(); } return EntityResult>.Success(items.Select(x => x.Value).ToList()); } /// public async Task> Move(string id, string parentId) { IPage model = await GetById(id); IPage parent = await GetById(parentId); if (model == null || (!parentId.IsNullOrEmpty() && parent == null)) { return EntityResult.Fail("@errors.idnotfound"); } IList pageTypes = await GetAllowedPageTypes(parentId); if (!pageTypes.Any(x => x.Alias == model.PageTypeAlias)) { return EntityResult.Fail("@errors.page.parentnotallowed"); } model.ParentId = parent?.Id; return await Save(model); } /// public async Task> Copy(string id, string destinationId, bool includeDescendants = false) { IPage model = await GetById(id); IPage parent = await GetById(destinationId); if (model == null || (!destinationId.IsNullOrEmpty() && parent == null)) { return EntityResult.Fail("@errors.idnotfound"); } string baseId = model.Id; // update new page properties model.Id = null; model.ParentId = parent?.Id; model.IsActive = false; model.CreatedDate = DateTimeOffset.Now; // check if new parent is allowed IList pageTypes = await GetAllowedPageTypes(destinationId); if (!pageTypes.Any(x => x.Alias == model.PageTypeAlias)) { return EntityResult.Fail("@errors.page.parentnotallowed"); } using (IAsyncDocumentSession session = Store.OpenAsyncSession()) { session.Advanced.WaitForIndexesAfterSaveChanges(throwOnTimeout: false); // recursive function to store descendants async Task AddChildren(string oldParentId, string newParentId) { Pages_WithChildren.Result childrenResult = await session.Query() .ProjectInto() .Include(x => x.Id) .Where(x => x.Id == oldParentId) .FirstOrDefaultAsync(); if (childrenResult == null || childrenResult.ChildrenIds.Length < 1) { return; } Dictionary childrenPages = await session.LoadAsync(childrenResult.ChildrenIds); foreach (var child in childrenPages) { IPage childPage = child.Value.Clone(); childPage.Id = null; childPage.IsActive = false; childPage.ParentId = newParentId; childPage.CreatedDate = DateTimeOffset.Now; await session.StoreAsync(childPage); await AddChildren(child.Key, childPage.Id); } } await session.StoreAsync(model); if (includeDescendants) { await AddChildren(baseId, model.Id); } await session.SaveChangesAsync(); } return EntityResult.Success(model); } /// public async Task> Delete(string id, bool moveToRecycleBin = true) { IList pages = await GetByIdWithDescendants(id); string[] ids = pages.Select(x => x.Id).ToArray(); if (pages.Count < 1) { return EntityResult.Fail("@errors.ondelete.idnotfound"); } if (moveToRecycleBin) { await RecycleBinApi.Add(pages, RECYCLE_BIN_GROUP); } await DeleteByIds(ids); return EntityResult.Success(ids); } /// public async Task> Restore(string id, bool includeDescendants = false) { EntityResult result = new EntityResult(); IRecycledEntity recycledEntity = await RecycleBinApi.GetById(id); List entities = new List() { recycledEntity }; if (recycledEntity == null) { return EntityResult.Fail(); // TODO correct error message } // get descendants from the operation if (includeDescendants && !recycledEntity.OperationId.IsNullOrEmpty()) { entities = (await RecycleBinApi.GetByOperation(recycledEntity.OperationId)).ToList(); } // fill ids string[] ids = entities.Select(x => x.OriginalId).ToArray(); // check if parents are available string[] parentIds = entities.Select(x => x.Content as IPage).Where(x => x != null).Select(x => x.ParentId).ToArray(); parentIds = (await GetByIds(parentIds)).Where(x => x.Value != null).Select(x => x.Value.Id).ToArray(); // validate and restore all items foreach (IRecycledEntity entity in entities) { // check if it contains page data if (entity.Group != RECYCLE_BIN_GROUP || !(entity.Content is IPage)) { //result.AddError("Cannot parse recycled entity as an IPage in group \"" + RECYCLE_BIN_GROUP + "\""); // TODO correct error message continue; } // get page IPage page = entity.Content as IPage; page.IsActive = false; // validate app and parent if (!page.ParentId.IsNullOrEmpty() && !ids.Contains(page.ParentId) && !parentIds.Contains(page.ParentId)) { // TODO correct error message continue; } // restore to pages EntityResult saveResult = await SaveModel(page); } // delete affected entities from recycle bin if (!recycledEntity.OperationId.IsNullOrEmpty()) { await RecycleBinApi.DeleteByOperation(recycledEntity.OperationId); } // set result result.Model = ids; result.IsSuccess = true; return result; } /// /// Get a page with all its descendants /// async Task> GetByIdWithDescendants(string id) { List items = new List(); IPage model = await GetById(id); if (model == null) { return items; } items.Add(model); using (IAsyncDocumentSession session = Store.OpenAsyncSession()) { // recursive function to store descendants async Task AddChildren(string parentId) { Pages_WithChildren.Result childrenResult = await session.Query() .ProjectInto() .Include(x => x.Id) .Where(x => x.Id == parentId) .FirstOrDefaultAsync(); if (childrenResult == null || childrenResult.ChildrenIds.Length < 1) { return; } Dictionary childrenPages = await session.LoadAsync(childrenResult.ChildrenIds); foreach (var child in childrenPages) { items.Add(child.Value); await AddChildren(child.Value.Id); } } await AddChildren(model.Id); } return items; } } public interface IPagesApi { /// /// Get a new empty page with the specified type /// public Task GetEmpty(string pageType, string parentId = null); /// /// Get page by Id /// Task GetById(string id); /// /// Get pages by ids /// Task> GetByIds(params string[] ids); /// /// Get all available page types /// IList GetPageTypes(); /// /// Get all page types which are allowed below a selected parent page /// Task> GetAllowedPageTypes(string parentId = null); /// /// Get a specific page type by alias /// PageType GetPageType(string alias); /// /// Creates or updates a page /// Task> Save(IPage model); /// /// Update sorting of pages on a specific level /// Task>> SaveSorting(string[] sortedIds); /// /// Move a page to a new parent /// Task> Move(string id, string parentId); /// /// Copies a page (with optional descendants) to a new location /// Task> Copy(string id, string destinationId, bool includeDescendants = false); /// /// Deletes a page by Id (with all it's descendants) /// Task> Delete(string id, bool moveToRecycleBin = true); /// /// Restores a page from the recycle bin /// Task> Restore(string id, bool includeDescendants = false); } }