From c8858c7972295870725fa7a7ff5102ccddb91bb9 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 24 Nov 2021 13:56:08 +0100 Subject: [PATCH] RE 6 --- .../Configuration/BackofficeOptions.cs | 5 - .../Modules/Pages/PageTreeService.cs | 150 +++++ .../Modules/Pages/PagesController.cs | 66 +++ .../Pages/ServiceCollectionExtensions.cs | 12 + .../Translations/TranslationsController.cs | 4 +- .../Resources/Localization/zero.en-us.json | 4 + .../_legacy/Controllers/PagesController.cs | 4 +- .../_legacy/Defaults/ZeroBackofficePlugin.cs | 2 +- zero.Core/Applications/ApplicationResolver.cs | 2 +- zero.Core/Applications/ApplicationStore.cs | 13 + .../ServiceCollectionExtensions.cs | 1 + .../ServiceCollectionExtensions.cs | 6 +- zero.Core/Configuration/ZeroOptions.cs | 13 +- .../CountriesStore.cs | 6 +- zero.Core/Localization/CultureService.cs | 31 + .../LanguagesStore.cs | 31 +- .../ServiceCollectionExtensions.cs | 3 + zero.Core/Localization/TranslationsStore.cs | 31 + .../{Stores => Mails}/MailTemplatesStore.cs | 2 +- .../Mails/ServiceCollectionExtensions.cs | 1 + zero.Core/Models/IZeroTreeEntity.cs | 19 + zero.Core/Pages/{Entities => Models}/Page.cs | 2 +- .../Pages/{Entities => Models}/PageFolder.cs | 0 .../Pages/{Entities => Models}/PageType.cs | 0 zero.Core/Pages/PageTypeService.cs | 90 +++ zero.Core/Pages/PagesStore.cs | 52 ++ .../Pages/ServiceCollectionExtensions.cs | 3 + zero.Core/Persistence/ZeroStore.cs | 2 +- zero.Core/Stores/CachableEntityStore.cs | 37 -- zero.Core/Stores/EntityStore.cs | 23 +- .../Stores/ServiceCollectionExtensions.cs | 5 - zero.Core/Stores/StoreCache.cs | 10 +- zero.Core/Stores/StoreConfig.cs | 14 + zero.Core/Stores/StoreContext.cs | 7 +- zero.Core/Stores/StoreOperations.Tree.cs | 197 +++++++ zero.Core/Stores/StoreOperations.cs | 40 ++ zero.Core/Stores/TreeEntityStore.cs | 89 +++ zero.Core/Unimplemented/PreviewApi.cs | 75 +++ zero.Core/Unimplemented/RecycleBinApi.cs | 258 +++++++++ .../Unimplemented/RecycleBinListQuery.cs | 9 + zero.Core/Unimplemented/RecycledEntity.cs | 28 + zero.Core/Unimplemented/RevisionsApi.cs | 102 ++++ zero.Core/Unimplemented/SetupApi.cs | 339 +++++++++++ zero.Core/Utils/ObjectCopycat.cs | 9 +- zero.Core/_legacy/Api/ApplicationsApi.cs | 91 --- zero.Core/_legacy/Api/BackofficeApi.cs | 242 -------- zero.Core/_legacy/Api/BackofficeStore.cs | 43 -- zero.Core/_legacy/Api/PreviewApi.cs | 75 --- zero.Core/_legacy/Api/RecycleBinApi.cs | 180 ------ zero.Core/_legacy/Api/RevisionsApi.cs | 102 ---- zero.Core/_legacy/Api/SectionsApi.cs | 47 -- zero.Core/_legacy/Api/SettingsApi.cs | 33 -- zero.Core/_legacy/Api/SetupApi.cs | 339 ----------- .../Collections/Pages/PagesCollection.cs | 534 ------------------ .../Translations/TranslationValidator.cs | 17 - .../Translations/TranslationsCollection.cs | 25 - .../RecycleBin/RecycleBinListQuery.cs | 9 - .../Entities/RecycleBin/RecycledEntity.cs | 28 - 58 files changed, 1691 insertions(+), 1871 deletions(-) create mode 100644 zero.Backoffice/Modules/Pages/PageTreeService.cs create mode 100644 zero.Backoffice/Modules/Pages/PagesController.cs create mode 100644 zero.Backoffice/Modules/Pages/ServiceCollectionExtensions.cs create mode 100644 zero.Core/Applications/ApplicationStore.cs rename zero.Core/{Stores => Localization}/CountriesStore.cs (62%) create mode 100644 zero.Core/Localization/CultureService.cs rename zero.Core/{Stores => Localization}/LanguagesStore.cs (60%) create mode 100644 zero.Core/Localization/TranslationsStore.cs rename zero.Core/{Stores => Mails}/MailTemplatesStore.cs (97%) create mode 100644 zero.Core/Models/IZeroTreeEntity.cs rename zero.Core/Pages/{Entities => Models}/Page.cs (94%) rename zero.Core/Pages/{Entities => Models}/PageFolder.cs (100%) rename zero.Core/Pages/{Entities => Models}/PageType.cs (100%) create mode 100644 zero.Core/Pages/PageTypeService.cs create mode 100644 zero.Core/Pages/PagesStore.cs delete mode 100644 zero.Core/Stores/CachableEntityStore.cs create mode 100644 zero.Core/Stores/StoreConfig.cs create mode 100644 zero.Core/Stores/StoreOperations.Tree.cs create mode 100644 zero.Core/Stores/TreeEntityStore.cs create mode 100644 zero.Core/Unimplemented/PreviewApi.cs create mode 100644 zero.Core/Unimplemented/RecycleBinApi.cs create mode 100644 zero.Core/Unimplemented/RecycleBinListQuery.cs create mode 100644 zero.Core/Unimplemented/RecycledEntity.cs create mode 100644 zero.Core/Unimplemented/RevisionsApi.cs create mode 100644 zero.Core/Unimplemented/SetupApi.cs delete mode 100644 zero.Core/_legacy/Api/ApplicationsApi.cs delete mode 100644 zero.Core/_legacy/Api/BackofficeApi.cs delete mode 100644 zero.Core/_legacy/Api/BackofficeStore.cs delete mode 100644 zero.Core/_legacy/Api/PreviewApi.cs delete mode 100644 zero.Core/_legacy/Api/RecycleBinApi.cs delete mode 100644 zero.Core/_legacy/Api/RevisionsApi.cs delete mode 100644 zero.Core/_legacy/Api/SectionsApi.cs delete mode 100644 zero.Core/_legacy/Api/SettingsApi.cs delete mode 100644 zero.Core/_legacy/Api/SetupApi.cs delete mode 100644 zero.Core/_legacy/Collections/Pages/PagesCollection.cs delete mode 100644 zero.Core/_legacy/Collections/Translations/TranslationValidator.cs delete mode 100644 zero.Core/_legacy/Collections/Translations/TranslationsCollection.cs delete mode 100644 zero.Core/_legacy/Entities/RecycleBin/RecycleBinListQuery.cs delete mode 100644 zero.Core/_legacy/Entities/RecycleBin/RecycledEntity.cs diff --git a/zero.Backoffice/Configuration/BackofficeOptions.cs b/zero.Backoffice/Configuration/BackofficeOptions.cs index dd7b71da..688f6901 100644 --- a/zero.Backoffice/Configuration/BackofficeOptions.cs +++ b/zero.Backoffice/Configuration/BackofficeOptions.cs @@ -2,11 +2,6 @@ public class BackofficeOptions { - /// - /// URL path to the backoffice (defaults to /zero) - /// - public string Path { get; set; } - /// /// Paths in the backoffice which are not handled by zero /// diff --git a/zero.Backoffice/Modules/Pages/PageTreeService.cs b/zero.Backoffice/Modules/Pages/PageTreeService.cs new file mode 100644 index 00000000..51f4085e --- /dev/null +++ b/zero.Backoffice/Modules/Pages/PageTreeService.cs @@ -0,0 +1,150 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Session; + +namespace zero.Backoffice.Modules; + +public class PageTreeService : IPageTreeService +{ + protected IPagesStore Pages { get; private set; } + + protected IRoutes Routes { get; set; } + + protected PageOptions PageOptions { get; set; } + + + public PageTreeService(IPagesStore pages, IZeroOptions options, IRoutes routes) + { + Pages = pages; + Routes = routes; + PageOptions = options.For(); + } + + + /// + public async Task> GetChildren(string parentId = null, string activeId = null, string search = null) + { + IList items = new List(); + IReadOnlyCollection pageTypes = PageOptions.GetAllItems(); + string[] openIds = new string[0] { }; + Paged pages = null; + IList children = null; + bool isSearch = !search.IsNullOrWhiteSpace(); + + if (isSearch) + { + pages = await Pages.Load(1, Int32.MaxValue, q => q.SearchIf(x => x.Name, search, "*").OrderBy(x => x.Sort, OrderingType.Long)); + + var urls = await Routes.GetUrls(pages.Items.ToArray()); + + foreach (Page page in pages.Items) + { + if (urls.TryGetValue(page, out string url)) + { + page.Url = url; + } + } + } + else + { + pages = await Pages.Load(1, Int32.MaxValue, q => q + .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) + .OrderBy(x => x.Sort, OrderingType.Long)); + + + // get hierarchy so we know if we should set the page to open + if (!activeId.IsNullOrEmpty()) + { + Pages_ByHierarchy.Result result = await Pages.Session.Query() + .ProjectInto() + .Include(x => x.Path.Select(p => p.Id)) + .FirstOrDefaultAsync(x => x.Id == activeId); + + if (result != null) + { + openIds = result.Path.Select(x => x.Id).ToArray(); // .Union(new string[1] { activeId }) + } + } + + + // get children for all pages + string[] pageIds = pages.Items.Select(x => x.Id).ToArray(); + + children = await Pages.Session.Query() + .ProjectInto() + .Where(x => x.Id.In(pageIds)) + .ToListAsync(); + } + + + // function to get modifier icon + TreeItemModifier GetModifier(Page page) + { + if (page.PublishDate > DateTimeOffset.Now || page.UnpublishDate > DateTimeOffset.Now) + { + return new TreeItemModifier("@page.schedule.scheduled", "fth-clock"); + } + if (!page.IsActive) + { + return new TreeItemModifier("@ui.inactive", "fth-minus-circle color-red"); + } + return null; + } + + + // build tree + foreach (Page page in pages.Items) + { + PageType pageType = pageTypes.FirstOrDefault(x => x.Alias == page.PageTypeAlias); + + if (pageType == null) + { + continue; + // TODO the page type does not exist anymore + } + + int childCount = isSearch ? 0 : children.Count(x => x.Id == page.Id); + + items.Add(new TreeItem() + { + Id = page.Id, + Name = page.Name, + HasChildren = childCount > 0, + ChildCount = childCount, + ParentId = page.ParentId, + Sort = page.Sort, + Icon = pageType.Icon, + IsOpen = openIds.Contains(page.Id), + IsInactive = !page.IsActive, + HasActions = true, + Modifier = GetModifier(page), + Description = isSearch ? page.Url : null + }); + } + + if (parentId.IsNullOrEmpty()) + { + items.Add(new TreeItem() + { + Id = "recyclebin", + ParentId = null, + Sort = 99999, + Name = "@recyclebin.name", + Icon = "fth-trash", + HasChildren = false, + HasActions = true + }); + } + + return items; + } +} + + +public interface IPageTreeService +{ + /// + /// Get page children as tree items + /// + Task> GetChildren(string parentId = null, string activeId = null, string search = null); +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Pages/PagesController.cs b/zero.Backoffice/Modules/Pages/PagesController.cs new file mode 100644 index 00000000..31c2b91a --- /dev/null +++ b/zero.Backoffice/Modules/Pages/PagesController.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Mvc; + +namespace zero.Backoffice.Modules; + +/// +/// | GET /zero/api/pages/empty +/// | GET /zero/api/pages/{id} +/// | GET /zero/api/pages/{id}/revisions +/// | GET /zero/api/pages[type,query,filter,...] +/// | POST /zero/api/pages +/// | PUT /zero/api/pages/{id} +/// | DELETE /zero/api/pages/{id} +/// +public class PagesController : ZeroBackofficeApiController +{ + protected IPagesStore Store { get; set; } + + public PagesController(IPagesStore store) + { + Store = store; + } + + + [HttpGet("empty")] + public virtual async Task New() + { + return await Store.Empty(); + } + + [HttpGet("{id}")] + public virtual async Task Get(string id, string changeVector = null) + { + return await Store.Load(id, changeVector); + } + + [HttpGet] + public virtual async Task> Get(ListBackofficeQuery query) + { + return await Store.Load(query.Page, query.PageSize, q => q.OrderByDescending(x => x.CreatedDate)); + } + + //[HttpGet("{id}/revisions")] + //public virtual async Task>> GetRevisions(string id) + //{ + // return await Collection.GetRevisions(id); + //} + + + [HttpPost] + public virtual async Task> Create(Country model) + { + return await Store.Create(model); + } + + [HttpPut("{id}")] + public virtual async Task> Update(string id, Country model) + { + return await Store.Update(model); + } + + [HttpDelete("{id}")] + public virtual async Task> Delete(string id) + { + return await Store.Delete(id); + } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Pages/ServiceCollectionExtensions.cs b/zero.Backoffice/Modules/Pages/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..2df74e2b --- /dev/null +++ b/zero.Backoffice/Modules/Pages/ServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Backoffice.Modules; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddZeroBackofficePages(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Translations/TranslationsController.cs b/zero.Backoffice/Modules/Translations/TranslationsController.cs index c9ac78a4..e3a0fe69 100644 --- a/zero.Backoffice/Modules/Translations/TranslationsController.cs +++ b/zero.Backoffice/Modules/Translations/TranslationsController.cs @@ -10,9 +10,9 @@ using zero.Core.Identity; namespace zero.Web.Controllers { [ZeroAuthorize(Permissions.Settings.Translations, PermissionsValue.Read)] - public class TranslationsController : ZeroBackofficeCollectionController + public class TranslationsController : ZeroBackofficeCollectionController { - public TranslationsController(ITranslationsCollection collection) : base(collection) { } + public TranslationsController(ITranslationsStore collection) : base(collection) { } public override async Task> GetByQuery([FromQuery] ListBackofficeQuery query) { diff --git a/zero.Backoffice/Resources/Localization/zero.en-us.json b/zero.Backoffice/Resources/Localization/zero.en-us.json index df1ba925..455bfa76 100644 --- a/zero.Backoffice/Resources/Localization/zero.en-us.json +++ b/zero.Backoffice/Resources/Localization/zero.en-us.json @@ -215,6 +215,10 @@ "parentnotallowed": "The page is not allowed as a children of the selected parent", "sortingmultipleparents": "Sorting pages is only allowed for a specific parent page" }, + "treeentity": { + "parentnotallowed": "This entity is not allowed as a children of the selected parent", + "sortingmultipleparents": "The selected entities have multiple parents, which is not allowed" + }, "forms": { "email_invalid": "The email address is not valid", "emails_invalid": "The email address(es) is/are not valid", diff --git a/zero.Backoffice/_legacy/Controllers/PagesController.cs b/zero.Backoffice/_legacy/Controllers/PagesController.cs index 25bcd226..78a6ee79 100644 --- a/zero.Backoffice/_legacy/Controllers/PagesController.cs +++ b/zero.Backoffice/_legacy/Controllers/PagesController.cs @@ -12,11 +12,11 @@ using zero.Web.Models; namespace zero.Web.Controllers { - public class PagesController : BackofficeCollectionController + public class PagesController : BackofficeCollectionController { IRoutes Routes; - public PagesController(IPagesCollection collection, IRoutes routes) : base(collection) + public PagesController(IPageService collection, IRoutes routes) : base(collection) { Routes = routes; } diff --git a/zero.Backoffice/_legacy/Defaults/ZeroBackofficePlugin.cs b/zero.Backoffice/_legacy/Defaults/ZeroBackofficePlugin.cs index 9238c5ab..e4dd2169 100644 --- a/zero.Backoffice/_legacy/Defaults/ZeroBackofficePlugin.cs +++ b/zero.Backoffice/_legacy/Defaults/ZeroBackofficePlugin.cs @@ -21,7 +21,7 @@ namespace zero.Web.Defaults services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/zero.Core/Applications/ApplicationResolver.cs b/zero.Core/Applications/ApplicationResolver.cs index 9f9b3feb..f04a4c60 100644 --- a/zero.Core/Applications/ApplicationResolver.cs +++ b/zero.Core/Applications/ApplicationResolver.cs @@ -41,7 +41,7 @@ public class ApplicationResolver : IApplicationResolver Application app; - if (context.IsBackofficeRequest(Options.BackofficePath)) + if (context.IsBackofficeRequest(Options.ZeroPath)) { app = await ResolveFromUser(user); } diff --git a/zero.Core/Applications/ApplicationStore.cs b/zero.Core/Applications/ApplicationStore.cs new file mode 100644 index 00000000..056da224 --- /dev/null +++ b/zero.Core/Applications/ApplicationStore.cs @@ -0,0 +1,13 @@ +namespace zero.Applications; + +public class ApplicationStore : EntityStore, IApplicationStore +{ + public ApplicationStore(IStoreContext context) : base(context) + { + Config.Database = Options.For().Database; + } +} + +public interface IApplicationStore : IEntityStore +{ +} \ No newline at end of file diff --git a/zero.Core/Applications/ServiceCollectionExtensions.cs b/zero.Core/Applications/ServiceCollectionExtensions.cs index f0a444a8..74aaf7f0 100644 --- a/zero.Core/Applications/ServiceCollectionExtensions.cs +++ b/zero.Core/Applications/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ internal static class ServiceCollectionExtensions public static IServiceCollection AddZeroApplications(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); return services; } } \ No newline at end of file diff --git a/zero.Core/Configuration/ServiceCollectionExtensions.cs b/zero.Core/Configuration/ServiceCollectionExtensions.cs index dbc3d293..a89ad3a6 100644 --- a/zero.Core/Configuration/ServiceCollectionExtensions.cs +++ b/zero.Core/Configuration/ServiceCollectionExtensions.cs @@ -8,7 +8,11 @@ internal static class ServiceCollectionExtensions { public static IServiceCollection AddZeroConfiguration(this IServiceCollection services, IConfiguration config) { - services.AddOptions().Bind(config.GetSection("Zero")).Configure(opts => { }); + services.AddOptions().Bind(config.GetSection("Zero")).Configure(opts => + { + opts.Version = "0.0.1-alpha.1"; + opts.ZeroPath = "/zero"; + }); services.AddTransient(factory => factory.GetService>().CurrentValue); return services; } diff --git a/zero.Core/Configuration/ZeroOptions.cs b/zero.Core/Configuration/ZeroOptions.cs index cfa5d7a5..c97ed144 100644 --- a/zero.Core/Configuration/ZeroOptions.cs +++ b/zero.Core/Configuration/ZeroOptions.cs @@ -13,7 +13,6 @@ public class ZeroOptions : IZeroOptions public ZeroOptions(IServiceProvider serviceProvider) { - Version = "0.0.1-alpha.1"; ServiceProvider = serviceProvider; //SupportedLanguages = new string[2] { "en-US", "de-DE" }; @@ -49,7 +48,10 @@ public class ZeroOptions : IZeroOptions public bool Initialized => !String.IsNullOrEmpty(For()?.Database); /// - public string Version { get; private set; } + public string Version { get; set; } + + /// + public string ZeroPath { get; set; } ///// //public string DefaultLanguage { get; set; } @@ -92,11 +94,16 @@ public class ZeroOptions : IZeroOptions public interface IZeroOptions { + /// + /// Path to the backoffice. Defaults to /zero + /// + string ZeroPath { get; set; } + /// /// The currently active version /// This should not be set manually, as it is used for setup and migrations and incremented automatically /// - string Version { get; } + string Version { get; set; } /// /// Whether this zero instance is initialized (setup is completed) diff --git a/zero.Core/Stores/CountriesStore.cs b/zero.Core/Localization/CountriesStore.cs similarity index 62% rename from zero.Core/Stores/CountriesStore.cs rename to zero.Core/Localization/CountriesStore.cs index a17c37e6..ba1c108d 100644 --- a/zero.Core/Stores/CountriesStore.cs +++ b/zero.Core/Localization/CountriesStore.cs @@ -1,10 +1,10 @@ using FluentValidation; -namespace zero.Stores; +namespace zero.Localization; -public class CountriesStore : CachableEntityStore, ICountriesStore +public class CountriesStore : EntityStore, ICountriesStore { - public CountriesStore(IStoreContext context, IStoreCache cache) : base(context, cache) { } + public CountriesStore(IStoreContext context) : base(context) { } /// protected override void ValidationRules(ZeroValidator validator) diff --git a/zero.Core/Localization/CultureService.cs b/zero.Core/Localization/CultureService.cs new file mode 100644 index 00000000..18f56d1c --- /dev/null +++ b/zero.Core/Localization/CultureService.cs @@ -0,0 +1,31 @@ +using System.Globalization; + +namespace zero.Localization; + +public class CultureService : ICultureService +{ + /// + 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(); + } +} + + +public interface ICultureService +{ + /// + /// Get all available cultures + /// + List GetAllCultures(params string[] codes); +} \ No newline at end of file diff --git a/zero.Core/Stores/LanguagesStore.cs b/zero.Core/Localization/LanguagesStore.cs similarity index 60% rename from zero.Core/Stores/LanguagesStore.cs rename to zero.Core/Localization/LanguagesStore.cs index 670c762e..f26bba99 100644 --- a/zero.Core/Stores/LanguagesStore.cs +++ b/zero.Core/Localization/LanguagesStore.cs @@ -1,32 +1,11 @@ using FluentValidation; -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using System.Globalization; -namespace zero.Stores; +namespace zero.Localization; public class LanguagesStore : EntityStore, ILanguagesStore { public LanguagesStore(IStoreContext 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) { @@ -42,10 +21,4 @@ public class LanguagesStore : EntityStore, ILanguagesStore } -public interface ILanguagesStore : IEntityStore -{ - /// - /// Get all available cultures - /// - List GetAllCultures(params string[] codes); -} \ No newline at end of file +public interface ILanguagesStore : IEntityStore { } \ No newline at end of file diff --git a/zero.Core/Localization/ServiceCollectionExtensions.cs b/zero.Core/Localization/ServiceCollectionExtensions.cs index 260aac34..0a013256 100644 --- a/zero.Core/Localization/ServiceCollectionExtensions.cs +++ b/zero.Core/Localization/ServiceCollectionExtensions.cs @@ -7,6 +7,9 @@ internal static class ServiceCollectionExtensions public static IServiceCollection AddZeroLocalization(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); return services; } diff --git a/zero.Core/Localization/TranslationsStore.cs b/zero.Core/Localization/TranslationsStore.cs new file mode 100644 index 00000000..723c5488 --- /dev/null +++ b/zero.Core/Localization/TranslationsStore.cs @@ -0,0 +1,31 @@ +using FluentValidation; + +namespace zero.Localization; + +public class TranslationsStore : EntityStore, ITranslationsStore +{ + public TranslationsStore(IStoreContext context) : base(context) { } + + + /// + public async Task LoadString(string id) + { + return (await Load(id))?.Value; + } + + + protected override void ValidationRules(ZeroValidator validator) + { + validator.RuleFor(x => x.Key).Length(2, 300).Unique(Context.Store); + validator.RuleFor(x => x.Value).MaximumLength(10 * 1000); + } +} + + +public interface ITranslationsStore : IEntityStore +{ + /// + /// Get a translated string by id + /// + Task LoadString(string id); +} \ No newline at end of file diff --git a/zero.Core/Stores/MailTemplatesStore.cs b/zero.Core/Mails/MailTemplatesStore.cs similarity index 97% rename from zero.Core/Stores/MailTemplatesStore.cs rename to zero.Core/Mails/MailTemplatesStore.cs index 3ad6ef72..3e54fe7a 100644 --- a/zero.Core/Stores/MailTemplatesStore.cs +++ b/zero.Core/Mails/MailTemplatesStore.cs @@ -1,7 +1,7 @@ using FluentValidation; using Raven.Client.Documents; -namespace zero.Stores; +namespace zero.Mails; public class MailTemplatesStore : EntityStore, IMailTemplatesStore { diff --git a/zero.Core/Mails/ServiceCollectionExtensions.cs b/zero.Core/Mails/ServiceCollectionExtensions.cs index 6165788b..3d89191a 100644 --- a/zero.Core/Mails/ServiceCollectionExtensions.cs +++ b/zero.Core/Mails/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ internal static class ServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } \ No newline at end of file diff --git a/zero.Core/Models/IZeroTreeEntity.cs b/zero.Core/Models/IZeroTreeEntity.cs new file mode 100644 index 00000000..5336cb19 --- /dev/null +++ b/zero.Core/Models/IZeroTreeEntity.cs @@ -0,0 +1,19 @@ +namespace zero.Models; + +public interface IZeroTreeEntity +{ + /// + /// Id of the entity + /// + string Id { get; set; } + + /// + /// Parent id + /// + string ParentId { get; set; } + + /// + /// Sort order + /// + uint Sort { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Pages/Entities/Page.cs b/zero.Core/Pages/Models/Page.cs similarity index 94% rename from zero.Core/Pages/Entities/Page.cs rename to zero.Core/Pages/Models/Page.cs index b9d73012..82085c47 100644 --- a/zero.Core/Pages/Entities/Page.cs +++ b/zero.Core/Pages/Models/Page.cs @@ -5,7 +5,7 @@ /// The backoffice rendering is done by an IRenderer /// [RavenCollection("Pages")] -public class Page : ZeroEntity +public class Page : ZeroEntity, IZeroTreeEntity { /// /// Use this field (when filled out) instead of the alias for URL generation diff --git a/zero.Core/Pages/Entities/PageFolder.cs b/zero.Core/Pages/Models/PageFolder.cs similarity index 100% rename from zero.Core/Pages/Entities/PageFolder.cs rename to zero.Core/Pages/Models/PageFolder.cs diff --git a/zero.Core/Pages/Entities/PageType.cs b/zero.Core/Pages/Models/PageType.cs similarity index 100% rename from zero.Core/Pages/Entities/PageType.cs rename to zero.Core/Pages/Models/PageType.cs diff --git a/zero.Core/Pages/PageTypeService.cs b/zero.Core/Pages/PageTypeService.cs new file mode 100644 index 00000000..e0d70ffc --- /dev/null +++ b/zero.Core/Pages/PageTypeService.cs @@ -0,0 +1,90 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; + +namespace zero.Pages; + +public class PageTypeService : IPageTypeService +{ + protected IZeroContext Context { get; private set; } + + protected IHandlerHolder Handler { get; private set; } + + protected PageOptions PageOptions { get; set; } + + + public PageTypeService(IZeroContext context, IZeroOptions options, IHandlerHolder handler) + { + Context = context; + Handler = handler; + PageOptions = options.For(); + } + + + /// + public IList GetPageTypes() + { + return PageOptions.GetAllItems().ToList(); + } + + + /// + public PageType GetPageType(string pageTypeAlias) + { + return PageOptions.GetAllItems().FirstOrDefault(x => x.Alias == pageTypeAlias); + } + + + /// + public async Task> GetAllowedPageTypes(string pageParentId = null) + { + IEnumerable types = PageOptions.GetAllItems(); + List parents = new(); + + var session = Context.Store.Session(); + + if (!pageParentId.IsNullOrEmpty()) + { + Pages_ByHierarchy.Result result = await session.Query() + .ProjectInto() + .Include(x => x.Id) + .Include(x => x.Path.Select(p => p.Id)) + .FirstOrDefaultAsync(x => x.Id == pageParentId); + + if (result != null) + { + List ids = result.Path.Select(x => x.Id).ToList(); + ids.Add(result.Id); + parents = (await session.LoadAsync(ids)).Select(x => x.Value).Reverse().ToList(); + } + } + + IPageTypeHandler handler = Handler.Get(); + + // if there is no registered handler we just allow all page types + if (handler == null) + { + return types.ToList(); + } + + return (await handler.GetAllowedPageTypes(Context.Application, types, parents))?.ToList() ?? new(); + } +} + + +public interface IPageTypeService +{ + /// + /// Get all available page types + /// + IList GetPageTypes(); + + /// + /// Get all page types which are allowed below a selected parent page + /// + Task> GetAllowedPageTypes(string pageParentId = null); + + /// + /// Get a specific page type by alias + /// + PageType GetPageType(string pageTypeAlias); +} diff --git a/zero.Core/Pages/PagesStore.cs b/zero.Core/Pages/PagesStore.cs new file mode 100644 index 00000000..bfbaaaa4 --- /dev/null +++ b/zero.Core/Pages/PagesStore.cs @@ -0,0 +1,52 @@ +namespace zero.Pages; + +public class PagesStore : TreeEntityStore, IPagesStore +{ + protected IPageTypeService PageTypes { get; private set; } + + + public PagesStore(IStoreContext context, IPageTypeService pageTypes) : base(context) + { + PageTypes = pageTypes; + } + + + /// + public override async Task IsAllowedAsChild(Page model, string parentId) + { + IList pageTypes = await PageTypes.GetAllowedPageTypes(parentId); + return pageTypes.Any(x => x.Alias == model.PageTypeAlias); + } + + + /// + public async Task Empty(string pageType, string parentId = null) + { + PageType type = PageTypes.GetPageType(pageType); + + if (type == null) + { + return null; + } + + Page model = await Empty(); + model.PageTypeAlias = type.Alias; + model.ParentId = parentId; + + if (!await IsAllowedAsChild(model, parentId)) + { + return null; // TODO we have no way to return an error here :-/ + } + + return model; + } +} + + +public interface IPagesStore : ITreeEntityStore +{ + /// + /// Get new instance of an entity for a specific page type + /// + Task Empty(string pageType, string parentId = null); +} \ No newline at end of file diff --git a/zero.Core/Pages/ServiceCollectionExtensions.cs b/zero.Core/Pages/ServiceCollectionExtensions.cs index 5fc3c3ec..ae0bd934 100644 --- a/zero.Core/Pages/ServiceCollectionExtensions.cs +++ b/zero.Core/Pages/ServiceCollectionExtensions.cs @@ -6,6 +6,9 @@ internal static class ServiceCollectionExtensions { public static IServiceCollection AddZeroPages(this IServiceCollection services) { + services.AddScoped(); + services.AddScoped(); + services.Configure(opts => { RavenOptions raven = opts.For(); diff --git a/zero.Core/Persistence/ZeroStore.cs b/zero.Core/Persistence/ZeroStore.cs index b76a3287..95881122 100644 --- a/zero.Core/Persistence/ZeroStore.cs +++ b/zero.Core/Persistence/ZeroStore.cs @@ -53,7 +53,7 @@ public class ZeroStore : IZeroStore /// public IZeroDocumentSession Session(bool global, string database = null, ZeroSessionResolution resolution = ZeroSessionResolution.Reuse, SessionOptions options = null) { - return Session(global ? Options.Raven.Database : database, resolution, options); + return Session(global ? Options.For().Database : database, resolution, options); } } diff --git a/zero.Core/Stores/CachableEntityStore.cs b/zero.Core/Stores/CachableEntityStore.cs deleted file mode 100644 index 7e1d3c50..00000000 --- a/zero.Core/Stores/CachableEntityStore.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace zero.Stores; - -public record CachableEntityStoreOptions(bool CacheIndividual, bool CacheAll); - - -public abstract class CachableEntityStore : EntityStore where T : ZeroIdEntity, new() -{ - protected CachableEntityStoreOptions CacheOptions { get; set; } - - protected IStoreCache Cache { get; set; } - - - public CachableEntityStore(IStoreContext collectionContext, IStoreCache collectionCache) : base(collectionContext) - { - CacheOptions = new(CacheIndividual: true, CacheAll: false); // TODO when props update we need to update Cache.For() - Cache = collectionCache.For(CacheOptions); - } - - - /// - public override async Task Load(string id, string changeVector = null) - { - if (changeVector.IsNullOrEmpty() && Cache.TryGetValue(id, out T model)) - { - return model; - } - - return await base.Load(id, changeVector); - } - - - /// - public override Task> Load(IEnumerable ids) - { - return base.Load(ids); - } -} \ No newline at end of file diff --git a/zero.Core/Stores/EntityStore.cs b/zero.Core/Stores/EntityStore.cs index 5af99033..d78e6264 100644 --- a/zero.Core/Stores/EntityStore.cs +++ b/zero.Core/Stores/EntityStore.cs @@ -10,26 +10,25 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, n public Guid Guid { get; private set; } = Guid.NewGuid(); /// - public IZeroDocumentSession Session => Context.Store.Session(); - - - protected record EntityCollectionOptions(bool IncludeInactive); + public IZeroDocumentSession Session => Context.Store.Session(Config.Database); protected IZeroContext Context { get; private set; } - protected EntityCollectionOptions Options { get; set; } + protected StoreConfig Config { get; private set; } = new(); protected IInterceptors Interceptors { get; private set; } protected IStoreOperations Operations { get; private set; } + protected IZeroOptions Options { get; private set; } + public EntityStore(IStoreContext collectionContext) { Operations = collectionContext.Operations; Context = collectionContext.Context; Interceptors = collectionContext.Interceptors; - Options = new(IncludeInactive: true); + Options = collectionContext.Options; } @@ -93,13 +92,23 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, n /// /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() /// - protected virtual T WhenActive(T model) => model != null && (Options.IncludeInactive || (model is ZeroEntity ? (model as ZeroEntity).IsActive : true)) ? model : default; + protected virtual T WhenActive(T model) => model != null && (Config.IncludeInactive || (model is ZeroEntity ? (model as ZeroEntity).IsActive : true)) ? model : default; } public interface IEntityStore where T : ZeroIdEntity, new() { + /// + /// Id for this store + /// + Guid Guid { get; } + + /// + /// Access the current document session + /// + IZeroDocumentSession Session { get; } + /// /// Get new instance of an entity /// diff --git a/zero.Core/Stores/ServiceCollectionExtensions.cs b/zero.Core/Stores/ServiceCollectionExtensions.cs index dbbafcbc..c0ddfe20 100644 --- a/zero.Core/Stores/ServiceCollectionExtensions.cs +++ b/zero.Core/Stores/ServiceCollectionExtensions.cs @@ -9,11 +9,6 @@ internal static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddSingleton(); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - return services; } } \ No newline at end of file diff --git a/zero.Core/Stores/StoreCache.cs b/zero.Core/Stores/StoreCache.cs index 8c17307b..9221d2a8 100644 --- a/zero.Core/Stores/StoreCache.cs +++ b/zero.Core/Stores/StoreCache.cs @@ -24,16 +24,16 @@ public class StoreCache : IStoreCache return false; } - public IStoreCache For(CachableEntityStoreOptions options) - { - return this; - } + //public IStoreCache For(CachableEntityStoreOptions options) + //{ + // return this; + //} } public interface IStoreCache { - IStoreCache For(CachableEntityStoreOptions options); + //IStoreCache For(CachableEntityStoreOptions options); bool TryGetValue(string id, out T model); } \ No newline at end of file diff --git a/zero.Core/Stores/StoreConfig.cs b/zero.Core/Stores/StoreConfig.cs new file mode 100644 index 00000000..dad6d999 --- /dev/null +++ b/zero.Core/Stores/StoreConfig.cs @@ -0,0 +1,14 @@ +namespace zero.Stores; + +public class StoreConfig +{ + /// + /// Whether to include entities with IsActive=false in queries. Defaults to false. + /// + public bool IncludeInactive { get; set; } = false; + + /// + /// Override the database which is resolved automatically by default (based on IZeroContext and IApplicationResolver). + /// + public string Database { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Stores/StoreContext.cs b/zero.Core/Stores/StoreContext.cs index 9b1bd523..af98a680 100644 --- a/zero.Core/Stores/StoreContext.cs +++ b/zero.Core/Stores/StoreContext.cs @@ -12,14 +12,17 @@ public class StoreContext : IStoreContext public IStoreOperations Operations { get; private set; } + public IStoreCache Cache { get; private set; } - public StoreContext(IZeroContext context, IInterceptors interceptors, IStoreOperations operations) + + public StoreContext(IZeroContext context, IInterceptors interceptors, IStoreOperations operations, IStoreCache cache) { Store = context.Store; Options = context.Options; Context = context; Interceptors = interceptors; Operations = operations; + Cache = cache; } } @@ -35,4 +38,6 @@ public interface IStoreContext IInterceptors Interceptors { get; } IStoreOperations Operations { get; } + + IStoreCache Cache { get; } } \ No newline at end of file diff --git a/zero.Core/Stores/StoreOperations.Tree.cs b/zero.Core/Stores/StoreOperations.Tree.cs new file mode 100644 index 00000000..5c19f369 --- /dev/null +++ b/zero.Core/Stores/StoreOperations.Tree.cs @@ -0,0 +1,197 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Indexes; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Session; + +namespace zero.Stores; + +public abstract partial class StoreOperations +{ + /// + public virtual Task IsAllowedAsChild(T model, string parentId) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + return Task.FromResult(true); + } + + + /// + public async Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + IRavenQueryable queryable = Session.Query().Where(x => x.ParentId == parentId).Statistics(out QueryStatistics statistics); + querySelector ??= x => x.OrderBy(x => x.Sort); + + List result = await querySelector(queryable).Paging(pageNumber, pageSize).ToListAsync(); + return new Paged(result, statistics.TotalResults, pageNumber, pageSize); + } + + + /// + public async Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) + where T : ZeroIdEntity, IZeroTreeEntity, new() + where TIndex : AbstractCommonApiForIndexes, new() + { + IRavenQueryable queryable = Session.Query().Where(x => x.ParentId == parentId).Statistics(out QueryStatistics statistics); + querySelector ??= x => x.OrderBy(x => x.Sort); + + List result = await querySelector(queryable).Paging(pageNumber, pageSize).ToListAsync(); + return new Paged(result, statistics.TotalResults, pageNumber, pageSize); + } + + + /// + public async Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + Dictionary items = await Load(sortedIds); + uint index = 0; + + // contains multiple parents, therefore fail + if (items.Select(x => x.Value?.ParentId).Distinct().Count() > 1) + { + return EntityResult>.Fail("@errors.treeentity.sortingmultipleparents"); + } + + foreach (var item in items) + { + item.Value.Sort = index; + index += 10; + await Update(item.Value); + } + + return EntityResult>.Success(items.Select(x => x.Value).OrderByDescending(x => x.Sort)); + } + + + /// + public async Task> Move(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + T model = await Load(id); + T parent = await Load(newParentId); + + if (model == null || (!newParentId.IsNullOrEmpty() && parent == null)) + { + return EntityResult.Fail("@errors.idnotfound"); + } + + if (isParentAllowed != null && !await isParentAllowed(model, newParentId)) + { + return EntityResult.Fail("@errors.treeentity.parentnotallowed"); + } + + model.ParentId = parent?.Id; + + return await Update(model); + } + + + /// + public Task> Copy(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new() => Copy(id, newParentId, false, isParentAllowed); + + + /// + public Task> CopyWithDescendants(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new() => Copy(id, newParentId, true, isParentAllowed); + + + /// + /// Copies an entity (with optional descendants) to a new location + /// + protected async Task> Copy(string id, string newParentId, bool includeDescendants, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + T model = await Load(id); + T parent = await Load(newParentId); + + if (model == null || (!newParentId.IsNullOrEmpty() && parent == null)) + { + return EntityResult.Fail("@errors.idnotfound"); + } + + string baseId = model.Id; + + // update new page properties + model.Id = null; + model.ParentId = parent?.Id; + + if (model is ZeroEntity zeroEntity) + { + zeroEntity.IsActive = false; + zeroEntity.CreatedDate = DateTime.Now; + } + + // check if new parent is allowed + if (isParentAllowed != null && !await isParentAllowed(model, newParentId)) + { + return EntityResult.Fail("@errors.treeentity.parentnotallowed"); + } + + // recursive function to store descendants + async Task AddChildren(string oldParentId, string newParentId) + { + List children = await Session.Query().Where(x => x.ParentId == oldParentId).ToListAsync(); + + foreach (T child in children) + { + T clonedChild = ObjectCopycat.Clone(child); + clonedChild.Id = null; + clonedChild.ParentId = newParentId; + if (clonedChild is ZeroEntity zeroEntity) + { + zeroEntity.IsActive = false; + zeroEntity.CreatedDate = DateTime.Now; + } + + await Create(clonedChild); + await AddChildren(child.Id, clonedChild.Id); + } + } + + if (includeDescendants) + { + await AddChildren(baseId, model.Id); + } + + return EntityResult.Success(model); + } + + + /// + public async Task> DeleteWithDescendants(T model) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + List pages = await GetDescendantsAndSelf(model); + + if (pages.Count < 1) + { + return EntityResult.Fail("@errors.ondelete.idnotfound"); + } + + await Delete(pages.ToArray()); + return EntityResult.Success(pages.Select(x => x.Id).ToArray()); + } + + + /// + public async Task> DeleteWithDescendants(string id) where T : ZeroIdEntity, IZeroTreeEntity, new() => await DeleteWithDescendants(await Load(id)); + + + /// + /// Get an entity with all its descendants + /// + async Task> GetDescendantsAndSelf(T model) where T : ZeroIdEntity, IZeroTreeEntity, new() + { + List items = new() { model }; + + async Task AddChildren(T parent) + { + string parentId = parent.Id; + List children = await Session.Query().Where(x => x.ParentId == parentId).ToListAsync(); + items.AddRange(children); + + foreach (T child in children) + { + await AddChildren(child); + } + } + + await AddChildren(model); + + return items; + } +} \ No newline at end of file diff --git a/zero.Core/Stores/StoreOperations.cs b/zero.Core/Stores/StoreOperations.cs index 7f84d689..61bad221 100644 --- a/zero.Core/Stores/StoreOperations.cs +++ b/zero.Core/Stores/StoreOperations.cs @@ -202,4 +202,44 @@ public interface IStoreOperations /// Deletes entities by Id /// Task Delete(IEnumerable ids) where T : ZeroIdEntity, new(); + + /// + /// Loads all children for an entity + /// + Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, IZeroTreeEntity, new(); + + /// + /// Get descendants by query (by using the specified index) + /// + Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, IZeroTreeEntity, new() where TIndex : AbstractCommonApiForIndexes, new(); + + /// + /// Update sorting of entities on a specific level + /// + Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, IZeroTreeEntity, new(); + + /// + /// Move an entity to a new parent + /// + Task> Move(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new(); + + /// + /// Copies an entity to a new location + /// + Task> Copy(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new(); + + /// + /// Copies an entity with descendants to a new location + /// + Task> CopyWithDescendants(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new(); + + /// + /// Deletes an entity with all descendents + /// + Task> DeleteWithDescendants(T model) where T : ZeroIdEntity, IZeroTreeEntity, new(); + + /// + /// Deletes an entity by Id with all descendents + /// + Task> DeleteWithDescendants(string id) where T : ZeroIdEntity, IZeroTreeEntity, new(); } \ No newline at end of file diff --git a/zero.Core/Stores/TreeEntityStore.cs b/zero.Core/Stores/TreeEntityStore.cs new file mode 100644 index 00000000..e49d90cf --- /dev/null +++ b/zero.Core/Stores/TreeEntityStore.cs @@ -0,0 +1,89 @@ +using Raven.Client.Documents.Indexes; +using Raven.Client.Documents.Linq; + +namespace zero.Stores; + +public abstract class TreeEntityStore : EntityStore, ITreeEntityStore where T : ZeroIdEntity, IZeroTreeEntity, new() +{ + public TreeEntityStore(IStoreContext collectionContext) : base(collectionContext) { } + + /// + public virtual Task IsAllowedAsChild(T model, string parentId) => Task.FromResult(true); + + /// + public virtual Task> Copy(string id, string newParentId) => Operations.Copy(id, newParentId, async (model, parentId) => await IsAllowedAsChild(model, parentId)); + + /// + public virtual Task> CopyWithDescendants(string id, string newParentId) => Operations.Copy(id, newParentId, async (model, parentId) => await IsAllowedAsChild(model, parentId)); + + /// + public virtual Task> Move(string id, string newParentId) => Operations.Copy(id, newParentId, async (model, parentId) => await IsAllowedAsChild(model, parentId)); + + /// + public virtual Task>> Sort(string[] sortedIds) => Operations.Sort(sortedIds); + + /// + public virtual Task> DeleteWithDescendants(T model) => Operations.DeleteWithDescendants(model); + + /// + public virtual Task> DeleteWithDescendants(string id) => Operations.DeleteWithDescendants(id); + + /// + public virtual Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = null) + => Operations.LoadChildren(parentId, pageNumber, pageSize, querySelector); + + /// + public virtual Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = null) where TIndex : AbstractCommonApiForIndexes, new() + => Operations.LoadChildren(parentId, pageNumber, pageSize, querySelector); +} + + + +public interface ITreeEntityStore : IEntityStore where T : ZeroIdEntity, IZeroTreeEntity, new() +{ + /// + /// Determines whether a model is allowed as a child for a new parent. + /// This is primarily used by copy/move operations + /// + Task IsAllowedAsChild(T model, string parentId); + + /// + /// Loads all children for an entity + /// + Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default); + + /// + /// Get descendants by query (by using the specified index) + /// + Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where TIndex : AbstractCommonApiForIndexes, new(); + + /// + /// Update sorting of entities on a specific level + /// + Task>> Sort(string[] sortedIds); + + /// + /// Move an entity to a new parent + /// + Task> Move(string id, string newParentId); + + /// + /// Copies an entity to a new location + /// + Task> Copy(string id, string newParentId); + + /// + /// Copies an entity with descendants to a new location + /// + Task> CopyWithDescendants(string id, string newParentId); + + /// + /// Deletes an entity with all descendents + /// + Task> DeleteWithDescendants(T model); + + /// + /// Deletes an entity by Id with all descendents + /// + Task> DeleteWithDescendants(string id); +} \ No newline at end of file diff --git a/zero.Core/Unimplemented/PreviewApi.cs b/zero.Core/Unimplemented/PreviewApi.cs new file mode 100644 index 00000000..7363049e --- /dev/null +++ b/zero.Core/Unimplemented/PreviewApi.cs @@ -0,0 +1,75 @@ +//using System; +//using System.Threading.Tasks; +//using zero.Core.Collections; +//using zero.Core.Entities; +//using zero.Core.Extensions; +//using Rc = Raven.Client; + +//namespace zero.Core.Api +//{ +// public class PreviewApi : BackofficeApi, IPreviewApi +// { +// Preview Blueprint; + +// public PreviewApi(IStoreContext store, Preview blueprint) : base(store) +// { +// Blueprint = blueprint; +// } + + +// /// +// public async Task> Add(TEntity model) where TEntity : ZeroEntity +// { +// return await Update(null, model); +// } + + +// /// +// public async Task> Update(string id, TEntity model) where TEntity : ZeroEntity +// { +// Preview entity = id == null ? await GetById(id) : Blueprint.Clone(); +// entity.Content = model; +// entity.OriginalId = model.Id; +// entity.Name = model.Name; + +// return await SaveModel(entity, meta: x => +// { +// x[Rc.Constants.Documents.Metadata.Expires] = GetExpiry(model); +// }); +// } + + +// /// +// public async Task GetById(string id) +// { +// return await GetById(id); +// } + + +// /// +// /// Get preview expiration for a document +// /// +// DateTime GetExpiry(ZeroEntity model) +// { +// return DateTime.Now.AddHours(1); +// } +// } + +// public interface IPreviewApi +// { +// /// +// /// Adds an entity to the preview collection. This will generate a preview with an id which can be used for the preview view. +// /// +// Task> Add(TEntity model) where TEntity : ZeroEntity; + +// /// +// /// Updates an entity in the preview collection. This will generate a preview with an id which can be used for the preview view. +// /// +// Task> Update(string id, TEntity model) where TEntity : ZeroEntity; + +// /// +// /// Get preview entity by Id +// /// +// Task GetById(string id); +// } +//} diff --git a/zero.Core/Unimplemented/RecycleBinApi.cs b/zero.Core/Unimplemented/RecycleBinApi.cs new file mode 100644 index 00000000..11afbded --- /dev/null +++ b/zero.Core/Unimplemented/RecycleBinApi.cs @@ -0,0 +1,258 @@ +//using Raven.Client.Documents; +//using Raven.Client.Documents.Linq; +//using Raven.Client.Documents.Session; +//using System.Collections.Generic; +//using System.Threading.Tasks; +//using zero.Core.Collections; +//using zero.Core.Database.Indexes; +//using zero.Core.Entities; +//using zero.Core.Extensions; +//using zero.Core.Utils; + +//namespace zero.Core.Api +//{ +// public class RecycleBinApi : BackofficeApi, IRecycleBinApi +// { +// RecycledEntity Blueprint; + +// public RecycleBinApi(IStoreContext store, RecycledEntity blueprint) : base(store) +// { +// Blueprint = blueprint; +// } + + +// /// +// public async Task> Add(TEntity model, string group = null, string operationId = null) where TEntity : ZeroEntity +// { +// RecycledEntity entity = Blueprint.Clone(); +// entity.Group = group; +// entity.Content = model; +// entity.OriginalId = model.Id; +// entity.OperationId = operationId; +// entity.Name = model.Name; + +// return await SaveModel(entity); +// } + + +// /// +// public async Task>> Add(IEnumerable models, string group = null) where TEntity : ZeroEntity +// { +// IList results = new List(); +// string operationId = IdGenerator.Create(); + +// foreach (TEntity model in models) +// { +// EntityResult result = await Add(model, group, operationId); + +// if (!result.IsSuccess) +// { +// return EntityResult>.Fail(result.Errors); +// } + +// results.Add(result.Model); +// } + +// return EntityResult>.Success(results); +// } + + +// /// +// public async Task GetById(string id) +// { +// return await GetById(id); +// } + + +// /// +// public async Task> GetByQuery(RecycleBinListQuery query) +// { +// query.SearchSelector = x => x.Name; + +// return await Session.Query() +// .WhereIf(x => x.Group == query.Group, !query.Group.IsNullOrWhiteSpace()) +// .WhereIf(x => x.OperationId == query.OperationId, !query.OperationId.IsNullOrWhiteSpace()) +// .ToQueriedListAsync(query); +// } + + +// /// +// public async Task> GetByOperation(string operationId) +// { +// return await Session.Query() +// .Where(x => x.OperationId == operationId) +// .ToListAsync(); +// } + + +// /// +// /// Get affected entity count from a specific operation +// /// +// public async Task GetCountByOperation(string operationId) +// { +// return await Session.Query() +// .Where(x => x.OperationId == operationId) +// .CountAsync(); +// } + + +// /// +// public async Task> Delete(string id) +// { +// return await DeleteById(id); +// } + + +// /// +// public async Task> DeleteAll() +// { +// // TODO make Purge operations app-aware +// return await Purge(); +// } + + +// /// +// public async Task> DeleteByOperation(string operationId) +// { +// return await Purge("where c.OperationId = $id", new Raven.Client.Parameters() { { "id", operationId } }); +// } + + +// /// +// public async Task> DeleteByGroup(string group) +// { +// return await Purge("where c.Group = $group", new Raven.Client.Parameters() { { "group", group } }); +// } +// } + +// public interface IRecycleBinApi +// { +// /// +// /// Adds an entity to the recycle bin. This operation will not remove this entity from it's own collection! +// /// +// Task> Add(TEntity model, string group = null, string operationId = null) where TEntity : ZeroEntity; + +// /// +// /// Adds the specified entities to the recycle bin. This operation will not remove entities from their own collection! +// /// +// Task>> Add(IEnumerable models, string group = null) where TEntity : ZeroEntity; + +// /// +// /// Get recycled entity by Id +// /// +// Task GetById(string id); + +// /// +// /// Get all recycled items +// /// +// Task> GetByQuery(RecycleBinListQuery query); + +// /// +// /// Get affected entities from a specific operation +// /// +// Task> GetByOperation(string operationId); + +// /// +// /// Get affected entity count from a specific operation +// /// +// Task GetCountByOperation(string operationId); + +// // +// /// Deletes a recycled entity by Id +// /// +// Task> Delete(string id); + +// /// +// /// Purges the recycle bin +// /// +// Task> DeleteAll(); + +// /// +// /// Deletes all recycled items from a specific operation +// /// +// Task> DeleteByOperation(string operationId); + +// /// +// /// Deletes all recycled items from a specific group +// /// +// Task> DeleteByGroup(string group); +// } +//} + + +/* + * RESTORE FOR PAGES + * + * + * + +/// + /// Restores a page from the recycle bin + /// + Task> Restore(string id, bool includeDescendants = false); + + + /// + public async Task> Restore(string id, bool includeDescendants = false) + { + EntityResult result = new EntityResult(); + RecycledEntity 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 Page).Where(x => x != null).Select(x => x.ParentId).ToArray(); + parentIds = (await Load(parentIds)).Where(x => x.Value != null).Select(x => x.Value.Id).ToArray(); + + // validate and restore all items + foreach (RecycledEntity entity in entities) + { + // check if it contains page data + if (entity.Group != RECYCLE_BIN_GROUP || !(entity.Content is Page)) + { + //result.AddError("Cannot parse recycled entity as an Page in group \"" + RECYCLE_BIN_GROUP + "\""); // TODO correct error message + continue; + } + + // get page + Page page = entity.Content as Page; + 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 Create(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; + } + + */ \ No newline at end of file diff --git a/zero.Core/Unimplemented/RecycleBinListQuery.cs b/zero.Core/Unimplemented/RecycleBinListQuery.cs new file mode 100644 index 00000000..c1ee3f7d --- /dev/null +++ b/zero.Core/Unimplemented/RecycleBinListQuery.cs @@ -0,0 +1,9 @@ +//namespace zero.Core.Entities +//{ +// public class RecycleBinListQuery : ListQuery +// { +// public string Group { get; set; } + +// public string OperationId { get; set; } +// } +//} diff --git a/zero.Core/Unimplemented/RecycledEntity.cs b/zero.Core/Unimplemented/RecycledEntity.cs new file mode 100644 index 00000000..a51befcc --- /dev/null +++ b/zero.Core/Unimplemented/RecycledEntity.cs @@ -0,0 +1,28 @@ +//using zero.Core.Attributes; + +//namespace zero.Core.Entities +//{ +// [Collection("RecycleBin")] +// public class RecycledEntity : ZeroEntity +// { +// /// +// /// Id of the recycled entity +// /// +// public string OriginalId { get; set; } + +// /// +// /// Group recycled entities together which have been recycled in a single operation +// /// +// public string OperationId { get; set; } + +// /// +// /// Contains the entity content +// /// +// public ZeroEntity Content { get; set; } + +// /// +// /// Recycled entities can be grouped together (e.g. pages, media, ...) +// /// +// public string Group { get; set; } +// } +//} diff --git a/zero.Core/Unimplemented/RevisionsApi.cs b/zero.Core/Unimplemented/RevisionsApi.cs new file mode 100644 index 00000000..bc50f876 --- /dev/null +++ b/zero.Core/Unimplemented/RevisionsApi.cs @@ -0,0 +1,102 @@ +//using Raven.Client.Documents.Session; +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading.Tasks; +//using zero.Core.Entities; +//using zero.Core.Extensions; + +//namespace zero.Core.Api +//{ +// public class RevisionsApi : IRevisionsApi +// { +// protected IAsyncDocumentSession Session { get; set; } + +// public RevisionsApi(IAsyncDocumentSession session) +// { +// Session = session; +// } + + +// /// +// /// Get revision list for an entity +// /// +// public async Task>> GetPagedWithModel(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity +// { +// // get paged revisions +// List items = await Session.Advanced.Revisions.GetForAsync(id, (pageNumber - 1) * pageSize, pageSize); +// long totalResults = await Session.Advanced.Revisions.GetCountForAsync(id); + +// // add creation for last page +// //if (!items.Any() && pageNumber >= (int)Math.Ceiling((decimal)totalResults / pageSize)) +// //{ +// // T currentRevision = await Session.LoadAsync(id); +// // if (currentRevision != null) +// // { +// // items.Add(currentRevision); +// // } +// //} + +// List> revisions = new(); + +// // load affected users as the revisions could have been edited by other users too +// string[] userIds = items.Select(x => x.LastModifiedById).Distinct().ToArray(); +// Dictionary users = await Session.LoadAsync(userIds); + +// // create revision objects +// foreach (T item in items) +// { +// Revision revision = new() +// { +// Model = item, +// ModelId = item.Id, +// ChangeVector = Session.Advanced.GetChangeVectorFor(item), +// Date = item.LastModifiedDate +// }; + +// if (!item.LastModifiedById.IsNullOrEmpty() && users.TryGetValue(item.LastModifiedById, out ZeroUser user) && user != null) +// { +// revision.User = new RevisionUser() +// { +// AvatarId = user.AvatarId, +// Id = user.Id, +// Name = user.Name +// }; +// } + +// revisions.Add(revision); +// } + +// return new Paged>(revisions, totalResults, pageNumber, pageSize); +// } + + +// /// +// /// Get revision list for an entity +// /// +// public async Task> GetPaged(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity +// { +// Paged> items = await GetPagedWithModel(id, pageNumber, pageSize); +// return items.MapTo(model => new Revision() +// { +// ChangeVector = model.ChangeVector, +// Date = model.Date, +// ModelId = model.ModelId, +// User = model.User +// }); +// } +// } + + +// public interface IRevisionsApi +// { +// /// +// /// Get revision list including models for an entity +// /// +// Task>> GetPagedWithModel(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity; + +// /// +// /// Get revision list for an entity +// /// +// Task> GetPaged(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity; +// } +//} diff --git a/zero.Core/Unimplemented/SetupApi.cs b/zero.Core/Unimplemented/SetupApi.cs new file mode 100644 index 00000000..0eefe3d1 --- /dev/null +++ b/zero.Core/Unimplemented/SetupApi.cs @@ -0,0 +1,339 @@ +//using FluentValidation.Results; +//using Microsoft.AspNetCore.Identity; +//using Newtonsoft.Json; +//using Raven.Client.Documents; +//using Raven.Client.Documents.Session; +//using System.IO; +//using System.Security.Cryptography; +//using zero.Setup; + +//namespace zero.Core.Api +//{ +// public class SetupApi : ISetupApi +// { +// protected IZeroOptions Options { get; private set; } + +// protected IPasswordHasher PasswordHasher { get; private set; } + +// protected IZeroDocumentConventionsBuilder ConventionsBuilder { get; private set; } + + +// public SetupApi(IZeroOptions options, IPasswordHasher passwordHasher, IZeroDocumentConventionsBuilder conventionsBuilder) +// { +// Options = options; +// PasswordHasher = passwordHasher; +// ConventionsBuilder = conventionsBuilder; +// } + + +// public async Task> Install(SetupModel model) +// { +// ValidationResult validation = await new SetupModelValidator().ValidateAsync(model); + +// if (!validation.IsValid) +// { +// return EntityResult.Fail(validation); +// } + +// DocumentStore raven = null; + +// try +// { +// // test read & write permissions on folders +// // TODO + +// // create temporary instance of database +// raven = new DocumentStore() +// { +// Urls = model.Database.Url.Split(','), +// Database = model.Database.Name +// }; + +// ConventionsBuilder.Run(raven.Conventions); +// raven.Initialize(); + +// // create application +// Application app = Prepare(new Application() +// { +// Id = "app." + Safenames.Alias(model.AppName), +// CreatedDate = DateTimeOffset.Now, +// IsActive = true, +// Name = model.AppName, +// Alias = Safenames.Alias(model.AppName), +// Database = model.Database.Name +// }); + +// // create user +// ZeroUser user = Prepare(new ZeroUser() +// { +// IsSuper = true, +// CreatedDate = DateTimeOffset.Now, +// Email = model.User.Email, +// Username = model.User.Email, +// Name = model.User.Name, +// IsActive = true, +// LanguageId = Options.DefaultLanguage, +// Alias = Safenames.Alias(model.User.Name), +// IsEmailConfirmed = true, +// SecurityStamp = NewSecurityStamp() +// }); + +// user.PasswordHash = PasswordHasher.HashPassword(user, model.User.Password); + +// // create default language +// Language language = Prepare(new Language() // TODO get default language selection from setup UI +// { +// Name = "English", +// Alias = Safenames.Alias("English"), +// CreatedDate = DateTimeOffset.Now, +// Code = "en-US", +// IsActive = true, +// IsDefault = true +// }); + +// using IAsyncDocumentSession session = raven.OpenAsyncSession(); + +// await session.StoreAsync(user); + +// // user creation failed +// //if (!result.Succeeded) +// //{ +// // EntityResult entityResult = EntityResult.Fail(); + +// // foreach (IdentityError error in result.Errors) +// // { +// // entityResult.AddError(error.Code, error.Description); +// // } + +// // return entityResult; +// //} + + +// await session.StoreAsync(app); + +// // save default user roles +// IList roles = GetRoles(model); + +// foreach (ZeroUserRole role in roles) +// { +// await session.StoreAsync(role); +// } + +// // add admin role to super user +// // set app-id for user and store it +// user.AppId = session.Advanced.GetDocumentId(app); +// user.CurrentAppId = user.AppId; +// user.RoleIds.Add(roles.First(role => role.Name == "Standard").Id); +// user.RoleIds.Add(roles.First(role => role.Name == "Administrator").Id); +// await session.StoreAsync(user); + +// // create language +// await session.StoreAsync(language); + +// // set countries +// using (Raven.Client.Documents.BulkInsert.BulkInsertOperation bulkInsert = raven.BulkInsert()) +// { +// foreach (Country country in GetCountries(model, language)) +// { +// await bulkInsert.StoreAsync(country); +// } +// } + +// // update settings file. if this fails the changes won't be stored +// UpdateSettingsFile(model); + +// await session.SaveChangesAsync(); +// } +// catch (Exception) +// { +// // TODO revert +// throw; +// } +// finally +// { +// raven?.Dispose(); +// } + +// return EntityResult.Success(model); +// } + + +// /// +// /// Updates the settings file with the new data (database connection and version) +// /// +// void UpdateSettingsFile(SetupModel model) +// { +// // TODO should we write this into appSettings.json now? +// // or let the user do it in the code editor? + +// //var filePath = Path.Combine(model.ContentRootPath, "zeroSettings.json"); +// //string json = File.ReadAllText(filePath); + +// //ZeroConfiguration config = JsonConvert.DeserializeObject(json); + +// //config.Raven = new RavenOptions() +// //{ +// // Database = model.Database.Name, +// // Url = model.Database.Url +// //}; + +// //config.ZeroVersion = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(); + +// //json = JsonConvert.SerializeObject(config, Formatting.Indented); + +// //File.WriteAllText(filePath, json); +// } + + +// /// +// /// Get countries for supported backoffice languages +// /// +// IList GetCountries(SetupModel model, Language defaultLanguage) +// { +// List countries = new List(); + +// //string[] isoCodes = Config.SupportedLanguages; +// string[] isoCodes = new string[1] { "en-US" }; + +// foreach (string languageISO in isoCodes) +// { +// // country list from: https://github.com/umpirsky/country-list/tree/master/data +// var filePath = Path.Combine(model.ContentRootPath, "Resources", "Countries", "countries." + languageISO.ToLowerInvariant() + ".json"); +// string json = File.ReadAllText(filePath); + + +// countries.AddRange(JsonConvert.DeserializeObject>(json).Select(country => Prepare(new Country() +// { +// CreatedDate = DateTimeOffset.Now, +// IsActive = true, +// //AppId = Constants.Database.SharedAppId, // TODO appx fix +// Alias = Safenames.Alias(country.Value), +// //LanguageId = defaultLanguage.Id, +// Code = country.Key.ToLowerInvariant(), +// Name = country.Value +// })).ToList()); +// } + +// return countries; +// } + + +// /// +// /// Create default roles +// /// +// IList GetRoles(SetupModel model) +// { +// string type = Constants.Auth.Claims.Permission; + +// ZeroUserRole adminRole = Prepare(new ZeroUserRole() +// { +// Name = "Administrator", +// Alias = Safenames.Alias("Administrator"), +// Sort = 0, +// Icon = "fth-award", +// CreatedDate = DateTimeOffset.Now, +// IsActive = true, +// Claims = new List() +// { +// //new UserClaim(type, Permissions.Applications, PermissionsValue.Write), +// new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Spaces, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Pages, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Media, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Settings, PermissionsValue.True), +// new UserClaim(type, Permissions.Settings.Applications, PermissionsValue.None), +// new UserClaim(type, Permissions.Settings.Countries, PermissionsValue.Update), +// new UserClaim(type, Permissions.Settings.Logging, PermissionsValue.Update), +// new UserClaim(type, Permissions.Settings.Translations, PermissionsValue.Update), +// new UserClaim(type, Permissions.Settings.Updates, PermissionsValue.Update), +// new UserClaim(type, Permissions.Settings.Users, PermissionsValue.Update), +// }, +// }); + +// ZeroUserRole editorRole = Prepare(new ZeroUserRole() +// { +// Name = "Editor", +// Alias = Safenames.Alias("Editor"), +// Sort = 1, +// Icon = "fth-feather", +// CreatedDate = DateTimeOffset.Now, +// IsActive = true, +// Claims = new List() +// { +// new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Spaces, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Pages, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Media, PermissionsValue.True), +// new UserClaim(type, Permissions.Sections.Settings, PermissionsValue.True), +// new UserClaim(type, Permissions.Settings.Translations, PermissionsValue.True) +// } +// }); + +// ZeroUserRole defaultRole = Prepare(new ZeroUserRole() +// { +// Name = "Standard", +// Alias = Safenames.Alias("Standard"), +// Sort = 2, +// Icon = "fth-users", +// CreatedDate = DateTimeOffset.Now, +// IsActive = true, +// Claims = new List() +// { +// new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True) +// } +// }); + +// return new List() { adminRole, editorRole, defaultRole }; +// } + + +// T Prepare(T model, string languageId = null) where T : ZeroIdEntity +// { +// ZeroEntity zeroEntity = model as ZeroEntity; + +// // set default properties +// if (zeroEntity != null && zeroEntity.CreatedDate == default) +// { +// zeroEntity.CreatedDate = DateTimeOffset.Now; +// } +// if (zeroEntity != null && zeroEntity.CreatedById == default) +// { +// zeroEntity.CreatedById = Constants.Auth.SystemUser; +// } +// if (zeroEntity != null && zeroEntity.LanguageId == default) +// { +// zeroEntity.LanguageId = languageId; +// } + +// // update name alias and last modified +// if (zeroEntity != null) +// { +// zeroEntity.Alias = Safenames.Alias(zeroEntity.Name); +// zeroEntity.LastModifiedById = Constants.Auth.SystemUser; +// zeroEntity.LastModifiedDate = DateTimeOffset.Now; +// zeroEntity.Hash ??= IdGenerator.Create(); +// zeroEntity.IsActive = true; +// } + +// return model; +// } + + +// /// +// /// Creates a new security stamp +// /// +// string NewSecurityStamp() +// { +// byte[] bytes = new byte[20]; +// RandomNumberGenerator.Fill(bytes); +// return Base32.ToBase32(bytes); +// } +// } + + + +// public interface ISetupApi +// { +// Task> Install(SetupModel model); +// } +//} diff --git a/zero.Core/Utils/ObjectCopycat.cs b/zero.Core/Utils/ObjectCopycat.cs index a29274d1..a3b6b15a 100644 --- a/zero.Core/Utils/ObjectCopycat.cs +++ b/zero.Core/Utils/ObjectCopycat.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using Newtonsoft.Json; +using System.Collections.Concurrent; using System.Reflection; namespace zero.Utils; @@ -12,6 +13,12 @@ public class ObjectCopycat static ConcurrentDictionary> PublicPropertiesPerType { get; set; } = new(); + public static T Clone(T obj) + { + Type type = obj.GetType(); + return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj), type); + } + public static T CopyProperties(T source, T target, params string[] exceptions) { diff --git a/zero.Core/_legacy/Api/ApplicationsApi.cs b/zero.Core/_legacy/Api/ApplicationsApi.cs deleted file mode 100644 index 5608a0a1..00000000 --- a/zero.Core/_legacy/Api/ApplicationsApi.cs +++ /dev/null @@ -1,91 +0,0 @@ -using FluentValidation; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Api -{ - public class ApplicationsApi : BackofficeApi, IApplicationsApi - { - IValidator Validator; - - - public ApplicationsApi(IStoreContext store, IValidator validator) : base(store, isCoreDatabase: true) - { - Validator = validator; - } - - - /// - public async Task GetById(string id) - { - return await GetById(id); - } - - - /// - public async Task> GetAll() - { - return await Session - .Query() - .OrderByDescending(x => x.CreatedDate) - .ToListAsync(); - } - - - /// - public async Task> GetByQuery(ListQuery query) - { - query.SearchFor(entity => entity.Name); - - return await Session.Query().ToQueriedListAsync(query); - } - - - /// - public async Task> Save(Application model) - { - return await SaveModel(model, Validator); - } - - - /// - public async Task> Delete(string id) - { - return await DeleteById(id); - } - } - - - public interface IApplicationsApi - { - /// - /// Get application by Id - /// - Task GetById(string id); - - /// - /// Get all available zero applications - /// - Task> GetAll(); - - /// - /// Get all available applications (with query) - /// - Task> GetByQuery(ListQuery query); - - /// - /// Creates or updates a application - /// - Task> Save(Application model); - - /// - /// Deletes a application by Id - /// - Task> Delete(string id); - } -} diff --git a/zero.Core/_legacy/Api/BackofficeApi.cs b/zero.Core/_legacy/Api/BackofficeApi.cs deleted file mode 100644 index 592dcecd..00000000 --- a/zero.Core/_legacy/Api/BackofficeApi.cs +++ /dev/null @@ -1,242 +0,0 @@ -using FluentValidation; -using FluentValidation.Results; -using Raven.Client; -using Raven.Client.Documents.Session; -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using zero.Core.Attributes; -using zero.Core.Collections; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Utils; - -namespace zero.Core.Api -{ - public abstract class BackofficeApi : IBackofficeApi - { - protected IZeroStore Store { get; private set; } - - const string NEW_ID = "new:"; - - protected IStoreContext Context { get; private set; } - - protected bool IsCoreDatabase { get; private set; } - - - public BackofficeApi(IStoreContext context) - { - Context = context; - Store = context.Store; - IsCoreDatabase = false; - } - - internal BackofficeApi(IStoreContext context, bool isCoreDatabase) : this(context) - { - IsCoreDatabase = isCoreDatabase; - } - - - protected IZeroDocumentSession Session => Store.Session(IsCoreDatabase); - - - /// - public async Task GetById(string id) where T : ZeroIdEntity - { - if (id.IsNullOrWhiteSpace()) - { - return default; - } - - return await Session.LoadAsync(id); - } - - - /// - public async Task> GetByIds(params string[] ids) where T : ZeroIdEntity - { - 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 async Task> SaveModel(T model, IValidator validator = null, Action meta = null) where T : ZeroEntity - { - // check for alias - //if (model is IUrlAliasEntity) - //{ - // IUrlAliasEntity entity = operation.Model as IUrlAliasEntity; - // entity.Alias = entity.Alias?.ToLower().ToUrlSegment(); - //} - - // run validator - if (validator != null) - { - ValidationResult validation = await validator.ValidateAsync(model); - - if (!validation.IsValid) - { - return EntityResult.Fail(validation); - } - } - - // find all media items in model - //List> media = ObjectTraverser.Find(model); - - // upload media items - //Dictionary mediaItems = new Dictionary(); - - //foreach (ObjectTraverser.Result item in media) - //{ - // string id = item.Item?.Id; - - // if (!Media.Upload(item.Item, out bool uploaded, out string uploadError)) - // { - // return EntityResult.Fail(item.Path, uploadError); - // } - // else - // { - // mediaItems.Add(id, item.Item); - // } - //} - - //if (operation.Media != null) - //{ - // operation.Media?.Invoke(operation.Model, mediaItems); - //} - - // 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) || id.StartsWith(NEW_ID)) - { - item.Property.SetValue(item.Parent, item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create()); - } - } - - // get current user - string userId = Context.Context.BackofficeUser.FindFirstValue(Constants.Auth.Claims.UserId); - - // set default properties - if (model.Id.IsNullOrEmpty()) - { - 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(); - - Session.Advanced.WaitForIndexesAfterSaveChanges(throwOnTimeout: false); - - await Session.StoreAsync(model); - - meta?.Invoke(Session.Advanced.GetMetadataFor(model)); - - await Session.SaveChangesAsync(); - - return EntityResult.Success(model); - } - - - - /// - public async Task> DeleteById(string id) where T : ZeroIdEntity - { - 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 async Task DeleteByIds(params string[] ids) where T : ZeroIdEntity - { - int successCount = 0; - - foreach (string id in ids) - { - EntityResult result = await DeleteById(id); - successCount += result.IsSuccess ? 1 : 0; - } - - return successCount; - } - - - /// - public async Task> Purge(string querySuffix = null, Parameters parameters = null) - { - await Store.Raven.PurgeAsync(Session.Advanced.DocumentStore.Database, querySuffix, parameters); - return EntityResult.Success(); - } - } - - - public interface IBackofficeApi - { - /// - /// Get an entity by Id. - /// If the requested entity is an IAppAwareEntity it will only return entities for the currently selected app + shared app - /// - Task GetById(string id) where T : ZeroIdEntity; - - /// - /// Get entities by ids. - /// If the requested entity is an IAppAwareEntity it will only return entities for the currently selected app + shared app - /// - Task> GetByIds(params string[] ids) where T : ZeroIdEntity; - - /// - /// Updates or creates an entity with an optional validator - /// - Task> SaveModel(T model, IValidator validator = null, Action meta = null) where T : ZeroEntity; - - /// - /// Deletes an entity by Id - /// - Task> DeleteById(string id) where T : ZeroIdEntity; - - /// - /// Deletes entities by Id - /// - Task DeleteByIds(params string[] ids) where T : ZeroIdEntity; - - /// - /// 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/_legacy/Api/BackofficeStore.cs b/zero.Core/_legacy/Api/BackofficeStore.cs deleted file mode 100644 index b5d6ae9d..00000000 --- a/zero.Core/_legacy/Api/BackofficeStore.cs +++ /dev/null @@ -1,43 +0,0 @@ -using zero.Core.Database; -using zero.Core.Messages; -using zero.Core.Options; - -namespace zero.Core.Api -{ - public class BackofficeStore : IBackofficeStore - { - public IZeroStore Store { get; private set; } - - public IZeroContext Context { get; private set; } - - public IZeroOptions Options { get; private set; } - - public IAuthenticationService Auth { get; private set; } - - public IMessageAggregator Messages { get; private set; } - - - public BackofficeStore(IZeroStore store, IZeroContext context, IZeroOptions options, IAuthenticationService authenticationApi, IMessageAggregator messages) - { - Store = store; - Context = context; - Options = options; - Auth = authenticationApi; - Messages = messages; - } - } - - - public interface IBackofficeStore - { - IZeroStore Store { get; } - - IZeroContext Context { get; } - - IZeroOptions Options { get; } - - IAuthenticationService Auth { get; } - - IMessageAggregator Messages { get; } - } -} \ No newline at end of file diff --git a/zero.Core/_legacy/Api/PreviewApi.cs b/zero.Core/_legacy/Api/PreviewApi.cs deleted file mode 100644 index 173eedb2..00000000 --- a/zero.Core/_legacy/Api/PreviewApi.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Entities; -using zero.Core.Extensions; -using Rc = Raven.Client; - -namespace zero.Core.Api -{ - public class PreviewApi : BackofficeApi, IPreviewApi - { - Preview Blueprint; - - public PreviewApi(IStoreContext store, Preview blueprint) : base(store) - { - Blueprint = blueprint; - } - - - /// - public async Task> Add(TEntity model) where TEntity : ZeroEntity - { - return await Update(null, model); - } - - - /// - public async Task> Update(string id, TEntity model) where TEntity : ZeroEntity - { - Preview entity = id == null ? await GetById(id) : Blueprint.Clone(); - entity.Content = model; - entity.OriginalId = model.Id; - entity.Name = model.Name; - - return await SaveModel(entity, meta: x => - { - x[Rc.Constants.Documents.Metadata.Expires] = GetExpiry(model); - }); - } - - - /// - public async Task GetById(string id) - { - return await GetById(id); - } - - - /// - /// Get preview expiration for a document - /// - DateTime GetExpiry(ZeroEntity model) - { - return DateTime.Now.AddHours(1); - } - } - - public interface IPreviewApi - { - /// - /// Adds an entity to the preview collection. This will generate a preview with an id which can be used for the preview view. - /// - Task> Add(TEntity model) where TEntity : ZeroEntity; - - /// - /// Updates an entity in the preview collection. This will generate a preview with an id which can be used for the preview view. - /// - Task> Update(string id, TEntity model) where TEntity : ZeroEntity; - - /// - /// Get preview entity by Id - /// - Task GetById(string id); - } -} diff --git a/zero.Core/_legacy/Api/RecycleBinApi.cs b/zero.Core/_legacy/Api/RecycleBinApi.cs deleted file mode 100644 index 4e654b47..00000000 --- a/zero.Core/_legacy/Api/RecycleBinApi.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Raven.Client.Documents; -using Raven.Client.Documents.Linq; -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Collections; -using zero.Core.Database.Indexes; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Utils; - -namespace zero.Core.Api -{ - public class RecycleBinApi : BackofficeApi, IRecycleBinApi - { - RecycledEntity Blueprint; - - public RecycleBinApi(IStoreContext store, RecycledEntity blueprint) : base(store) - { - Blueprint = blueprint; - } - - - /// - public async Task> Add(TEntity model, string group = null, string operationId = null) where TEntity : ZeroEntity - { - RecycledEntity entity = Blueprint.Clone(); - entity.Group = group; - entity.Content = model; - entity.OriginalId = model.Id; - entity.OperationId = operationId; - entity.Name = model.Name; - - return await SaveModel(entity); - } - - - /// - public async Task>> Add(IEnumerable models, string group = null) where TEntity : ZeroEntity - { - IList results = new List(); - string operationId = IdGenerator.Create(); - - foreach (TEntity model in models) - { - EntityResult result = await Add(model, group, operationId); - - if (!result.IsSuccess) - { - return EntityResult>.Fail(result.Errors); - } - - results.Add(result.Model); - } - - return EntityResult>.Success(results); - } - - - /// - public async Task GetById(string id) - { - return await GetById(id); - } - - - /// - public async Task> GetByQuery(RecycleBinListQuery query) - { - query.SearchSelector = x => x.Name; - - return await Session.Query() - .WhereIf(x => x.Group == query.Group, !query.Group.IsNullOrWhiteSpace()) - .WhereIf(x => x.OperationId == query.OperationId, !query.OperationId.IsNullOrWhiteSpace()) - .ToQueriedListAsync(query); - } - - - /// - public async Task> GetByOperation(string operationId) - { - return await Session.Query() - .Where(x => x.OperationId == operationId) - .ToListAsync(); - } - - - /// - /// Get affected entity count from a specific operation - /// - public async Task GetCountByOperation(string operationId) - { - return await Session.Query() - .Where(x => x.OperationId == operationId) - .CountAsync(); - } - - - /// - public async Task> Delete(string id) - { - return await DeleteById(id); - } - - - /// - public async Task> DeleteAll() - { - // TODO make Purge operations app-aware - return await Purge(); - } - - - /// - public async Task> DeleteByOperation(string operationId) - { - return await Purge("where c.OperationId = $id", new Raven.Client.Parameters() { { "id", operationId } }); - } - - - /// - public async Task> DeleteByGroup(string group) - { - return await Purge("where c.Group = $group", new Raven.Client.Parameters() { { "group", group } }); - } - } - - public interface IRecycleBinApi - { - /// - /// Adds an entity to the recycle bin. This operation will not remove this entity from it's own collection! - /// - Task> Add(TEntity model, string group = null, string operationId = null) where TEntity : ZeroEntity; - - /// - /// Adds the specified entities to the recycle bin. This operation will not remove entities from their own collection! - /// - Task>> Add(IEnumerable models, string group = null) where TEntity : ZeroEntity; - - /// - /// Get recycled entity by Id - /// - Task GetById(string id); - - /// - /// Get all recycled items - /// - Task> GetByQuery(RecycleBinListQuery query); - - /// - /// Get affected entities from a specific operation - /// - Task> GetByOperation(string operationId); - - /// - /// Get affected entity count from a specific operation - /// - Task GetCountByOperation(string operationId); - - // - /// Deletes a recycled entity by Id - /// - Task> Delete(string id); - - /// - /// Purges the recycle bin - /// - Task> DeleteAll(); - - /// - /// Deletes all recycled items from a specific operation - /// - Task> DeleteByOperation(string operationId); - - /// - /// Deletes all recycled items from a specific group - /// - Task> DeleteByGroup(string group); - } -} diff --git a/zero.Core/_legacy/Api/RevisionsApi.cs b/zero.Core/_legacy/Api/RevisionsApi.cs deleted file mode 100644 index 08ea47c8..00000000 --- a/zero.Core/_legacy/Api/RevisionsApi.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using zero.Core.Entities; -using zero.Core.Extensions; - -namespace zero.Core.Api -{ - public class RevisionsApi : IRevisionsApi - { - protected IAsyncDocumentSession Session { get; set; } - - public RevisionsApi(IAsyncDocumentSession session) - { - Session = session; - } - - - /// - /// Get revision list for an entity - /// - public async Task>> GetPagedWithModel(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity - { - // get paged revisions - List items = await Session.Advanced.Revisions.GetForAsync(id, (pageNumber - 1) * pageSize, pageSize); - long totalResults = await Session.Advanced.Revisions.GetCountForAsync(id); - - // add creation for last page - //if (!items.Any() && pageNumber >= (int)Math.Ceiling((decimal)totalResults / pageSize)) - //{ - // T currentRevision = await Session.LoadAsync(id); - // if (currentRevision != null) - // { - // items.Add(currentRevision); - // } - //} - - List> revisions = new(); - - // load affected users as the revisions could have been edited by other users too - string[] userIds = items.Select(x => x.LastModifiedById).Distinct().ToArray(); - Dictionary users = await Session.LoadAsync(userIds); - - // create revision objects - foreach (T item in items) - { - Revision revision = new() - { - Model = item, - ModelId = item.Id, - ChangeVector = Session.Advanced.GetChangeVectorFor(item), - Date = item.LastModifiedDate - }; - - if (!item.LastModifiedById.IsNullOrEmpty() && users.TryGetValue(item.LastModifiedById, out ZeroUser user) && user != null) - { - revision.User = new RevisionUser() - { - AvatarId = user.AvatarId, - Id = user.Id, - Name = user.Name - }; - } - - revisions.Add(revision); - } - - return new Paged>(revisions, totalResults, pageNumber, pageSize); - } - - - /// - /// Get revision list for an entity - /// - public async Task> GetPaged(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity - { - Paged> items = await GetPagedWithModel(id, pageNumber, pageSize); - return items.MapTo(model => new Revision() - { - ChangeVector = model.ChangeVector, - Date = model.Date, - ModelId = model.ModelId, - User = model.User - }); - } - } - - - public interface IRevisionsApi - { - /// - /// Get revision list including models for an entity - /// - Task>> GetPagedWithModel(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity; - - /// - /// Get revision list for an entity - /// - Task> GetPaged(string id, int pageNumber = 1, int pageSize = 10) where T : ZeroEntity; - } -} diff --git a/zero.Core/_legacy/Api/SectionsApi.cs b/zero.Core/_legacy/Api/SectionsApi.cs deleted file mode 100644 index 93000343..00000000 --- a/zero.Core/_legacy/Api/SectionsApi.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; -using zero.Core.Options; - -namespace zero.Core.Api -{ - public class SectionsApi : ISectionsApi - { - protected IZeroOptions Options { get; private set; } - - - public SectionsApi(IZeroOptions options) - { - Options = options; - } - - - /// - public IReadOnlyCollection GetAll() - { - return Options.Sections.GetAllItems(); - } - - - /// - public ISection GetByAlias(string alias) - { - return Options.Sections.GetAllItems().FirstOrDefault(section => section.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase)); - } - } - - - public interface ISectionsApi - { - /// - /// Get all available backoffice sections - /// - IReadOnlyCollection GetAll(); - - /// - /// Get backoffice section by alias - /// - ISection GetByAlias(string alias); - } -} diff --git a/zero.Core/_legacy/Api/SettingsApi.cs b/zero.Core/_legacy/Api/SettingsApi.cs deleted file mode 100644 index 58e25101..00000000 --- a/zero.Core/_legacy/Api/SettingsApi.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using zero.Core.Entities; -using zero.Core.Options; - -namespace zero.Core.Api -{ - public class SettingsApi : ISettingsApi - { - protected IZeroOptions Options { get; set; } - - - public SettingsApi(IZeroOptions options) - { - Options = options; - } - - - /// - public IReadOnlyCollection GetAreas() - { - return Options.Settings.GetAllItems(); - } - } - - - public interface ISettingsApi - { - /// - /// Get settings areas for backoffice display and grouping - /// - IReadOnlyCollection GetAreas(); - } -} diff --git a/zero.Core/_legacy/Api/SetupApi.cs b/zero.Core/_legacy/Api/SetupApi.cs deleted file mode 100644 index 760c4aaa..00000000 --- a/zero.Core/_legacy/Api/SetupApi.cs +++ /dev/null @@ -1,339 +0,0 @@ -using FluentValidation.Results; -using Microsoft.AspNetCore.Identity; -using Newtonsoft.Json; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System.IO; -using System.Security.Cryptography; -using zero.Setup; - -namespace zero.Core.Api -{ - public class SetupApi : ISetupApi - { - protected IZeroOptions Options { get; private set; } - - protected IPasswordHasher PasswordHasher { get; private set; } - - protected IZeroDocumentConventionsBuilder ConventionsBuilder { get; private set; } - - - public SetupApi(IZeroOptions options, IPasswordHasher passwordHasher, IZeroDocumentConventionsBuilder conventionsBuilder) - { - Options = options; - PasswordHasher = passwordHasher; - ConventionsBuilder = conventionsBuilder; - } - - - public async Task> Install(SetupModel model) - { - ValidationResult validation = await new SetupModelValidator().ValidateAsync(model); - - if (!validation.IsValid) - { - return EntityResult.Fail(validation); - } - - DocumentStore raven = null; - - try - { - // test read & write permissions on folders - // TODO - - // create temporary instance of database - raven = new DocumentStore() - { - Urls = model.Database.Url.Split(','), - Database = model.Database.Name - }; - - ConventionsBuilder.Run(raven.Conventions); - raven.Initialize(); - - // create application - Application app = Prepare(new Application() - { - Id = "app." + Safenames.Alias(model.AppName), - CreatedDate = DateTimeOffset.Now, - IsActive = true, - Name = model.AppName, - Alias = Safenames.Alias(model.AppName), - Database = model.Database.Name - }); - - // create user - ZeroUser user = Prepare(new ZeroUser() - { - IsSuper = true, - CreatedDate = DateTimeOffset.Now, - Email = model.User.Email, - Username = model.User.Email, - Name = model.User.Name, - IsActive = true, - LanguageId = Options.DefaultLanguage, - Alias = Safenames.Alias(model.User.Name), - IsEmailConfirmed = true, - SecurityStamp = NewSecurityStamp() - }); - - user.PasswordHash = PasswordHasher.HashPassword(user, model.User.Password); - - // create default language - Language language = Prepare(new Language() // TODO get default language selection from setup UI - { - Name = "English", - Alias = Safenames.Alias("English"), - CreatedDate = DateTimeOffset.Now, - Code = "en-US", - IsActive = true, - IsDefault = true - }); - - using IAsyncDocumentSession session = raven.OpenAsyncSession(); - - await session.StoreAsync(user); - - // user creation failed - //if (!result.Succeeded) - //{ - // EntityResult entityResult = EntityResult.Fail(); - - // foreach (IdentityError error in result.Errors) - // { - // entityResult.AddError(error.Code, error.Description); - // } - - // return entityResult; - //} - - - await session.StoreAsync(app); - - // save default user roles - IList roles = GetRoles(model); - - foreach (ZeroUserRole role in roles) - { - await session.StoreAsync(role); - } - - // add admin role to super user - // set app-id for user and store it - user.AppId = session.Advanced.GetDocumentId(app); - user.CurrentAppId = user.AppId; - user.RoleIds.Add(roles.First(role => role.Name == "Standard").Id); - user.RoleIds.Add(roles.First(role => role.Name == "Administrator").Id); - await session.StoreAsync(user); - - // create language - await session.StoreAsync(language); - - // set countries - using (Raven.Client.Documents.BulkInsert.BulkInsertOperation bulkInsert = raven.BulkInsert()) - { - foreach (Country country in GetCountries(model, language)) - { - await bulkInsert.StoreAsync(country); - } - } - - // update settings file. if this fails the changes won't be stored - UpdateSettingsFile(model); - - await session.SaveChangesAsync(); - } - catch (Exception) - { - // TODO revert - throw; - } - finally - { - raven?.Dispose(); - } - - return EntityResult.Success(model); - } - - - /// - /// Updates the settings file with the new data (database connection and version) - /// - void UpdateSettingsFile(SetupModel model) - { - // TODO should we write this into appSettings.json now? - // or let the user do it in the code editor? - - //var filePath = Path.Combine(model.ContentRootPath, "zeroSettings.json"); - //string json = File.ReadAllText(filePath); - - //ZeroConfiguration config = JsonConvert.DeserializeObject(json); - - //config.Raven = new RavenOptions() - //{ - // Database = model.Database.Name, - // Url = model.Database.Url - //}; - - //config.ZeroVersion = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(); - - //json = JsonConvert.SerializeObject(config, Formatting.Indented); - - //File.WriteAllText(filePath, json); - } - - - /// - /// Get countries for supported backoffice languages - /// - IList GetCountries(SetupModel model, Language defaultLanguage) - { - List countries = new List(); - - //string[] isoCodes = Config.SupportedLanguages; - string[] isoCodes = new string[1] { "en-US" }; - - foreach (string languageISO in isoCodes) - { - // country list from: https://github.com/umpirsky/country-list/tree/master/data - var filePath = Path.Combine(model.ContentRootPath, "Resources", "Countries", "countries." + languageISO.ToLowerInvariant() + ".json"); - string json = File.ReadAllText(filePath); - - - countries.AddRange(JsonConvert.DeserializeObject>(json).Select(country => Prepare(new Country() - { - CreatedDate = DateTimeOffset.Now, - IsActive = true, - //AppId = Constants.Database.SharedAppId, // TODO appx fix - Alias = Safenames.Alias(country.Value), - //LanguageId = defaultLanguage.Id, - Code = country.Key.ToLowerInvariant(), - Name = country.Value - })).ToList()); - } - - return countries; - } - - - /// - /// Create default roles - /// - IList GetRoles(SetupModel model) - { - string type = Constants.Auth.Claims.Permission; - - ZeroUserRole adminRole = Prepare(new ZeroUserRole() - { - Name = "Administrator", - Alias = Safenames.Alias("Administrator"), - Sort = 0, - Icon = "fth-award", - CreatedDate = DateTimeOffset.Now, - IsActive = true, - Claims = new List() - { - //new UserClaim(type, Permissions.Applications, PermissionsValue.Write), - new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Spaces, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Pages, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Media, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Settings, PermissionsValue.True), - new UserClaim(type, Permissions.Settings.Applications, PermissionsValue.None), - new UserClaim(type, Permissions.Settings.Countries, PermissionsValue.Update), - new UserClaim(type, Permissions.Settings.Logging, PermissionsValue.Update), - new UserClaim(type, Permissions.Settings.Translations, PermissionsValue.Update), - new UserClaim(type, Permissions.Settings.Updates, PermissionsValue.Update), - new UserClaim(type, Permissions.Settings.Users, PermissionsValue.Update), - }, - }); - - ZeroUserRole editorRole = Prepare(new ZeroUserRole() - { - Name = "Editor", - Alias = Safenames.Alias("Editor"), - Sort = 1, - Icon = "fth-feather", - CreatedDate = DateTimeOffset.Now, - IsActive = true, - Claims = new List() - { - new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Spaces, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Pages, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Media, PermissionsValue.True), - new UserClaim(type, Permissions.Sections.Settings, PermissionsValue.True), - new UserClaim(type, Permissions.Settings.Translations, PermissionsValue.True) - } - }); - - ZeroUserRole defaultRole = Prepare(new ZeroUserRole() - { - Name = "Standard", - Alias = Safenames.Alias("Standard"), - Sort = 2, - Icon = "fth-users", - CreatedDate = DateTimeOffset.Now, - IsActive = true, - Claims = new List() - { - new UserClaim(type, Permissions.Sections.Dashboard, PermissionsValue.True) - } - }); - - return new List() { adminRole, editorRole, defaultRole }; - } - - - T Prepare(T model, string languageId = null) where T : ZeroIdEntity - { - ZeroEntity zeroEntity = model as ZeroEntity; - - // set default properties - if (zeroEntity != null && zeroEntity.CreatedDate == default) - { - zeroEntity.CreatedDate = DateTimeOffset.Now; - } - if (zeroEntity != null && zeroEntity.CreatedById == default) - { - zeroEntity.CreatedById = Constants.Auth.SystemUser; - } - if (zeroEntity != null && zeroEntity.LanguageId == default) - { - zeroEntity.LanguageId = languageId; - } - - // update name alias and last modified - if (zeroEntity != null) - { - zeroEntity.Alias = Safenames.Alias(zeroEntity.Name); - zeroEntity.LastModifiedById = Constants.Auth.SystemUser; - zeroEntity.LastModifiedDate = DateTimeOffset.Now; - zeroEntity.Hash ??= IdGenerator.Create(); - zeroEntity.IsActive = true; - } - - return model; - } - - - /// - /// Creates a new security stamp - /// - string NewSecurityStamp() - { - byte[] bytes = new byte[20]; - RandomNumberGenerator.Fill(bytes); - return Base32.ToBase32(bytes); - } - } - - - - public interface ISetupApi - { - Task> Install(SetupModel model); - } -} diff --git a/zero.Core/_legacy/Collections/Pages/PagesCollection.cs b/zero.Core/_legacy/Collections/Pages/PagesCollection.cs deleted file mode 100644 index da540a02..00000000 --- a/zero.Core/_legacy/Collections/Pages/PagesCollection.cs +++ /dev/null @@ -1,534 +0,0 @@ -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.Api; -using zero.Core.Database.Indexes; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Handlers; -using zero.Core.Routing; - -namespace zero.Core.Collections -{ - public class PagesCollection : CollectionBase, IPagesCollection - { - const string RECYCLE_BIN_GROUP = "pages"; - - protected IRecycleBinApi RecycleBinApi { get; private set; } - - protected ILogger Logger { get; private set; } - - protected IHandlerHolder Handler { get; private set; } - - protected IRoutes Routes { get; set; } - - - public PagesCollection(IStoreContext context, IRecycleBinApi recycleBinApi, IHandlerHolder handler, - ILogger logger, IRoutes routes, IValidator validator = null) : base(context, validator) - { - RecycleBinApi = recycleBinApi; - Handler = handler; - Logger = logger; - Routes = routes; - } - - - /// - public Task GetEmpty(string pageType, string parentId = null) - { - PageType type = GetPageType(pageType); - - if (type == null) - { - return Task.FromResult(null); - } - - try - { - Page model = Activator.CreateInstance(type.ContentType) as Page; - - model.PageTypeAlias = type.Alias; - model.ParentId = parentId; // TODO validate if type is allowed and if parentid is allowed - - return Task.FromResult(model); - } - catch - { - Logger.LogWarning("Could not create page with type {type}", type); - } - - return Task.FromResult(null); - } - - - /// - public IList GetPageTypes() => Context.Options.Pages.GetAllItems().ToList(); - - - /// - public PageType GetPageType(string alias) => Context.Options.Pages.GetAllItems().FirstOrDefault(x => x.Alias == alias); - - - /// - public async Task> GetAllowedPageTypes(string parentId = null) - { - IEnumerable types = Context.Options.Pages.GetAllItems(); - List parents = new(); - - if (!parentId.IsNullOrEmpty()) - { - Pages_ByHierarchy.Result result = await Session.Query() - .ProjectInto() - .Include(x => x.Id) - .Include(x => x.Path.Select(p => p.Id)) - .FirstOrDefaultAsync(x => x.Id == parentId); - - if (result != null) - { - List ids = result.Path.Select(x => x.Id).ToList(); - ids.Add(result.Id); - parents = (await Session.LoadAsync(ids)).Select(x => x.Value).Reverse().ToList(); - } - } - - IPageTypeHandler handler = Handler.Get(); - - // if there is no registered handler we just allow all page types - if (handler == null) - { - return types.ToList(); - } - - return (await handler.GetAllowedPageTypes(Context.Application, types, parents))?.ToList() ?? new(); - } - - - /// - 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"); - } - - foreach (var item in items) - { - item.Value.Sort = index; - index += 10; - await Save(item.Value); - //session.Advanced.Patch(item.Value.Id, x => x.Sort, index++); - } - - return EntityResult>.Success(items.Select(x => x.Value).ToList()); - } - - - /// - public async Task> Move(string id, string parentId) - { - Page model = await GetById(id); - Page 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) - { - Page model = await GetById(id); - Page 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"); - } - - // 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 GetByIds(childrenResult.ChildrenIds); - - foreach (var child in childrenPages) - { - Page childPage = child.Value.Clone(); - childPage.Id = null; - childPage.IsActive = false; - childPage.ParentId = newParentId; - childPage.CreatedDate = DateTimeOffset.Now; - - await Save(childPage); - await AddChildren(child.Key, childPage.Id); - } - } - - if (includeDescendants) - { - await AddChildren(baseId, model.Id); - } - - return EntityResult.Success(model); - } - - - /// - public async Task> Delete(string id, bool recursive = true, bool moveToRecycleBin = true) - { - List pages = recursive ? await GetByIdWithDescendants(id) : new() { await GetById(id) }; - - if (pages.Count < 1) - { - return EntityResult.Fail("@errors.ondelete.idnotfound"); - } - - if (moveToRecycleBin) - { - await RecycleBinApi.Add(pages, RECYCLE_BIN_GROUP); - } - - await Delete(pages.ToArray()); - - return EntityResult.Success(pages.Select(x => x.Id).ToArray()); - } - - - /// - public async Task> Restore(string id, bool includeDescendants = false) - { - EntityResult result = new EntityResult(); - RecycledEntity 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 Page).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 (RecycledEntity entity in entities) - { - // check if it contains page data - if (entity.Group != RECYCLE_BIN_GROUP || !(entity.Content is Page)) - { - //result.AddError("Cannot parse recycled entity as an Page in group \"" + RECYCLE_BIN_GROUP + "\""); // TODO correct error message - continue; - } - - // get page - Page page = entity.Content as Page; - 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 Save(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; - } - - - /// - public async Task> GetChildren(string parentId = null, string activeId = null, string search = null) - { - IList items = new List(); - IReadOnlyCollection pageTypes = Context.Options.Pages.GetAllItems(); - string[] openIds = new string[0] { }; - IList pages = null; - IList children = null; - bool isSearch = !search.IsNullOrWhiteSpace(); - - if (isSearch) - { - pages = await Session - .Query() - .SearchIf(x => x.Name, search, "*") - .OrderBy(x => x.Sort, OrderingType.Long) - .ToListAsync(); - - var urls = await Routes.GetUrls(pages.ToArray()); - - foreach (Page page in pages) - { - if (urls.TryGetValue(page, out string url)) - { - page.Url = url; - } - } - } - else - { - - pages = await Session - .Query() - .WhereIf(x => x.ParentId == parentId, !parentId.IsNullOrEmpty(), x => x.ParentId == null) - .OrderBy(x => x.Sort, OrderingType.Long) - .ToListAsync(); - - - // get hierarchy so we know if we should set the page to open - if (!activeId.IsNullOrEmpty()) - { - Pages_ByHierarchy.Result result = await Session.Query() - .ProjectInto() - .Include(x => x.Path.Select(p => p.Id)) - .FirstOrDefaultAsync(x => x.Id == activeId); - - if (result != null) - { - openIds = result.Path.Select(x => x.Id).ToArray(); // .Union(new string[1] { activeId }) - } - } - - - // get children for all pages - string[] pageIds = pages.Select(x => x.Id).ToArray(); - - children = await Session.Query() - .ProjectInto() - .Where(x => x.Id.In(pageIds)) - .ToListAsync(); - } - - - // function to get modifier icon - TreeItemModifier GetModifier(Page page) - { - if (page.PublishDate > DateTimeOffset.Now || page.UnpublishDate > DateTimeOffset.Now) - { - return new TreeItemModifier("@page.schedule.scheduled", "fth-clock"); - } - if (!page.IsActive) - { - return new TreeItemModifier("@ui.inactive", "fth-minus-circle color-red"); - } - return null; - } - - - // build tree - foreach (Page page in pages) - { - PageType pageType = pageTypes.FirstOrDefault(x => x.Alias == page.PageTypeAlias); - - if (pageType == null) - { - continue; - // TODO the page type does not exist anymore - } - - int childCount = isSearch ? 0 : children.Count(x => x.Id == page.Id); - - items.Add(new TreeItem() - { - Id = page.Id, - Name = page.Name, - HasChildren = childCount > 0, - ChildCount = childCount, - ParentId = page.ParentId, - Sort = page.Sort, - Icon = pageType.Icon, - IsOpen = openIds.Contains(page.Id), - IsInactive = !page.IsActive, - HasActions = true, - Modifier = GetModifier(page), - Description = isSearch ? page.Url : null - }); - } - - if (parentId.IsNullOrEmpty()) - { - items.Add(new TreeItem() - { - Id = "recyclebin", - ParentId = null, - Sort = 99999, - Name = "@recyclebin.name", - Icon = "fth-trash", - HasChildren = false, - HasActions = true - }); - } - - return items; - } - - - /// - /// Get a page with all its descendants - /// - async Task> GetByIdWithDescendants(string id) - { - List items = new List(); - - Page model = await GetById(id); - - if (model == null) - { - return items; - } - - items.Add(model); - - // 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 GetByIds(childrenResult.ChildrenIds); - - foreach (var child in childrenPages) - { - items.Add(child.Value); - await AddChildren(child.Value.Id); - } - } - - await AddChildren(model.Id); - - return items; - } - } - - - public interface IPagesCollection : ICollectionBase - { - /// - /// Get a new empty page with the specified type - /// - public Task GetEmpty(string pageType, string parentId = null); - - /// - /// 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); - - /// - /// Get all children for the current parent page (or root if empty) - /// - Task> GetChildren(string parentId = null, string activeId = null, string search = null); - - /// - /// 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 recursive = true, bool moveToRecycleBin = true); - - /// - /// Restores a page from the recycle bin - /// - Task> Restore(string id, bool includeDescendants = false); - } -} diff --git a/zero.Core/_legacy/Collections/Translations/TranslationValidator.cs b/zero.Core/_legacy/Collections/Translations/TranslationValidator.cs deleted file mode 100644 index a0bfcc98..00000000 --- a/zero.Core/_legacy/Collections/Translations/TranslationValidator.cs +++ /dev/null @@ -1,17 +0,0 @@ -using FluentValidation; -using zero.Core.Database; -using zero.Core.Entities; -using zero.Core.Extensions; -using zero.Core.Validation; - -namespace zero.Core.Collections -{ - public class TranslationValidator : ZeroValidator - { - public TranslationValidator(IZeroStore store) - { - RuleFor(x => x.Key).Length(2, 300).Unique(store); - RuleFor(x => x.Value).MaximumLength(10 * 1000); - } - } -} diff --git a/zero.Core/_legacy/Collections/Translations/TranslationsCollection.cs b/zero.Core/_legacy/Collections/Translations/TranslationsCollection.cs deleted file mode 100644 index 0f96d764..00000000 --- a/zero.Core/_legacy/Collections/Translations/TranslationsCollection.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -namespace zero.Core.Collections -{ - public class TranslationsCollection : EntityStore, ITranslationsCollection - { - public TranslationsCollection(IStoreContext context) : base(context) { } - - - /// - public async Task LoadString(string id) - { - return (await Load(id))?.Value; - } - } - - - public interface ITranslationsCollection : IEntityStore - { - /// - /// Get a translated string by id - /// - Task LoadString(string id); - } -} diff --git a/zero.Core/_legacy/Entities/RecycleBin/RecycleBinListQuery.cs b/zero.Core/_legacy/Entities/RecycleBin/RecycleBinListQuery.cs deleted file mode 100644 index 2abe5dbf..00000000 --- a/zero.Core/_legacy/Entities/RecycleBin/RecycleBinListQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace zero.Core.Entities -{ - public class RecycleBinListQuery : ListQuery - { - public string Group { get; set; } - - public string OperationId { get; set; } - } -} diff --git a/zero.Core/_legacy/Entities/RecycleBin/RecycledEntity.cs b/zero.Core/_legacy/Entities/RecycleBin/RecycledEntity.cs deleted file mode 100644 index 4ceb0084..00000000 --- a/zero.Core/_legacy/Entities/RecycleBin/RecycledEntity.cs +++ /dev/null @@ -1,28 +0,0 @@ -using zero.Core.Attributes; - -namespace zero.Core.Entities -{ - [Collection("RecycleBin")] - public class RecycledEntity : ZeroEntity - { - /// - /// Id of the recycled entity - /// - public string OriginalId { get; set; } - - /// - /// Group recycled entities together which have been recycled in a single operation - /// - public string OperationId { get; set; } - - /// - /// Contains the entity content - /// - public ZeroEntity Content { get; set; } - - /// - /// Recycled entities can be grouped together (e.g. pages, media, ...) - /// - public string Group { get; set; } - } -}