From e0fc7c620acb35ab19555520159cb75bf9110ef3 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Wed, 1 Dec 2021 15:54:11 +0100 Subject: [PATCH] start Flavors --- zero.Core/Localization/LocalizationModule.cs | 7 ++ zero.Core/Media/Indexes/Media_ByChildren.cs | 2 +- zero.Core/Media/Indexes/Media_ByHierarchy.cs | 2 +- zero.Core/Media/Models/Media.cs | 2 +- zero.Core/Models/ISupportsFlavors.cs | 14 ++++ ...ZeroRouteEntity.cs => ISupportsRouting.cs} | 2 +- .../{IZeroTreeEntity.cs => ISupportsTrees.cs} | 2 +- zero.Core/Models/ZeroEntity.cs | 7 +- zero.Core/Pages/Indexes/Pages_AsHistory.cs | 2 +- zero.Core/Pages/Indexes/Pages_ByHierarchy.cs | 2 +- zero.Core/Pages/Indexes/Pages_WithChildren.cs | 2 +- zero.Core/Pages/Models/Page.cs | 2 +- ...Conventions.cs => ISupportsPersistence.cs} | 2 +- zero.Core/Persistence/Tokens/ChangeToken.cs | 2 +- zero.Core/Persistence/Tokens/Token.cs | 2 +- .../ZeroDocumentConventionsBuilder.cs | 2 +- zero.Core/Routing/Routes.cs | 32 ++++---- .../Routing/ZeroEntityRouteInterceptor.cs | 2 +- zero.Core/Routing/ZeroRouteProvider.cs | 52 ++++++------- zero.Core/Stores/EntityStoreExtensions.cs | 2 +- zero.Core/Stores/Flavors/FlavorConfig.cs | 54 +++++++++++++ zero.Core/Stores/Flavors/FlavorOptions.cs | 77 +++++++++++++++++++ zero.Core/Stores/StoreOperations.Tree.cs | 20 ++--- zero.Core/Stores/StoreOperations.cs | 14 ++-- zero.Core/Stores/StoreOperationsExtensions.cs | 2 +- zero.Core/Stores/StoresModule.cs | 2 + zero.Core/Stores/TreeEntityStore.cs | 4 +- 27 files changed, 237 insertions(+), 78 deletions(-) create mode 100644 zero.Core/Models/ISupportsFlavors.cs rename zero.Core/Models/{IZeroRouteEntity.cs => ISupportsRouting.cs} (87%) rename zero.Core/Models/{IZeroTreeEntity.cs => ISupportsTrees.cs} (89%) rename zero.Core/Persistence/{IZeroDbConventions.cs => ISupportsPersistence.cs} (75%) create mode 100644 zero.Core/Stores/Flavors/FlavorConfig.cs create mode 100644 zero.Core/Stores/Flavors/FlavorOptions.cs diff --git a/zero.Core/Localization/LocalizationModule.cs b/zero.Core/Localization/LocalizationModule.cs index 75a61557..6fc961e4 100644 --- a/zero.Core/Localization/LocalizationModule.cs +++ b/zero.Core/Localization/LocalizationModule.cs @@ -13,5 +13,12 @@ public class LocalizationModule : ZeroModule services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.Configure(opts => + { + opts.Provide("zero.country", "Country", "Default country", "fth-flag"); + opts.Provide("zero.language", "Language", "Default language", "fth-flag"); + opts.Provide("zero.translation", "Translation", "Default translation", "fth-flag"); + }); } } \ No newline at end of file diff --git a/zero.Core/Media/Indexes/Media_ByChildren.cs b/zero.Core/Media/Indexes/Media_ByChildren.cs index b53fc927..37c4e9cf 100644 --- a/zero.Core/Media/Indexes/Media_ByChildren.cs +++ b/zero.Core/Media/Indexes/Media_ByChildren.cs @@ -4,7 +4,7 @@ namespace zero.Media; public class Media_ByChildren : ZeroMultiMapIndex { - public class Result : ZeroIdEntity, IZeroDbConventions + public class Result : ZeroIdEntity, ISupportsPersistence { public string ParentId { get; set; } diff --git a/zero.Core/Media/Indexes/Media_ByHierarchy.cs b/zero.Core/Media/Indexes/Media_ByHierarchy.cs index 6de87950..ad33cd33 100644 --- a/zero.Core/Media/Indexes/Media_ByHierarchy.cs +++ b/zero.Core/Media/Indexes/Media_ByHierarchy.cs @@ -4,7 +4,7 @@ namespace zero.Media; public class Media_ByHierarchy : ZeroIndex { - public class Result : ZeroIdEntity, IZeroDbConventions + public class Result : ZeroIdEntity, ISupportsPersistence { public string Name { get; set; } diff --git a/zero.Core/Media/Models/Media.cs b/zero.Core/Media/Models/Media.cs index fb38417f..70a5813b 100644 --- a/zero.Core/Media/Models/Media.cs +++ b/zero.Core/Media/Models/Media.cs @@ -4,7 +4,7 @@ /// A media file (can contain an image or other media like videos and documents) /// [RavenCollection("Media")] -public class Media : ZeroEntity, IZeroTreeEntity +public class Media : ZeroEntity, ISupportsTrees { public Media() { diff --git a/zero.Core/Models/ISupportsFlavors.cs b/zero.Core/Models/ISupportsFlavors.cs new file mode 100644 index 00000000..aac608cd --- /dev/null +++ b/zero.Core/Models/ISupportsFlavors.cs @@ -0,0 +1,14 @@ +namespace zero.Models; + +public interface ISupportsFlavors +{ + /// + /// Id of the entity + /// + string Id { get; set; } + + /// + /// Alias of the used flavor + /// + string Flavor { get; set; } +} \ No newline at end of file diff --git a/zero.Core/Models/IZeroRouteEntity.cs b/zero.Core/Models/ISupportsRouting.cs similarity index 87% rename from zero.Core/Models/IZeroRouteEntity.cs rename to zero.Core/Models/ISupportsRouting.cs index f8ac87f2..4189bbb3 100644 --- a/zero.Core/Models/IZeroRouteEntity.cs +++ b/zero.Core/Models/ISupportsRouting.cs @@ -1,6 +1,6 @@ namespace zero.Models; -public interface IZeroRouteEntity +public interface ISupportsRouting { /// /// Id of the entity diff --git a/zero.Core/Models/IZeroTreeEntity.cs b/zero.Core/Models/ISupportsTrees.cs similarity index 89% rename from zero.Core/Models/IZeroTreeEntity.cs rename to zero.Core/Models/ISupportsTrees.cs index 5336cb19..437433ac 100644 --- a/zero.Core/Models/IZeroTreeEntity.cs +++ b/zero.Core/Models/ISupportsTrees.cs @@ -1,6 +1,6 @@ namespace zero.Models; -public interface IZeroTreeEntity +public interface ISupportsTrees { /// /// Id of the entity diff --git a/zero.Core/Models/ZeroEntity.cs b/zero.Core/Models/ZeroEntity.cs index 37e863a0..34f60089 100644 --- a/zero.Core/Models/ZeroEntity.cs +++ b/zero.Core/Models/ZeroEntity.cs @@ -4,7 +4,7 @@ using System.Diagnostics; namespace zero.Models; [DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")] -public class ZeroEntity : ZeroIdEntity, IZeroDbConventions, IZeroRouteEntity +public class ZeroEntity : ZeroIdEntity, ISupportsPersistence, ISupportsRouting, ISupportsFlavors { /// /// Full name of the entity @@ -66,6 +66,11 @@ public class ZeroEntity : ZeroIdEntity, IZeroDbConventions, IZeroRouteEntity /// public string LanguageId { get; set; } + /// + /// Alias of the used flavor (uses default if empty) + /// + public string Flavor { get; set; } + /// /// [Warning] This field is always empty when bound to the database. /// It is only filled in the app-code for routing. diff --git a/zero.Core/Pages/Indexes/Pages_AsHistory.cs b/zero.Core/Pages/Indexes/Pages_AsHistory.cs index 797e8e18..70f17d1d 100644 --- a/zero.Core/Pages/Indexes/Pages_AsHistory.cs +++ b/zero.Core/Pages/Indexes/Pages_AsHistory.cs @@ -4,7 +4,7 @@ namespace zero.Pages; public class Pages_AsHistory : ZeroIndex { - public class Result : ZeroIdEntity, IZeroDbConventions + public class Result : ZeroIdEntity, ISupportsPersistence { public DateTimeOffset LastModified { get; set; } } diff --git a/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs b/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs index c6f1ff50..cfc4b15a 100644 --- a/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs +++ b/zero.Core/Pages/Indexes/Pages_ByHierarchy.cs @@ -4,7 +4,7 @@ namespace zero.Pages; public class Pages_ByHierarchy : ZeroIndex { - public class Result : ZeroIdEntity, IZeroDbConventions + public class Result : ZeroIdEntity, ISupportsPersistence { public string Name { get; set; } diff --git a/zero.Core/Pages/Indexes/Pages_WithChildren.cs b/zero.Core/Pages/Indexes/Pages_WithChildren.cs index 4010c4b7..170515eb 100644 --- a/zero.Core/Pages/Indexes/Pages_WithChildren.cs +++ b/zero.Core/Pages/Indexes/Pages_WithChildren.cs @@ -4,7 +4,7 @@ namespace zero.Pages; public class Pages_WithChildren : ZeroIndex { - public class Result : ZeroIdEntity, IZeroDbConventions + public class Result : ZeroIdEntity, ISupportsPersistence { public string ParentId { get; set; } diff --git a/zero.Core/Pages/Models/Page.cs b/zero.Core/Pages/Models/Page.cs index 82085c47..88bab4bd 100644 --- a/zero.Core/Pages/Models/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, IZeroTreeEntity +public class Page : ZeroEntity, ISupportsTrees { /// /// Use this field (when filled out) instead of the alias for URL generation diff --git a/zero.Core/Persistence/IZeroDbConventions.cs b/zero.Core/Persistence/ISupportsPersistence.cs similarity index 75% rename from zero.Core/Persistence/IZeroDbConventions.cs rename to zero.Core/Persistence/ISupportsPersistence.cs index d0433123..68ad2af0 100644 --- a/zero.Core/Persistence/IZeroDbConventions.cs +++ b/zero.Core/Persistence/ISupportsPersistence.cs @@ -3,4 +3,4 @@ /// /// Triggers custom Raven conventions for database operations /// -public interface IZeroDbConventions { } \ No newline at end of file +public interface ISupportsPersistence { } \ No newline at end of file diff --git a/zero.Core/Persistence/Tokens/ChangeToken.cs b/zero.Core/Persistence/Tokens/ChangeToken.cs index 392504a3..06554343 100644 --- a/zero.Core/Persistence/Tokens/ChangeToken.cs +++ b/zero.Core/Persistence/Tokens/ChangeToken.cs @@ -4,7 +4,7 @@ /// A change token holds a reference to a database entity /// This is used to verify change requests for entities in the zero backoffice /// -public class ChangeToken : IZeroDbConventions +public class ChangeToken : ISupportsPersistence { public string Id { get; set; } diff --git a/zero.Core/Persistence/Tokens/Token.cs b/zero.Core/Persistence/Tokens/Token.cs index 95a869c8..891d2cd6 100644 --- a/zero.Core/Persistence/Tokens/Token.cs +++ b/zero.Core/Persistence/Tokens/Token.cs @@ -1,7 +1,7 @@ namespace zero.Persistence; [RavenCollection("Tokens")] -public class SecurityToken : IZeroDbConventions +public class SecurityToken : ISupportsPersistence { public string Id { get; set; } diff --git a/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs b/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs index 0912d703..c920df37 100644 --- a/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs +++ b/zero.Core/Persistence/ZeroDocumentConventionsBuilder.cs @@ -9,7 +9,7 @@ public class ZeroDocumentConventionsBuilder : IZeroDocumentConventionsBuilder { protected HashSet PolymorphTypes { get; private set; } = new(); - protected Type AcceptsZeroConventionsType { get; set; } = typeof(IZeroDbConventions); + protected Type AcceptsZeroConventionsType { get; set; } = typeof(ISupportsPersistence); protected char IdentityPartsSeparator { get; set; } = '.'; diff --git a/zero.Core/Routing/Routes.cs b/zero.Core/Routing/Routes.cs index a85329e1..7cafd2c2 100644 --- a/zero.Core/Routing/Routes.cs +++ b/zero.Core/Routing/Routes.cs @@ -23,19 +23,19 @@ public class Routes : IRoutes /// - public async Task GetUrl(T model) where T : IZeroRouteEntity => (await GetRoute(model))?.Url; + public async Task GetUrl(T model) where T : ISupportsRouting => (await GetRoute(model))?.Url; /// - public async Task GetUrl(string id) where T : IZeroRouteEntity => (await GetRoute(id))?.Url; + public async Task GetUrl(string id) where T : ISupportsRouting => (await GetRoute(id))?.Url; /// - public async Task> GetUrls(params T[] models) where T : IZeroRouteEntity => (await GetRoutes(models)).ToDictionary(x => x.Key, x => x.Value?.Url); + public async Task> GetUrls(params T[] models) where T : ISupportsRouting => (await GetRoutes(models)).ToDictionary(x => x.Key, x => x.Value?.Url); /// - public async Task GetRoute(string id) where T : IZeroRouteEntity + public async Task GetRoute(string id) where T : ISupportsRouting { T model = await GetContext().Session.LoadAsync(id); return await GetRoute(model); @@ -43,7 +43,7 @@ public class Routes : IRoutes /// - public async Task GetRoute(T model) where T : IZeroRouteEntity + public async Task GetRoute(T model) where T : ISupportsRouting { if (model == null) { @@ -61,7 +61,7 @@ public class Routes : IRoutes /// - public async Task> GetRoutes(params T[] models) where T : IZeroRouteEntity + public async Task> GetRoutes(params T[] models) where T : ISupportsRouting { if (models.Length < 1) { @@ -81,12 +81,12 @@ public class Routes : IRoutes } RoutingContext context = GetContext(); - return (await routeProvider.Find(context, models.Select(x => (IZeroRouteEntity)x).ToArray())).ToDictionary(x => (T)x.Key, x => x.Value); + return (await routeProvider.Find(context, models.Select(x => (ISupportsRouting)x).ToArray())).ToDictionary(x => (T)x.Key, x => x.Value); } /// - public virtual bool TryGetProvider(T model, out IRouteProvider provider) where T : IZeroRouteEntity + public virtual bool TryGetProvider(T model, out IRouteProvider provider) where T : ISupportsRouting { Type type = model.GetType(); provider = Providers.OrderByDescending(x => x.Priority).FirstOrDefault(x => x.CanHandle(type)); @@ -126,7 +126,7 @@ public class Routes : IRoutes /// /// Find a persisted route for an entity /// - protected virtual async Task FindRoute(IRouteProvider provider, RoutingContext context, IZeroRouteEntity model) + protected virtual async Task FindRoute(IRouteProvider provider, RoutingContext context, ISupportsRouting model) { return await provider.Find(context, model); } @@ -146,37 +146,37 @@ public interface IRoutes /// /// Get the URL for an entity /// - Task GetUrl(T model) where T : IZeroRouteEntity; + Task GetUrl(T model) where T : ISupportsRouting; /// /// Get the URL for an entity /// - Task GetUrl(string id) where T : IZeroRouteEntity; + Task GetUrl(string id) where T : ISupportsRouting; /// /// Get URLs for multiple entities /// - Task> GetUrls(params T[] models) where T : IZeroRouteEntity; + Task> GetUrls(params T[] models) where T : ISupportsRouting; /// /// Get the route object for an entity /// - Task GetRoute(T model) where T : IZeroRouteEntity; + Task GetRoute(T model) where T : ISupportsRouting; /// /// Get the route object for an entity /// - Task GetRoute(string id) where T : IZeroRouteEntity; + Task GetRoute(string id) where T : ISupportsRouting; /// /// Get routes for multiple entities /// - Task> GetRoutes(params T[] models) where T : IZeroRouteEntity; + Task> GetRoutes(params T[] models) where T : ISupportsRouting; /// /// Find a provider for a certain entity /// - bool TryGetProvider(T model, out IRouteProvider provider) where T : IZeroRouteEntity; + bool TryGetProvider(T model, out IRouteProvider provider) where T : ISupportsRouting; /// /// Find a provider by alias diff --git a/zero.Core/Routing/ZeroEntityRouteInterceptor.cs b/zero.Core/Routing/ZeroEntityRouteInterceptor.cs index 56630f91..2f1486ec 100644 --- a/zero.Core/Routing/ZeroEntityRouteInterceptor.cs +++ b/zero.Core/Routing/ZeroEntityRouteInterceptor.cs @@ -173,7 +173,7 @@ public class ZeroEntityRouteInterceptor : Interceptor /// /// Get route dependencies for an entity /// - protected async Task> GetDependencies(RoutingContext context, T model) where T : IZeroRouteEntity + protected async Task> GetDependencies(RoutingContext context, T model) where T : ISupportsRouting { string[] ids = new[] { model.Id }; return await context.Session.Query().Where(x => x.Dependencies.ContainsAny(ids)).ToListAsync(); diff --git a/zero.Core/Routing/ZeroRouteProvider.cs b/zero.Core/Routing/ZeroRouteProvider.cs index ca040ba5..b3892db6 100644 --- a/zero.Core/Routing/ZeroRouteProvider.cs +++ b/zero.Core/Routing/ZeroRouteProvider.cs @@ -1,6 +1,6 @@ namespace zero.Routing; -public abstract class ZeroRouteProvider : ZeroRouteProvider, IRouteProvider where T : IZeroRouteEntity +public abstract class ZeroRouteProvider : ZeroRouteProvider, IRouteProvider where T : ISupportsRouting { public ZeroRouteProvider(string alias) : base(alias) { } @@ -11,31 +11,31 @@ public abstract class ZeroRouteProvider : ZeroRouteProvider, IRouteProvider Create(RoutingContext context, T model) => base.Create(context, model); /// - public sealed override Task Create(RoutingContext context, IZeroRouteEntity model) => Create(context, (T)model); + public sealed override Task Create(RoutingContext context, ISupportsRouting model) => Create(context, (T)model); /// public virtual string Key(T model) => model.Hash; /// - public sealed override string Key(IZeroRouteEntity model) => Key((T)model); + public sealed override string Key(ISupportsRouting model) => Key((T)model); /// public virtual Task IsRouteStale(RoutingContext context, T previous, T current) => Task.FromResult(true); /// - public sealed override Task IsRouteStale(RoutingContext context, IZeroRouteEntity previous, IZeroRouteEntity current) => IsRouteStale(context, (T)previous, (T)current); + public sealed override Task IsRouteStale(RoutingContext context, ISupportsRouting previous, ISupportsRouting current) => IsRouteStale(context, (T)previous, (T)current); /// public virtual string Id(T model) => base.Id(model); /// - public sealed override string Id(IZeroRouteEntity model) => base.Id(model); + public sealed override string Id(ISupportsRouting model) => base.Id(model); /// public virtual async Task Find(RoutingContext context, T model) => await context.Session.LoadAsync(Id(model)); /// - public sealed override Task Find(RoutingContext context, IZeroRouteEntity model) => Find(context, (T)model); + public sealed override Task Find(RoutingContext context, ISupportsRouting model) => Find(context, (T)model); /// public virtual async Task> Find(RoutingContext context, params T[] models) @@ -53,9 +53,9 @@ public abstract class ZeroRouteProvider : ZeroRouteProvider, IRouteProvider - public sealed override async Task> Find(RoutingContext context, params IZeroRouteEntity[] models) + public sealed override async Task> Find(RoutingContext context, params ISupportsRouting[] models) { - return (await Find(context, models.Select(x => (T)x).ToArray())).ToDictionary(x => (IZeroRouteEntity)x.Key, x => x.Value); + return (await Find(context, models.Select(x => (T)x).ToArray())).ToDictionary(x => (ISupportsRouting)x.Key, x => x.Value); } } @@ -81,7 +81,7 @@ public abstract class ZeroRouteProvider : IRouteProvider public virtual bool CanHandle(Type type) => false; /// - public virtual Task Create(RoutingContext context, IZeroRouteEntity model) => Task.FromResult(new Route() + public virtual Task Create(RoutingContext context, ISupportsRouting model) => Task.FromResult(new Route() { Id = Id(model), ProviderAlias = Alias, @@ -89,22 +89,22 @@ public abstract class ZeroRouteProvider : IRouteProvider }); /// - public virtual string Key(IZeroRouteEntity model) => model.Hash; + public virtual string Key(ISupportsRouting model) => model.Hash; /// public virtual IAsyncEnumerable Seed(RoutingContext context) => AsyncEnumerable.Empty(); /// - public virtual IAsyncEnumerable SeedOnUpdate(RoutingContext context, T model) where T : IZeroRouteEntity => AsyncEnumerable.Empty(); + public virtual IAsyncEnumerable SeedOnUpdate(RoutingContext context, T model) where T : ISupportsRouting => AsyncEnumerable.Empty(); /// public virtual Task Model(RoutingContext context, RouteResponse response) => Task.FromResult((IRouteModel)new RouteModel() { Route = response.Route }); /// - public virtual Task IsRouteStale(RoutingContext context, IZeroRouteEntity previous, IZeroRouteEntity current) => Task.FromResult(true); + public virtual Task IsRouteStale(RoutingContext context, ISupportsRouting previous, ISupportsRouting current) => Task.FromResult(true); /// - public virtual string Id(IZeroRouteEntity model) => "routes." + Alias + "." + Key(model); + public virtual string Id(ISupportsRouting model) => "routes." + Alias + "." + Key(model); /// public virtual RouteEndpoint Map(RoutingContext context, IRouteModel route) @@ -127,16 +127,16 @@ public abstract class ZeroRouteProvider : IRouteProvider } /// - public virtual async Task Find(RoutingContext context, IZeroRouteEntity model) => await context.Session.LoadAsync(Id(model)); + public virtual async Task Find(RoutingContext context, ISupportsRouting model) => await context.Session.LoadAsync(Id(model)); /// - public virtual async Task> Find(RoutingContext context, params IZeroRouteEntity[] models) + public virtual async Task> Find(RoutingContext context, params ISupportsRouting[] models) { - Dictionary idMap = models.ToDistinctDictionary(x => Id(x), x => x); + Dictionary idMap = models.ToDistinctDictionary(x => Id(x), x => x); Dictionary routes = await context.Session.LoadAsync(idMap.Select(x => x.Key)); - Dictionary result = new(); + Dictionary result = new(); - foreach ((string id, IZeroRouteEntity model) in idMap) + foreach ((string id, ISupportsRouting model) in idMap) { result.Add(model, routes.GetValueOrDefault(id)); } @@ -146,7 +146,7 @@ public abstract class ZeroRouteProvider : IRouteProvider } -public interface IRouteProvider : IRouteProvider where T : IZeroRouteEntity +public interface IRouteProvider : IRouteProvider where T : ISupportsRouting { /// /// Generate unique route key for a model @@ -196,17 +196,17 @@ public interface IRouteProvider /// /// Generate unique route key for a model /// - string Key(IZeroRouteEntity model); + string Key(ISupportsRouting model); /// /// Generate unique ID for a model /// - string Id(IZeroRouteEntity model); + string Id(ISupportsRouting model); /// /// Create route entity from a model /// - Task Create(RoutingContext context, IZeroRouteEntity model); + Task Create(RoutingContext context, ISupportsRouting model); /// /// Get all models which should be provided and handled by this instance @@ -216,7 +216,7 @@ public interface IRouteProvider /// /// Get all models which should be updated when an entity changes /// - IAsyncEnumerable SeedOnUpdate(RoutingContext context, T model) where T : IZeroRouteEntity; + IAsyncEnumerable SeedOnUpdate(RoutingContext context, T model) where T : ISupportsRouting; /// /// Converts a route to a model which is passed to the endpoint @@ -227,7 +227,7 @@ public interface IRouteProvider /// Determines whether the route for previous is stale and needs to be refreshed /// based on comparison with the previous version /// - Task IsRouteStale(RoutingContext context, IZeroRouteEntity previous, IZeroRouteEntity current); + Task IsRouteStale(RoutingContext context, ISupportsRouting previous, ISupportsRouting current); /// /// Map a route model to an endpoint @@ -237,10 +237,10 @@ public interface IRouteProvider /// /// Find a persisted route for an entity /// - Task Find(RoutingContext context, IZeroRouteEntity model); + Task Find(RoutingContext context, ISupportsRouting model); /// /// Find persisted routes for entities /// - Task> Find(RoutingContext context, params IZeroRouteEntity[] models); + Task> Find(RoutingContext context, params ISupportsRouting[] models); } \ No newline at end of file diff --git a/zero.Core/Stores/EntityStoreExtensions.cs b/zero.Core/Stores/EntityStoreExtensions.cs index 3b46e1d3..11f97bc7 100644 --- a/zero.Core/Stores/EntityStoreExtensions.cs +++ b/zero.Core/Stores/EntityStoreExtensions.cs @@ -42,5 +42,5 @@ public static class EntityStoreExtensions /// /// Deletes an entity by Id with all descendents /// - public static async Task> DeleteWithDescendants(this ITreeEntityStore store, string id) where T : ZeroIdEntity, IZeroTreeEntity, new() => await store.DeleteWithDescendants(await store.Load(id)); + public static async Task> DeleteWithDescendants(this ITreeEntityStore store, string id) where T : ZeroIdEntity, ISupportsTrees, new() => await store.DeleteWithDescendants(await store.Load(id)); } \ No newline at end of file diff --git a/zero.Core/Stores/Flavors/FlavorConfig.cs b/zero.Core/Stores/Flavors/FlavorConfig.cs new file mode 100644 index 00000000..be0cd0bc --- /dev/null +++ b/zero.Core/Stores/Flavors/FlavorConfig.cs @@ -0,0 +1,54 @@ +namespace zero.Stores; + +internal class FlavorProviderConfig : FlavorConfig +{ + public FlavorProviderConfig(Type type) : base(type) + { + IsDefault = true; + } +} + +/// +/// A flavor config holds information about a flavor implementation +/// +public class FlavorConfig +{ + /// + /// Whether this is the default flavor + /// + public bool IsDefault { get; internal set; } + + /// + /// Type of the associated entity + /// + public Type FlavorType { get; private set; } + + /// + /// Alias for querying + /// + public string Alias { get; set; } + + /// + /// Name of the flavor + /// + public string Name { get; set; } + + /// + /// Optional description + /// + public string Description { get; set; } + + /// + /// Icon of the flavor + /// + public string Icon { get; set; } + + + public Func Construct { get; set; } + + + public FlavorConfig(Type type) + { + FlavorType = type; + } +} \ No newline at end of file diff --git a/zero.Core/Stores/Flavors/FlavorOptions.cs b/zero.Core/Stores/Flavors/FlavorOptions.cs new file mode 100644 index 00000000..581472ce --- /dev/null +++ b/zero.Core/Stores/Flavors/FlavorOptions.cs @@ -0,0 +1,77 @@ +using System.Collections.Concurrent; + +namespace zero.Pages; + +public class FlavorOptions +{ + readonly ConcurrentDictionary> Flavors = new(); + + + public void Provide(string alias, string name, string description = null, string icon = null) + where TEntity : ISupportsFlavors, new() + { + Provide(_ => new TEntity(), alias, name, description, icon); + } + + + public void Provide(Func construct, string alias, string name, string description = null, string icon = null) + where TEntity : ISupportsFlavors + { + Type type = typeof(TEntity); + + if (Flavors.ContainsKey(type)) + { + throw new KeyNotFoundException($"Already a provider for type '{type.Name}' registered."); + } + + FlavorProviderConfig config = new(type) + { + Alias = alias, + Name = name, + Description = description, + Icon = icon, + Construct = _ => construct(_) + }; + + if (!Flavors.TryAdd(type, new() { config })) + { + throw new Exception($"Could not add flavor provider '{type.Name}'"); + } + } + + + public void Add(string alias, string name, string description = null, string icon = null) + where TEntity : ISupportsFlavors + where TFlavor : TEntity, new() + { + Add(typeof(TEntity), typeof(TFlavor), _ => new TFlavor(), alias, name, description, icon); + } + + + public void Add(Func construct, string alias, string name, string description = null, string icon = null) + where TEntity : ISupportsFlavors + where TFlavor : TEntity, new() + { + Add(typeof(TEntity), typeof(TFlavor), construct, alias, name, description, icon); + } + + + void Add(Type baseEntityType, Type flavorType, Func construct, string alias, string name, string description = null, string icon = null) + { + if (!Flavors.ContainsKey(baseEntityType)) + { + throw new KeyNotFoundException($"No provider for type '{baseEntityType.Name}' found. Use Provide<>() first."); + } + + FlavorConfig config = new(flavorType) + { + Alias = alias, + Name = name, + Description = description, + Icon = icon, + Construct = _ => construct(_) + }; + + Flavors[baseEntityType].Add(config); + } +} diff --git a/zero.Core/Stores/StoreOperations.Tree.cs b/zero.Core/Stores/StoreOperations.Tree.cs index 456886ae..58bafd55 100644 --- a/zero.Core/Stores/StoreOperations.Tree.cs +++ b/zero.Core/Stores/StoreOperations.Tree.cs @@ -8,14 +8,14 @@ namespace zero.Stores; public partial class StoreOperations : IStoreOperations { /// - public virtual Task IsAllowedAsChild(T model, string parentId) where T : ZeroIdEntity, IZeroTreeEntity, new() + public virtual Task IsAllowedAsChild(T model, string parentId) where T : ZeroIdEntity, ISupportsTrees, new() { return Task.FromResult(true); } /// - public async Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, IZeroTreeEntity, new() + public async Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, ISupportsTrees, new() { IRavenQueryable queryable = Session.Query().Where(x => x.ParentId == parentId).Statistics(out QueryStatistics statistics); querySelector ??= x => x.OrderBy(x => x.Sort); @@ -27,7 +27,7 @@ public partial class StoreOperations : IStoreOperations /// public async Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) - where T : ZeroIdEntity, IZeroTreeEntity, new() + where T : ZeroIdEntity, ISupportsTrees, new() where TIndex : AbstractCommonApiForIndexes, new() { IRavenQueryable queryable = Session.Query().Where(x => x.ParentId == parentId).Statistics(out QueryStatistics statistics); @@ -39,7 +39,7 @@ public partial class StoreOperations : IStoreOperations /// - public async Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, IZeroTreeEntity, new() + public async Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, ISupportsTrees, new() { Dictionary items = await Load(sortedIds); uint index = 0; @@ -62,7 +62,7 @@ public partial class StoreOperations : IStoreOperations /// - public async Task> Move(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new() + public async Task> Move(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, new() { T model = await Load(id); T parent = await Load(newParentId); @@ -84,17 +84,17 @@ public partial class StoreOperations : IStoreOperations /// - public Task> Copy(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new() => Copy(id, newParentId, false, isParentAllowed); + public Task> Copy(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, 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); + public Task> CopyWithDescendants(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, 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() + protected async Task> Copy(string id, string newParentId, bool includeDescendants, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, new() { T model = await Load(id); T parent = await Load(newParentId); @@ -153,7 +153,7 @@ public partial class StoreOperations : IStoreOperations /// - public async Task> DeleteWithDescendants(T model) where T : ZeroIdEntity, IZeroTreeEntity, new() + public async Task> DeleteWithDescendants(T model) where T : ZeroIdEntity, ISupportsTrees, new() { List pages = await GetDescendantsAndSelf(model); @@ -170,7 +170,7 @@ public partial class StoreOperations : IStoreOperations /// /// Get an entity with all its descendants /// - async Task> GetDescendantsAndSelf(T model) where T : ZeroIdEntity, IZeroTreeEntity, new() + async Task> GetDescendantsAndSelf(T model) where T : ZeroIdEntity, ISupportsTrees, new() { List items = new() { model }; diff --git a/zero.Core/Stores/StoreOperations.cs b/zero.Core/Stores/StoreOperations.cs index 1527f63e..e3fea465 100644 --- a/zero.Core/Stores/StoreOperations.cs +++ b/zero.Core/Stores/StoreOperations.cs @@ -191,35 +191,35 @@ public interface IStoreOperations /// /// Loads all children for an entity /// - Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, IZeroTreeEntity, new(); + Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, ISupportsTrees, 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(); + Task> LoadChildren(string parentId, int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, ISupportsTrees, new() where TIndex : AbstractCommonApiForIndexes, new(); /// /// Update sorting of entities on a specific level /// - Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, IZeroTreeEntity, new(); + Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, ISupportsTrees, new(); /// /// Move an entity to a new parent /// - Task> Move(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new(); + Task> Move(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, new(); /// /// Copies an entity to a new location /// - Task> Copy(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new(); + Task> Copy(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, new(); /// /// Copies an entity with descendants to a new location /// - Task> CopyWithDescendants(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, IZeroTreeEntity, new(); + Task> CopyWithDescendants(string id, string newParentId, Func> isParentAllowed = null) where T : ZeroIdEntity, ISupportsTrees, new(); /// /// Deletes an entity with all descendents /// - Task> DeleteWithDescendants(T model) where T : ZeroIdEntity, IZeroTreeEntity, new(); + Task> DeleteWithDescendants(T model) where T : ZeroIdEntity, ISupportsTrees, new(); } \ No newline at end of file diff --git a/zero.Core/Stores/StoreOperationsExtensions.cs b/zero.Core/Stores/StoreOperationsExtensions.cs index 7e883617..5039d029 100644 --- a/zero.Core/Stores/StoreOperationsExtensions.cs +++ b/zero.Core/Stores/StoreOperationsExtensions.cs @@ -41,5 +41,5 @@ public static class StoreOperationsExtensions /// /// Deletes an entity by Id with all descendents /// - public static async Task> DeleteWithDescendants(this IStoreOperations ops, string id) where T : ZeroIdEntity, IZeroTreeEntity, new() => await ops.DeleteWithDescendants(await ops.Load(id)); + public static async Task> DeleteWithDescendants(this IStoreOperations ops, string id) where T : ZeroIdEntity, ISupportsTrees, new() => await ops.DeleteWithDescendants(await ops.Load(id)); } \ No newline at end of file diff --git a/zero.Core/Stores/StoresModule.cs b/zero.Core/Stores/StoresModule.cs index 3038640d..5a04e5e3 100644 --- a/zero.Core/Stores/StoresModule.cs +++ b/zero.Core/Stores/StoresModule.cs @@ -10,5 +10,7 @@ public class StoresModule : ZeroModule services.AddScoped(); services.AddScoped(); services.AddSingleton(); + + services.AddOptions(); } } \ No newline at end of file diff --git a/zero.Core/Stores/TreeEntityStore.cs b/zero.Core/Stores/TreeEntityStore.cs index cc68ef1b..680807a9 100644 --- a/zero.Core/Stores/TreeEntityStore.cs +++ b/zero.Core/Stores/TreeEntityStore.cs @@ -3,7 +3,7 @@ using Raven.Client.Documents.Linq; namespace zero.Stores; -public abstract class TreeEntityStore : EntityStore, ITreeEntityStore where T : ZeroIdEntity, IZeroTreeEntity, new() +public abstract class TreeEntityStore : EntityStore, ITreeEntityStore where T : ZeroIdEntity, ISupportsTrees, new() { public TreeEntityStore(IStoreContext collectionContext) : base(collectionContext) { } @@ -60,7 +60,7 @@ public abstract class TreeEntityStore : EntityStore, ITreeEntityStore w -public interface ITreeEntityStore : IEntityStore where T : ZeroIdEntity, IZeroTreeEntity, new() +public interface ITreeEntityStore : IEntityStore where T : ZeroIdEntity, ISupportsTrees, new() { /// /// Determines whether a model is allowed as a child for a new parent.