From 346a6743067beabcafcd07a5ff10069ce0b71f47 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Mon, 29 Nov 2021 18:25:50 +0100 Subject: [PATCH] new API project --- .../Abstractions/PickerProvider.cs | 4 +- zero.Api/ApiRequestHints.cs | 23 +++ zero.Api/Configuration/ApiOptions.cs | 11 ++ zero.Api/Configuration/BackofficeConstants.cs | 11 ++ .../Extensions/IMapperProfileExtensions.cs | 57 ++++++ .../Filters/BackofficeFilterAttribute.cs | 4 +- .../BackofficeJsonSerlializerSettings.cs | 2 +- .../Filters/CanEditAttribute.cs | 2 +- .../DisableBrowserCacheFilterAttribute.cs | 2 +- .../ModelStateValidationFilterAttribute.cs | 2 +- .../Filters/VerifyTokenAttribute.cs | 0 .../Filters/ZeroBackofficeAttribute.cs | 0 zero.Api/Models/BasicModel.cs | 29 +++ zero.Api/Models/DisplayModel.cs | 120 +++++++++++++ .../Models/ListQuery/ListQuery.cs | 2 +- .../Models/ListQuery/ListQueryDisplayType.cs | 2 +- .../Models/ListQuery/ListQueryExtensions.cs | 2 +- .../Models/ListQuery/ListQueryOrderType.cs | 2 +- .../Models/ListQuery/ListQueryRange.cs | 2 +- .../Models/ListQuery/ListSpecificQuery.cs | 2 +- .../Models/PickerModel.cs | 4 +- zero.Api/Models/PickerPreviewModel.cs | 21 +++ zero.Api/Models/SaveModel.cs | 29 +++ .../Models/TreeItem.cs | 2 +- .../Models/TreeItemModifier.cs | 2 +- .../Models/_legacy/ActionCopyModel.cs | 0 .../Models/_legacy/ListItemModel.cs | 0 .../Models/_legacy/ListModel.cs | 0 .../Models/_legacy/LoginModel.cs | 0 .../Models/_legacy/ObsoleteEditModel.cs | 0 .../Models/_legacy/PageEditModel.cs | 0 .../Modules/Countries/CountriesController.cs | 49 ++++-- .../Modules/Countries/CountryPermissions.cs | 2 +- .../Countries/CountryPickerProvider.cs | 2 +- .../zero_Backoffice_Countries_Listing.cs | 2 +- .../Modules/Countries/Maps/CountryBasic.cs | 4 +- .../Modules/Countries/Maps/CountryDisplay.cs | 6 +- .../Countries/Maps/CountryMapperProfile.cs | 49 ++++++ .../Modules/Countries/Maps/CountrySave.cs | 8 + .../Countries/ServiceCollectionExtensions.cs | 20 +++ .../Modules/Pages/PageTreeService.cs | 2 +- .../Modules/Search/BackofficeSearchService.cs | 2 +- .../Modules/Search/SearchEndpoint.cs | 2 +- .../Modules/Search/SearchIndexMap.cs | 2 +- .../Modules/Search/SearchOptions.cs | 2 +- .../Modules/Search/SearchResult.cs | 2 +- .../Modules/Search/zero_Backoffice_Search.cs | 2 +- .../Modules/ServiceCollectionExtensions.cs | 25 +++ zero.Api/Plugin.cs | 60 +++++++ zero.Api/ServiceCollectionExtensions.cs | 30 ++++ zero.Api/Usings.cs | 30 ++++ zero.Api/ZeroApiController.cs | 18 ++ ...ZeroBackofficeControllerModelConvention.cs | 4 +- zero.Api/ZeroBackofficeMvcOptions.cs | 19 ++ zero.Api/zero.Api.csproj | 24 +++ .../Configuration/BackofficeConstants.cs | 2 + .../Configuration/BackofficeMaps.cs | 28 --- .../Configuration/BackofficeOptions.cs | 16 +- .../ZeroBackofficeApiController.cs | 12 -- .../Controllers/ZeroBackofficeController.cs | 10 -- .../ZeroBackofficeControllerExtensions.cs | 71 +------- .../Controllers/ZeroIndexController.cs | 1 + zero.Backoffice/Mapper/MapperExtensions.cs | 17 -- zero.Backoffice/Mapper/ZeroMapper.cs | 16 -- zero.Backoffice/Mapper/ZeroMapperProfile.cs | 8 - zero.Backoffice/Models/BasicModel.cs | 6 - zero.Backoffice/Models/DisplayModel.cs | 61 ------- zero.Backoffice/Models/PickerPreviewModel.cs | 14 -- zero.Backoffice/Models/SaveModel.cs | 6 - .../Countries/Maps/CountryMapperProfile.cs | 11 -- .../Modules/Countries/Maps/CountrySave.cs | 14 -- .../Modules/ServiceCollectionExtensions.cs | 36 ---- zero.Backoffice/Plugin.cs | 21 +-- zero.Backoffice/Usings.cs | 7 +- .../_legacy => }/ZeroBackofficeModel.cs | 0 zero.Backoffice/ZeroBackofficeMvcOptions.cs | 1 + zero.Backoffice/zero.Backoffice.csproj | 11 +- zero.Core/Mapper/MapperExtensions.cs | 50 ++++++ .../Mapper/ServiceCollectionExtensions.cs | 12 ++ zero.Core/Mapper/ZeroMapper.cs | 166 ++++++++++++++++++ zero.Core/Mapper/ZeroMapperContext.cs | 22 +++ zero.Core/Mapper/ZeroMapperProfile.cs | 16 ++ zero.Core/Usings.cs | 1 + zero.Core/ZeroBuilder.cs | 1 + zero.sln | 10 +- 85 files changed, 940 insertions(+), 410 deletions(-) rename {zero.Backoffice => zero.Api}/Abstractions/PickerProvider.cs (97%) create mode 100644 zero.Api/ApiRequestHints.cs create mode 100644 zero.Api/Configuration/ApiOptions.cs create mode 100644 zero.Api/Configuration/BackofficeConstants.cs create mode 100644 zero.Api/Extensions/IMapperProfileExtensions.cs rename {zero.Backoffice/Controllers => zero.Api}/Filters/BackofficeFilterAttribute.cs (83%) rename {zero.Backoffice => zero.Api/Filters}/BackofficeJsonSerlializerSettings.cs (96%) rename {zero.Backoffice/Controllers => zero.Api}/Filters/CanEditAttribute.cs (95%) rename {zero.Backoffice/Controllers => zero.Api}/Filters/DisableBrowserCacheFilterAttribute.cs (96%) rename {zero.Backoffice/Controllers => zero.Api}/Filters/ModelStateValidationFilterAttribute.cs (91%) rename {zero.Backoffice/Controllers => zero.Api}/Filters/VerifyTokenAttribute.cs (100%) rename {zero.Backoffice/Controllers => zero.Api}/Filters/ZeroBackofficeAttribute.cs (100%) create mode 100644 zero.Api/Models/BasicModel.cs create mode 100644 zero.Api/Models/DisplayModel.cs rename {zero.Backoffice => zero.Api}/Models/ListQuery/ListQuery.cs (96%) rename {zero.Backoffice => zero.Api}/Models/ListQuery/ListQueryDisplayType.cs (88%) rename {zero.Backoffice => zero.Api}/Models/ListQuery/ListQueryExtensions.cs (98%) rename {zero.Backoffice => zero.Api}/Models/ListQuery/ListQueryOrderType.cs (59%) rename {zero.Backoffice => zero.Api}/Models/ListQuery/ListQueryRange.cs (86%) rename {zero.Backoffice => zero.Api}/Models/ListQuery/ListSpecificQuery.cs (73%) rename {zero.Backoffice => zero.Api}/Models/PickerModel.cs (63%) create mode 100644 zero.Api/Models/PickerPreviewModel.cs create mode 100644 zero.Api/Models/SaveModel.cs rename {zero.Backoffice => zero.Api}/Models/TreeItem.cs (97%) rename {zero.Backoffice => zero.Api}/Models/TreeItemModifier.cs (92%) rename {zero.Backoffice => zero.Api}/Models/_legacy/ActionCopyModel.cs (100%) rename {zero.Backoffice => zero.Api}/Models/_legacy/ListItemModel.cs (100%) rename {zero.Backoffice => zero.Api}/Models/_legacy/ListModel.cs (100%) rename {zero.Backoffice => zero.Api}/Models/_legacy/LoginModel.cs (100%) rename {zero.Backoffice => zero.Api}/Models/_legacy/ObsoleteEditModel.cs (100%) rename {zero.Backoffice => zero.Api}/Models/_legacy/PageEditModel.cs (100%) rename {zero.Backoffice => zero.Api}/Modules/Countries/CountriesController.cs (61%) rename {zero.Backoffice => zero.Api}/Modules/Countries/CountryPermissions.cs (94%) rename {zero.Backoffice => zero.Api}/Modules/Countries/CountryPickerProvider.cs (88%) rename {zero.Backoffice => zero.Api}/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs (87%) rename {zero.Backoffice => zero.Api}/Modules/Countries/Maps/CountryBasic.cs (59%) rename {zero.Backoffice => zero.Api}/Modules/Countries/Maps/CountryDisplay.cs (51%) create mode 100644 zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs create mode 100644 zero.Api/Modules/Countries/Maps/CountrySave.cs create mode 100644 zero.Api/Modules/Countries/ServiceCollectionExtensions.cs rename {zero.Backoffice => zero.Api}/Modules/Pages/PageTreeService.cs (99%) rename {zero.Backoffice => zero.Api}/Modules/Search/BackofficeSearchService.cs (97%) rename {zero.Backoffice => zero.Api}/Modules/Search/SearchEndpoint.cs (91%) rename {zero.Backoffice => zero.Api}/Modules/Search/SearchIndexMap.cs (98%) rename {zero.Backoffice => zero.Api}/Modules/Search/SearchOptions.cs (84%) rename {zero.Backoffice => zero.Api}/Modules/Search/SearchResult.cs (92%) rename {zero.Backoffice => zero.Api}/Modules/Search/zero_Backoffice_Search.cs (92%) create mode 100644 zero.Api/Modules/ServiceCollectionExtensions.cs create mode 100644 zero.Api/Plugin.cs create mode 100644 zero.Api/ServiceCollectionExtensions.cs create mode 100644 zero.Api/Usings.cs create mode 100644 zero.Api/ZeroApiController.cs rename {zero.Backoffice/Controllers => zero.Api}/ZeroBackofficeControllerModelConvention.cs (89%) create mode 100644 zero.Api/ZeroBackofficeMvcOptions.cs create mode 100644 zero.Api/zero.Api.csproj delete mode 100644 zero.Backoffice/Configuration/BackofficeMaps.cs delete mode 100644 zero.Backoffice/Controllers/ZeroBackofficeApiController.cs delete mode 100644 zero.Backoffice/Controllers/ZeroBackofficeController.cs delete mode 100644 zero.Backoffice/Mapper/MapperExtensions.cs delete mode 100644 zero.Backoffice/Mapper/ZeroMapper.cs delete mode 100644 zero.Backoffice/Mapper/ZeroMapperProfile.cs delete mode 100644 zero.Backoffice/Models/BasicModel.cs delete mode 100644 zero.Backoffice/Models/DisplayModel.cs delete mode 100644 zero.Backoffice/Models/PickerPreviewModel.cs delete mode 100644 zero.Backoffice/Models/SaveModel.cs delete mode 100644 zero.Backoffice/Modules/Countries/Maps/CountryMapperProfile.cs delete mode 100644 zero.Backoffice/Modules/Countries/Maps/CountrySave.cs delete mode 100644 zero.Backoffice/Modules/ServiceCollectionExtensions.cs rename zero.Backoffice/{Models/_legacy => }/ZeroBackofficeModel.cs (100%) create mode 100644 zero.Core/Mapper/MapperExtensions.cs create mode 100644 zero.Core/Mapper/ServiceCollectionExtensions.cs create mode 100644 zero.Core/Mapper/ZeroMapper.cs create mode 100644 zero.Core/Mapper/ZeroMapperContext.cs create mode 100644 zero.Core/Mapper/ZeroMapperProfile.cs diff --git a/zero.Backoffice/Abstractions/PickerProvider.cs b/zero.Api/Abstractions/PickerProvider.cs similarity index 97% rename from zero.Backoffice/Abstractions/PickerProvider.cs rename to zero.Api/Abstractions/PickerProvider.cs index c553c93c..b87667c0 100644 --- a/zero.Backoffice/Abstractions/PickerProvider.cs +++ b/zero.Api/Abstractions/PickerProvider.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Abstractions; +namespace zero.Api.Abstractions; public class PickerProvider : IPickerProvider where T : ZeroIdEntity, new() { @@ -83,7 +83,7 @@ public class PickerProvider : IPickerProvider where T : ZeroIdEntity, new( { return new() { - HasError = true, + //HasError = true, Icon = "fth-alert-circle color-red", Id = id, Name = "@errors.preview.notfound", diff --git a/zero.Api/ApiRequestHints.cs b/zero.Api/ApiRequestHints.cs new file mode 100644 index 00000000..d1533f5b --- /dev/null +++ b/zero.Api/ApiRequestHints.cs @@ -0,0 +1,23 @@ +namespace zero.Api; + +public struct ApiRequestHints +{ + public ApiResponsePreference ResponsePreference { get; set; } +} + + +/// +/// Preference for POST + PUT requests +/// see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#75-standard-request-headers +/// +public enum ApiResponsePreference +{ + /// + /// Returns a minimal repsonse for inserts and updates + /// + Minimal = 0, + /// + /// Returns status as well as model for inserts and updates + /// + Representation = 1 +} \ No newline at end of file diff --git a/zero.Api/Configuration/ApiOptions.cs b/zero.Api/Configuration/ApiOptions.cs new file mode 100644 index 00000000..da7670c2 --- /dev/null +++ b/zero.Api/Configuration/ApiOptions.cs @@ -0,0 +1,11 @@ +using zero.Api.Modules.Search; + +namespace zero.Api.Configuration; + +public class ApiOptions +{ + /// + /// Configure search maps + /// + public SearchOptions Search { get; set; } = new(); +} \ No newline at end of file diff --git a/zero.Api/Configuration/BackofficeConstants.cs b/zero.Api/Configuration/BackofficeConstants.cs new file mode 100644 index 00000000..2fee0432 --- /dev/null +++ b/zero.Api/Configuration/BackofficeConstants.cs @@ -0,0 +1,11 @@ +namespace zero.Api.Configuration; + +public static class BackofficeConstants +{ + public static class HttpErrors + { + public const string NoIdMatchOnUpdate = "The Id as part of the URL does not match the Id of the model"; + + public const string IdNotFound = "Could not find persisted model for the given Id"; + } +} \ No newline at end of file diff --git a/zero.Api/Extensions/IMapperProfileExtensions.cs b/zero.Api/Extensions/IMapperProfileExtensions.cs new file mode 100644 index 00000000..6cb8bfb2 --- /dev/null +++ b/zero.Api/Extensions/IMapperProfileExtensions.cs @@ -0,0 +1,57 @@ +namespace zero.Api.Extensions; + +public static class IMapperProfileExtensions +{ + /// + /// Map data for a zero entity BasicModel + /// + public static void MapBasicData(this IMapperProfile profile, TSource source, TDestination target) + where TSource : ZeroEntity + where TDestination : BasicModel + { + target.Id = source.Id; + target.Name = source.Name; + target.Alias = source.Alias; + target.Key = source.Key; + target.IsActive = source.IsActive; + target.CreatedDate = source.CreatedDate; + } + + + /// + /// Map data for a zero entity DiplayModel + /// + public static void MapDisplayData(this IMapperProfile profile, TSource source, TDestination target) + where TSource : ZeroEntity + where TDestination : DisplayModel + { + target.Id = source.Id; + target.Name = source.Name; + target.Alias = source.Alias; + target.Key = source.Key; + target.IsActive = source.IsActive; + target.CreatedDate = source.CreatedDate; + + target.Sort = source.Sort; + target.Hash = source.Hash; + target.LastModifiedById = source.LastModifiedById; + target.LastModifiedDate = source.LastModifiedDate; + target.CreatedById = source.CreatedById; + target.LanguageId = source.LanguageId; + } + + + /// + /// Map data for a zero entity SaveModel + /// + public static void MapSaveData(this IMapperProfile profile, TSource source, TDestination target) + where TSource : SaveModel + where TDestination : ZeroEntity + { + target.Name = source.Name; + target.Alias = source.Alias; + target.Key = source.Key; + target.Sort = source.Sort; + target.IsActive = source.IsActive; + } +} diff --git a/zero.Backoffice/Controllers/Filters/BackofficeFilterAttribute.cs b/zero.Api/Filters/BackofficeFilterAttribute.cs similarity index 83% rename from zero.Backoffice/Controllers/Filters/BackofficeFilterAttribute.cs rename to zero.Api/Filters/BackofficeFilterAttribute.cs index 18120a4a..370701ad 100644 --- a/zero.Backoffice/Controllers/Filters/BackofficeFilterAttribute.cs +++ b/zero.Api/Filters/BackofficeFilterAttribute.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.Filters; -namespace zero.Backoffice.Controllers; +namespace zero.Api.Controllers; public class BackofficeFilterAttribute : IActionFilter { @@ -11,7 +11,7 @@ public class BackofficeFilterAttribute : IActionFilter { Type type = context.Controller.GetType(); - if (typeof(ZeroBackofficeController).IsAssignableFrom(type)) + if (typeof(ZeroApiController).IsAssignableFrom(type)) { if (context.HttpContext.Request.Query.TryGetValue(SCOPE_KEY, out var scope)) { diff --git a/zero.Backoffice/BackofficeJsonSerlializerSettings.cs b/zero.Api/Filters/BackofficeJsonSerlializerSettings.cs similarity index 96% rename from zero.Backoffice/BackofficeJsonSerlializerSettings.cs rename to zero.Api/Filters/BackofficeJsonSerlializerSettings.cs index 4bde72ef..7fd6eb17 100644 --- a/zero.Backoffice/BackofficeJsonSerlializerSettings.cs +++ b/zero.Api/Filters/BackofficeJsonSerlializerSettings.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; -namespace zero.Backoffice; +namespace zero.Api; internal class BackofficeJsonSerlializerSettings : JsonSerializerSettings { diff --git a/zero.Backoffice/Controllers/Filters/CanEditAttribute.cs b/zero.Api/Filters/CanEditAttribute.cs similarity index 95% rename from zero.Backoffice/Controllers/Filters/CanEditAttribute.cs rename to zero.Api/Filters/CanEditAttribute.cs index df111032..02173ec2 100644 --- a/zero.Backoffice/Controllers/Filters/CanEditAttribute.cs +++ b/zero.Api/Filters/CanEditAttribute.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace zero.Backoffice.Controllers; +namespace zero.Api.Controllers; public class CanEditAttribute : TypeFilterAttribute { diff --git a/zero.Backoffice/Controllers/Filters/DisableBrowserCacheFilterAttribute.cs b/zero.Api/Filters/DisableBrowserCacheFilterAttribute.cs similarity index 96% rename from zero.Backoffice/Controllers/Filters/DisableBrowserCacheFilterAttribute.cs rename to zero.Api/Filters/DisableBrowserCacheFilterAttribute.cs index 367d4828..206e8793 100644 --- a/zero.Backoffice/Controllers/Filters/DisableBrowserCacheFilterAttribute.cs +++ b/zero.Api/Filters/DisableBrowserCacheFilterAttribute.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Net.Http.Headers; -namespace zero.Backoffice.Controllers; +namespace zero.Api.Controllers; /// /// Ensures that the request is not cached by the browser diff --git a/zero.Backoffice/Controllers/Filters/ModelStateValidationFilterAttribute.cs b/zero.Api/Filters/ModelStateValidationFilterAttribute.cs similarity index 91% rename from zero.Backoffice/Controllers/Filters/ModelStateValidationFilterAttribute.cs rename to zero.Api/Filters/ModelStateValidationFilterAttribute.cs index d8788590..210fb1f5 100644 --- a/zero.Backoffice/Controllers/Filters/ModelStateValidationFilterAttribute.cs +++ b/zero.Api/Filters/ModelStateValidationFilterAttribute.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace zero.Backoffice.Controllers; +namespace zero.Api.Controllers; public class ModelStateValidationFilterAttribute : IActionFilter { diff --git a/zero.Backoffice/Controllers/Filters/VerifyTokenAttribute.cs b/zero.Api/Filters/VerifyTokenAttribute.cs similarity index 100% rename from zero.Backoffice/Controllers/Filters/VerifyTokenAttribute.cs rename to zero.Api/Filters/VerifyTokenAttribute.cs diff --git a/zero.Backoffice/Controllers/Filters/ZeroBackofficeAttribute.cs b/zero.Api/Filters/ZeroBackofficeAttribute.cs similarity index 100% rename from zero.Backoffice/Controllers/Filters/ZeroBackofficeAttribute.cs rename to zero.Api/Filters/ZeroBackofficeAttribute.cs diff --git a/zero.Api/Models/BasicModel.cs b/zero.Api/Models/BasicModel.cs new file mode 100644 index 00000000..cc9e6c6e --- /dev/null +++ b/zero.Api/Models/BasicModel.cs @@ -0,0 +1,29 @@ +namespace zero.Api.Models; + +public abstract class BasicModel : ZeroIdEntity where T : ZeroEntity +{ + /// + /// Full name of the entity + /// + public string Name { get; set; } + + /// + /// Alias (non-unique) which can be used in the frontend and URLs + /// + public string Alias { get; set; } + + /// + /// A key which can be used to query this entity in code + /// + public string Key { get; set; } + + /// + /// Whether the entity is visible in the frontend + /// + public bool IsActive { get; set; } + + /// + /// Date of creation + /// + public DateTimeOffset CreatedDate { get; set; } +} \ No newline at end of file diff --git a/zero.Api/Models/DisplayModel.cs b/zero.Api/Models/DisplayModel.cs new file mode 100644 index 00000000..971b9e3f --- /dev/null +++ b/zero.Api/Models/DisplayModel.cs @@ -0,0 +1,120 @@ +namespace zero.Api.Models; + +public abstract class DisplayModel : ZeroIdEntity +{ + /// + /// Meta data + /// + public DisplayModelConfiguration Configuration { get; set; } = new(); + + /// + /// Permissions for this entity + /// + public DisplayModelPermissions Permissions { get; set; } = new(); +} + + +public abstract class DisplayModel : DisplayModel where T : ZeroEntity +{ + /// + /// Full name of the entity + /// + public string Name { get; set; } + + /// + /// Alias (non-unique) which can be used in the frontend and URLs + /// + public string Alias { get; set; } + + /// + /// A key which can be used to query this entity in code + /// + public string Key { get; set; } + + /// + /// Sort order + /// + public uint Sort { get; set; } + + /// + /// Whether the entity is visible in the frontend + /// + public bool IsActive { get; set; } + + /// + /// Unique hash for this entity (primarily used for routing) + /// + public string Hash { get; set; } + + /// + /// Backoffice user who last modified this content + /// + public string LastModifiedById { get; set; } + + /// + /// Date of last modification + /// + public DateTimeOffset LastModifiedDate { get; set; } + + /// + /// Backoffice user who created this content + /// + public string CreatedById { get; set; } + + /// + /// Date of creation + /// + public DateTimeOffset CreatedDate { get; set; } + + /// + /// Language of the entity + /// + public string LanguageId { get; set; } +} + + +public class DisplayModelConfiguration +{ + /// + /// Wehther this entity is application aware + /// + public bool IsAppAware { get; set; } + + /// + /// Whether this entity can be shared across applications (only for IsAppAware=true) + /// + public bool CanBeShared { get; set; } + + public bool IsShared { get; set; } + + /// + /// The change token maps to a database entity which holds ID and collection of the model to edit + /// If these values do not match the entity on save it is rejected + /// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60); + /// + public string Token { get; set; } +} + + +public class DisplayModelPermissions +{ + /// + /// Whether an entity of this type can be created + /// + public bool CanCreate { get; set; } + + /// + /// Whether an entity of this type can be created in the shared app space + /// + public bool CanCreateShared { get; set; } + + /// + /// Whether this entity can be edited or only viewed + /// + public bool CanEdit { get; set; } + + /// + /// Whether this entity can be deleted + /// + public bool CanDelete { get; set; } +} \ No newline at end of file diff --git a/zero.Backoffice/Models/ListQuery/ListQuery.cs b/zero.Api/Models/ListQuery/ListQuery.cs similarity index 96% rename from zero.Backoffice/Models/ListQuery/ListQuery.cs rename to zero.Api/Models/ListQuery/ListQuery.cs index b267b147..fee111ee 100644 --- a/zero.Backoffice/Models/ListQuery/ListQuery.cs +++ b/zero.Api/Models/ListQuery/ListQuery.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; -namespace zero.Backoffice.Models; +namespace zero.Api.Models; public class ListQuery { diff --git a/zero.Backoffice/Models/ListQuery/ListQueryDisplayType.cs b/zero.Api/Models/ListQuery/ListQueryDisplayType.cs similarity index 88% rename from zero.Backoffice/Models/ListQuery/ListQueryDisplayType.cs rename to zero.Api/Models/ListQuery/ListQueryDisplayType.cs index fd06ae63..349cf722 100644 --- a/zero.Backoffice/Models/ListQuery/ListQueryDisplayType.cs +++ b/zero.Api/Models/ListQuery/ListQueryDisplayType.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; public enum ListQueryDisplayType { diff --git a/zero.Backoffice/Models/ListQuery/ListQueryExtensions.cs b/zero.Api/Models/ListQuery/ListQueryExtensions.cs similarity index 98% rename from zero.Backoffice/Models/ListQuery/ListQueryExtensions.cs rename to zero.Api/Models/ListQuery/ListQueryExtensions.cs index 84060ccd..e7c7949f 100644 --- a/zero.Backoffice/Models/ListQuery/ListQueryExtensions.cs +++ b/zero.Api/Models/ListQuery/ListQueryExtensions.cs @@ -2,7 +2,7 @@ using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; -namespace zero.Backoffice.Models; +namespace zero.Api.Models; public static class ListQueryExtensions { diff --git a/zero.Backoffice/Models/ListQuery/ListQueryOrderType.cs b/zero.Api/Models/ListQuery/ListQueryOrderType.cs similarity index 59% rename from zero.Backoffice/Models/ListQuery/ListQueryOrderType.cs rename to zero.Api/Models/ListQuery/ListQueryOrderType.cs index 58ecebb5..b130ca1f 100644 --- a/zero.Backoffice/Models/ListQuery/ListQueryOrderType.cs +++ b/zero.Api/Models/ListQuery/ListQueryOrderType.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; public enum ListQueryOrderType { diff --git a/zero.Backoffice/Models/ListQuery/ListQueryRange.cs b/zero.Api/Models/ListQuery/ListQueryRange.cs similarity index 86% rename from zero.Backoffice/Models/ListQuery/ListQueryRange.cs rename to zero.Api/Models/ListQuery/ListQueryRange.cs index 58e61570..d7d77125 100644 --- a/zero.Backoffice/Models/ListQuery/ListQueryRange.cs +++ b/zero.Api/Models/ListQuery/ListQueryRange.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; public class ListQueryDateRange { diff --git a/zero.Backoffice/Models/ListQuery/ListSpecificQuery.cs b/zero.Api/Models/ListQuery/ListSpecificQuery.cs similarity index 73% rename from zero.Backoffice/Models/ListQuery/ListSpecificQuery.cs rename to zero.Api/Models/ListQuery/ListSpecificQuery.cs index 6ae45225..935e28b6 100644 --- a/zero.Backoffice/Models/ListQuery/ListSpecificQuery.cs +++ b/zero.Api/Models/ListQuery/ListSpecificQuery.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; public interface IListSpecificQuery { } diff --git a/zero.Backoffice/Models/PickerModel.cs b/zero.Api/Models/PickerModel.cs similarity index 63% rename from zero.Backoffice/Models/PickerModel.cs rename to zero.Api/Models/PickerModel.cs index c3e918ed..85bb6611 100644 --- a/zero.Backoffice/Models/PickerModel.cs +++ b/zero.Api/Models/PickerModel.cs @@ -1,6 +1,6 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; -public struct PickerModel +public class PickerModel { public string Id { get; set; } diff --git a/zero.Api/Models/PickerPreviewModel.cs b/zero.Api/Models/PickerPreviewModel.cs new file mode 100644 index 00000000..2ab20829 --- /dev/null +++ b/zero.Api/Models/PickerPreviewModel.cs @@ -0,0 +1,21 @@ +namespace zero.Api.Models; + +public class PickerPreviewModel +{ + public string Id { get; set; } + + public string Icon { get; set; } + + public string Text { get; set; } + + public string Name { get; set; } + + //public static PickerPreviewModel NotFound(string id) => new() + //{ + // HasError = true, + // Icon = "fth-alert-circle color-red", + // Id = id, + // Name = "@errors.preview.notfound", + // Text = "@errors.preview.notfound_text" + //}; +} \ No newline at end of file diff --git a/zero.Api/Models/SaveModel.cs b/zero.Api/Models/SaveModel.cs new file mode 100644 index 00000000..02b10bfa --- /dev/null +++ b/zero.Api/Models/SaveModel.cs @@ -0,0 +1,29 @@ +namespace zero.Api.Models; + +public abstract class SaveModel : ZeroIdEntity where T : ZeroEntity +{ + /// + /// Full name of the entity + /// + public string Name { get; set; } + + /// + /// Alias (non-unique) which can be used in the frontend and URLs + /// + public string Alias { get; set; } + + /// + /// A key which can be used to query this entity in code + /// + public string Key { get; set; } + + /// + /// Sort order + /// + public uint Sort { get; set; } + + /// + /// Whether the entity is visible in the frontend + /// + public bool IsActive { get; set; } +} \ No newline at end of file diff --git a/zero.Backoffice/Models/TreeItem.cs b/zero.Api/Models/TreeItem.cs similarity index 97% rename from zero.Backoffice/Models/TreeItem.cs rename to zero.Api/Models/TreeItem.cs index 082f2fbf..b8b0c652 100644 --- a/zero.Backoffice/Models/TreeItem.cs +++ b/zero.Api/Models/TreeItem.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; /// /// Represents an item in a tree diff --git a/zero.Backoffice/Models/TreeItemModifier.cs b/zero.Api/Models/TreeItemModifier.cs similarity index 92% rename from zero.Backoffice/Models/TreeItemModifier.cs rename to zero.Api/Models/TreeItemModifier.cs index b4ad653b..6a142fdd 100644 --- a/zero.Backoffice/Models/TreeItemModifier.cs +++ b/zero.Api/Models/TreeItemModifier.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Models; +namespace zero.Api.Models; /// /// The modifier displays a small icon (with hover text) next to the main item icon diff --git a/zero.Backoffice/Models/_legacy/ActionCopyModel.cs b/zero.Api/Models/_legacy/ActionCopyModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/ActionCopyModel.cs rename to zero.Api/Models/_legacy/ActionCopyModel.cs diff --git a/zero.Backoffice/Models/_legacy/ListItemModel.cs b/zero.Api/Models/_legacy/ListItemModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/ListItemModel.cs rename to zero.Api/Models/_legacy/ListItemModel.cs diff --git a/zero.Backoffice/Models/_legacy/ListModel.cs b/zero.Api/Models/_legacy/ListModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/ListModel.cs rename to zero.Api/Models/_legacy/ListModel.cs diff --git a/zero.Backoffice/Models/_legacy/LoginModel.cs b/zero.Api/Models/_legacy/LoginModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/LoginModel.cs rename to zero.Api/Models/_legacy/LoginModel.cs diff --git a/zero.Backoffice/Models/_legacy/ObsoleteEditModel.cs b/zero.Api/Models/_legacy/ObsoleteEditModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/ObsoleteEditModel.cs rename to zero.Api/Models/_legacy/ObsoleteEditModel.cs diff --git a/zero.Backoffice/Models/_legacy/PageEditModel.cs b/zero.Api/Models/_legacy/PageEditModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/PageEditModel.cs rename to zero.Api/Models/_legacy/PageEditModel.cs diff --git a/zero.Backoffice/Modules/Countries/CountriesController.cs b/zero.Api/Modules/Countries/CountriesController.cs similarity index 61% rename from zero.Backoffice/Modules/Countries/CountriesController.cs rename to zero.Api/Modules/Countries/CountriesController.cs index 266a853b..f21943da 100644 --- a/zero.Backoffice/Modules/Countries/CountriesController.cs +++ b/zero.Api/Modules/Countries/CountriesController.cs @@ -1,8 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using System.Collections; -namespace zero.Backoffice.Modules.Countries; +namespace zero.Api.Modules.Countries; -public class CountriesController : ZeroBackofficeApiController +public class CountriesController : ZeroApiController { protected ICountryStore Store { get; set; } @@ -18,7 +19,7 @@ public class CountriesController : ZeroBackofficeApiController [HttpGet("empty")] [ZeroAuthorize(CountryPermissions.Create)] - public virtual async Task> Empty() + public virtual async Task> Empty() { Country model = await Store.Empty(); @@ -33,24 +34,26 @@ public class CountriesController : ZeroBackofficeApiController [HttpGet("pick")] [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task> Pick(ListQuery query) + public virtual async Task> Pick([FromQuery] ListQuery query) { query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); - return await Picker.PickFrom(query.Page, query.PageSize, query); + Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); + return Mapper.Map(result); } - [HttpGet("pick")] + [HttpGet("pickpreview")] [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task>> Pick(string[] ids) + public virtual async Task> Pick([FromQuery] string[] ids) { - return await Picker.GetPreviews(ids); + Dictionary model = await Store.Load(ids); + return Mapper.Map(model); } [HttpGet("{id}")] [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task> Get(string id, string changeVector = null) + public virtual async Task> Get(string id, string changeVector = null) { Country model = await Store.Load(id, changeVector); @@ -65,7 +68,7 @@ public class CountriesController : ZeroBackofficeApiController [HttpGet("")] [ZeroAuthorize(CountryPermissions.Read)] - public virtual async Task> Get(ListQuery query) + public virtual async Task> Get([FromQuery] ListQuery query) { query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name); Paged result = await Store.Load(query.Page, query.PageSize, q => q.Filter(query)); @@ -86,7 +89,12 @@ public class CountriesController : ZeroBackofficeApiController return CreatedAtAction(nameof(CountriesController.Get), new { id = model.Id }, mappedResult); } - return result.WithoutModel(); + if (Hints.ResponsePreference == ApiResponsePreference.Minimal) + { + return result.WithoutModel(); + } + + return Mapper.Map(result); } @@ -99,11 +107,24 @@ public class CountriesController : ZeroBackofficeApiController return BadRequest(BackofficeConstants.HttpErrors.NoIdMatchOnUpdate); } - Country model = Mapper.Map(updateModel); - model.Id = id; + Country model = await Store.Load(id); + + if (model == null) + { + return BadRequest(BackofficeConstants.HttpErrors.IdNotFound); + } + + Mapper.Map(updateModel, model); + Result result = await Store.Update(model); - return result.WithoutModel(); + if (Hints.ResponsePreference == ApiResponsePreference.Minimal) + { + return result.WithoutModel(); + } + + // TODO add Preference-Applied header, see https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#76-standard-response-headers + return Mapper.Map(result); } diff --git a/zero.Backoffice/Modules/Countries/CountryPermissions.cs b/zero.Api/Modules/Countries/CountryPermissions.cs similarity index 94% rename from zero.Backoffice/Modules/Countries/CountryPermissions.cs rename to zero.Api/Modules/Countries/CountryPermissions.cs index be6598bb..0d815784 100644 --- a/zero.Backoffice/Modules/Countries/CountryPermissions.cs +++ b/zero.Api/Modules/Countries/CountryPermissions.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Modules.Countries; +namespace zero.Api.Modules.Countries; public class CountryPermissions : PermissionProvider { diff --git a/zero.Backoffice/Modules/Countries/CountryPickerProvider.cs b/zero.Api/Modules/Countries/CountryPickerProvider.cs similarity index 88% rename from zero.Backoffice/Modules/Countries/CountryPickerProvider.cs rename to zero.Api/Modules/Countries/CountryPickerProvider.cs index c62dc77d..962ad412 100644 --- a/zero.Backoffice/Modules/Countries/CountryPickerProvider.cs +++ b/zero.Api/Modules/Countries/CountryPickerProvider.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Modules.Countries; +namespace zero.Api.Modules.Countries; public class CountryPickerProvider : PickerProvider { diff --git a/zero.Backoffice/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs b/zero.Api/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs similarity index 87% rename from zero.Backoffice/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs rename to zero.Api/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs index d915cbfc..7288740a 100644 --- a/zero.Backoffice/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs +++ b/zero.Api/Modules/Countries/Indexes/zero_Backoffice_Countries_Listing.cs @@ -1,6 +1,6 @@ using Raven.Client.Documents.Indexes; -namespace zero.Backoffice.Modules.Countries; +namespace zero.Api.Modules.Countries; public class zero_Backoffice_Countries_Listing : ZeroIndex { diff --git a/zero.Backoffice/Modules/Countries/Maps/CountryBasic.cs b/zero.Api/Modules/Countries/Maps/CountryBasic.cs similarity index 59% rename from zero.Backoffice/Modules/Countries/Maps/CountryBasic.cs rename to zero.Api/Modules/Countries/Maps/CountryBasic.cs index 0d5cbd1a..e04243ac 100644 --- a/zero.Backoffice/Modules/Countries/Maps/CountryBasic.cs +++ b/zero.Api/Modules/Countries/Maps/CountryBasic.cs @@ -1,9 +1,7 @@ -namespace zero.Backoffice.Modules.Countries; +namespace zero.Api.Modules.Countries; public class CountryBasic : BasicModel { - public bool IsActive { get; set; } - public bool IsPreferred { get; set; } public string Code { get; set; } diff --git a/zero.Backoffice/Modules/Countries/Maps/CountryDisplay.cs b/zero.Api/Modules/Countries/Maps/CountryDisplay.cs similarity index 51% rename from zero.Backoffice/Modules/Countries/Maps/CountryDisplay.cs rename to zero.Api/Modules/Countries/Maps/CountryDisplay.cs index f3883c80..449419ea 100644 --- a/zero.Backoffice/Modules/Countries/Maps/CountryDisplay.cs +++ b/zero.Api/Modules/Countries/Maps/CountryDisplay.cs @@ -1,12 +1,8 @@ -namespace zero.Backoffice.Modules.Countries; +namespace zero.Api.Modules.Countries; public class CountryDisplay : DisplayModel { - public string Name { get; set; } - public bool IsPreferred { get; set; } public string Code { get; set; } - - public string LanguageId { get; set; } } \ No newline at end of file diff --git a/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs b/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs new file mode 100644 index 00000000..c6c94902 --- /dev/null +++ b/zero.Api/Modules/Countries/Maps/CountryMapperProfile.cs @@ -0,0 +1,49 @@ +namespace zero.Api.Modules.Countries; + +public class CountryMapperProfile : ZeroMapperProfile +{ + public override void Configure(IZeroMapper mapper) + { + mapper.Define((source, ctx) => new(), Map); + mapper.Define((source, ctx) => new(), Map); + mapper.Define((source, ctx) => new(), Map); + mapper.Define((source, ctx) => new(), Map); + mapper.Define((source, ctx) => new(), Map); + } + + + void Map(Country source, CountryBasic target, IZeroMapperContext ctx) + { + this.MapBasicData(source, target); + target.Code = source.Code; + target.IsPreferred = source.IsPreferred; + } + + void Map(Country source, CountryDisplay target, IZeroMapperContext ctx) + { + this.MapDisplayData(source, target); + target.Code = source.Code; + target.IsPreferred = source.IsPreferred; + } + + void Map(CountrySave source, Country target, IZeroMapperContext ctx) + { + this.MapSaveData(source, target); + target.Code = source.Code; + target.IsPreferred = source.IsPreferred; + } + + void Map(Country source, PickerModel target, IZeroMapperContext ctx) + { + target.Id = source.Id; + target.Name = source.Name; + target.IsActive = source.IsActive; + } + + void Map(Country source, PickerPreviewModel target, IZeroMapperContext ctx) + { + target.Id = source.Id; + target.Name = source.Name; + target.Icon = "flag-" + source.Code.ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/zero.Api/Modules/Countries/Maps/CountrySave.cs b/zero.Api/Modules/Countries/Maps/CountrySave.cs new file mode 100644 index 00000000..68a264a4 --- /dev/null +++ b/zero.Api/Modules/Countries/Maps/CountrySave.cs @@ -0,0 +1,8 @@ +namespace zero.Api.Modules.Countries; + +public class CountrySave : SaveModel +{ + public bool IsPreferred { get; set; } + + public string Code { get; set; } +} \ No newline at end of file diff --git a/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs b/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..450ebe34 --- /dev/null +++ b/zero.Api/Modules/Countries/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Api.Modules.Countries; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddZeroBackofficeCountries(this IServiceCollection services) + { + services.AddScoped, CountryPickerProvider>(); + services.AddSingleton(); + services.AddSingleton(); + + services.Configure(opts => + { + opts.Indexes.Add(); + }); + + return services; + } +} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Pages/PageTreeService.cs b/zero.Api/Modules/Pages/PageTreeService.cs similarity index 99% rename from zero.Backoffice/Modules/Pages/PageTreeService.cs rename to zero.Api/Modules/Pages/PageTreeService.cs index 16d210f2..4ea7cc04 100644 --- a/zero.Backoffice/Modules/Pages/PageTreeService.cs +++ b/zero.Api/Modules/Pages/PageTreeService.cs @@ -2,7 +2,7 @@ using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; -namespace zero.Backoffice.Modules.Pages; +namespace zero.Api.Modules.Pages; public class PageTreeService : IPageTreeService { diff --git a/zero.Backoffice/Modules/Search/BackofficeSearchService.cs b/zero.Api/Modules/Search/BackofficeSearchService.cs similarity index 97% rename from zero.Backoffice/Modules/Search/BackofficeSearchService.cs rename to zero.Api/Modules/Search/BackofficeSearchService.cs index 2511e1b5..842525e0 100644 --- a/zero.Backoffice/Modules/Search/BackofficeSearchService.cs +++ b/zero.Api/Modules/Search/BackofficeSearchService.cs @@ -2,7 +2,7 @@ using Raven.Client.Documents.Queries; using Raven.Client.Documents.Session; -namespace zero.Backoffice.Modules.Search; +namespace zero.Api.Modules.Search; public class BackofficeSearchService : IBackofficeSearchService { diff --git a/zero.Backoffice/Modules/Search/SearchEndpoint.cs b/zero.Api/Modules/Search/SearchEndpoint.cs similarity index 91% rename from zero.Backoffice/Modules/Search/SearchEndpoint.cs rename to zero.Api/Modules/Search/SearchEndpoint.cs index b63042ee..7cefada4 100644 --- a/zero.Backoffice/Modules/Search/SearchEndpoint.cs +++ b/zero.Api/Modules/Search/SearchEndpoint.cs @@ -1,6 +1,6 @@ //using Microsoft.AspNetCore.Mvc; -//namespace zero.Backoffice.Modules; +//namespace zero.Api.Modules; //[ZeroAuthorize] //public class SearchController : BackofficeController diff --git a/zero.Backoffice/Modules/Search/SearchIndexMap.cs b/zero.Api/Modules/Search/SearchIndexMap.cs similarity index 98% rename from zero.Backoffice/Modules/Search/SearchIndexMap.cs rename to zero.Api/Modules/Search/SearchIndexMap.cs index 8ae32291..0f5862fb 100644 --- a/zero.Backoffice/Modules/Search/SearchIndexMap.cs +++ b/zero.Api/Modules/Search/SearchIndexMap.cs @@ -1,6 +1,6 @@ using Raven.Client.Documents; -namespace zero.Backoffice.Modules.Search; +namespace zero.Api.Modules.Search; public class SearchIndexMap { diff --git a/zero.Backoffice/Modules/Search/SearchOptions.cs b/zero.Api/Modules/Search/SearchOptions.cs similarity index 84% rename from zero.Backoffice/Modules/Search/SearchOptions.cs rename to zero.Api/Modules/Search/SearchOptions.cs index 16af622d..510d7375 100644 --- a/zero.Backoffice/Modules/Search/SearchOptions.cs +++ b/zero.Api/Modules/Search/SearchOptions.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Modules.Search; +namespace zero.Api.Modules.Search; public class SearchOptions : List { diff --git a/zero.Backoffice/Modules/Search/SearchResult.cs b/zero.Api/Modules/Search/SearchResult.cs similarity index 92% rename from zero.Backoffice/Modules/Search/SearchResult.cs rename to zero.Api/Modules/Search/SearchResult.cs index d7ee76bd..8723b796 100644 --- a/zero.Backoffice/Modules/Search/SearchResult.cs +++ b/zero.Api/Modules/Search/SearchResult.cs @@ -1,4 +1,4 @@ -namespace zero.Backoffice.Modules.Search; +namespace zero.Api.Modules.Search; public class SearchResult { diff --git a/zero.Backoffice/Modules/Search/zero_Backoffice_Search.cs b/zero.Api/Modules/Search/zero_Backoffice_Search.cs similarity index 92% rename from zero.Backoffice/Modules/Search/zero_Backoffice_Search.cs rename to zero.Api/Modules/Search/zero_Backoffice_Search.cs index 18b1c88f..f9fca171 100644 --- a/zero.Backoffice/Modules/Search/zero_Backoffice_Search.cs +++ b/zero.Api/Modules/Search/zero_Backoffice_Search.cs @@ -1,6 +1,6 @@ using Raven.Client.Documents; -namespace zero.Backoffice.Modules.Search; +namespace zero.Api.Modules.Search; public class zero_Backoffice_Search : ZeroJavascriptIndex { diff --git a/zero.Api/Modules/ServiceCollectionExtensions.cs b/zero.Api/Modules/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..8f9afd4f --- /dev/null +++ b/zero.Api/Modules/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using zero.Api.Modules.Countries; +using zero.Api.Modules.Pages; +using zero.Api.Modules.Search; + +namespace zero.Api.Modules; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddZeroBackofficeModules(this IServiceCollection services, IConfiguration config) + { + services.AddZeroBackofficeCountries(); + + services.AddScoped(); + services.AddScoped(); + + //services.Configure(opts => + //{ + // opts.Indexes.Add(); + //}); + + return services; + } +} \ No newline at end of file diff --git a/zero.Api/Plugin.cs b/zero.Api/Plugin.cs new file mode 100644 index 00000000..fea541a5 --- /dev/null +++ b/zero.Api/Plugin.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace zero.Api; + +public class ZeroApiPlugin : ZeroPlugin +{ + internal Action PostConfigureServices = null; + + + public ZeroApiPlugin() + { + Options.Name = "zero.Api"; + } + + + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddOptions().Bind(configuration.GetSection("Zero:Api")).Configure(ConfigureOptions); + services.TryAddEnumerable(ServiceDescriptor.Transient, ZeroBackofficeMvcOptions>()); + + //Mvc.AddNewtonsoftJson(x => + //{ + // // TODO this shall only be configurated for backoffice controllers + // BackofficeJsonSerlializerSettings.Setup(x.SerializerSettings); + //}); + + services.AddScoped(); + + services.AddZeroBackofficeModules(configuration); + + //services.AddTransient(); + //services.AddTransient(); + + //services.AddScoped(); + + + PostConfigureServices?.Invoke(services, configuration); + } + + + protected void ConfigureOptions(ApiOptions options, IWebHostEnvironment env) + { + options.Search.Enabled = true; + //Map().Display((x, res, opts) => + //{ + // PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias); + // if (pageType != null) + // { + // res.Icon = pageType.Icon; + // } + // res.Url = "/pages/edit/" + x.Id; + //}); + //Map("fth-image"); + } +} \ No newline at end of file diff --git a/zero.Api/ServiceCollectionExtensions.cs b/zero.Api/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..52bf68a1 --- /dev/null +++ b/zero.Api/ServiceCollectionExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Api; + +public static class ServiceCollectionExtensions +{ + public static ZeroBuilder AddApi(this ZeroBuilder builder) where T : ZeroApiPlugin, IZeroPlugin, new() + { + return builder.AddApi(_ => { }); + } + + public static ZeroBuilder AddApi(this ZeroBuilder builder) + { + return builder.AddApi(); + } + + public static ZeroBuilder AddApi(this ZeroBuilder builder, Action options) where T : ZeroApiPlugin, IZeroPlugin, new() + { + return builder.AddPlugin(services => + { + T plugin = new(); + plugin.PostConfigureServices = (services, configuration) => + { + services.Configure(opts => options(opts)); + }; + + return plugin; + }); + } +} \ No newline at end of file diff --git a/zero.Api/Usings.cs b/zero.Api/Usings.cs new file mode 100644 index 00000000..9c23550b --- /dev/null +++ b/zero.Api/Usings.cs @@ -0,0 +1,30 @@ + +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; + +global using zero.Applications; +global using zero.Architecture; +global using zero.Stores; +global using zero.Configuration; +global using zero.Context; +global using zero.Extensions; +global using zero.FileStorage; +global using zero.Identity; +global using zero.Localization; +global using zero.Mapper; +global using zero.Media; +global using zero.Models; +global using zero.Pages; +global using zero.Persistence; +global using zero.Routing; +global using zero.Utils; + +global using zero.Api.Configuration; +global using zero.Api.Controllers; +global using zero.Api.Models; +global using zero.Api.Modules; +global using zero.Api.Abstractions; +global using zero.Api.Extensions; \ No newline at end of file diff --git a/zero.Api/ZeroApiController.cs b/zero.Api/ZeroApiController.cs new file mode 100644 index 00000000..2c73fa7c --- /dev/null +++ b/zero.Api/ZeroApiController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Api.Controllers; + +[ApiController] +[ZeroAuthorize] +[DisableBrowserCache] +//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))] +//[ServiceFilter(typeof(BackofficeFilterAttribute))] +public abstract class ZeroApiController : ControllerBase +{ + IZeroMapper _mapper; + protected IZeroMapper Mapper => _mapper ?? (_mapper = HttpContext?.RequestServices?.GetService()); + + + protected ApiRequestHints Hints { get; set; } = new(); +} diff --git a/zero.Backoffice/Controllers/ZeroBackofficeControllerModelConvention.cs b/zero.Api/ZeroBackofficeControllerModelConvention.cs similarity index 89% rename from zero.Backoffice/Controllers/ZeroBackofficeControllerModelConvention.cs rename to zero.Api/ZeroBackofficeControllerModelConvention.cs index 285cd2c8..08d118c5 100644 --- a/zero.Backoffice/Controllers/ZeroBackofficeControllerModelConvention.cs +++ b/zero.Api/ZeroBackofficeControllerModelConvention.cs @@ -1,13 +1,13 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; -namespace zero.Backoffice.Controllers; +namespace zero.Api.Controllers; public class ZeroBackofficeControllerModelConvention : IApplicationModelConvention { readonly AttributeRouteModel RouteModel; - readonly Type BaseClass = typeof(ZeroBackofficeController); + readonly Type BaseClass = typeof(ZeroApiController); public ZeroBackofficeControllerModelConvention(string backofficePath) diff --git a/zero.Api/ZeroBackofficeMvcOptions.cs b/zero.Api/ZeroBackofficeMvcOptions.cs new file mode 100644 index 00000000..6f76eaf5 --- /dev/null +++ b/zero.Api/ZeroBackofficeMvcOptions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace zero.Api; + +internal class ZeroBackofficeMvcOptions : IConfigureOptions +{ + IZeroOptions Options { get; set; } + + public ZeroBackofficeMvcOptions(IZeroOptions options) + { + Options = options; + } + + public void Configure(MvcOptions options) + { + options.Conventions.Add(new ZeroBackofficeControllerModelConvention(Options.ZeroPath)); + } +} \ No newline at end of file diff --git a/zero.Api/zero.Api.csproj b/zero.Api/zero.Api.csproj new file mode 100644 index 00000000..5bec65ef --- /dev/null +++ b/zero.Api/zero.Api.csproj @@ -0,0 +1,24 @@ + + + + zero.Api + 0.1.0 + preview + net6.0 + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/zero.Backoffice/Configuration/BackofficeConstants.cs b/zero.Backoffice/Configuration/BackofficeConstants.cs index 61c66ea8..06398391 100644 --- a/zero.Backoffice/Configuration/BackofficeConstants.cs +++ b/zero.Backoffice/Configuration/BackofficeConstants.cs @@ -5,5 +5,7 @@ public static class BackofficeConstants public static class HttpErrors { public const string NoIdMatchOnUpdate = "The Id as part of the URL does not match the Id of the model"; + + public const string IdNotFound = "Could not find persisted model for the given Id"; } } \ No newline at end of file diff --git a/zero.Backoffice/Configuration/BackofficeMaps.cs b/zero.Backoffice/Configuration/BackofficeMaps.cs deleted file mode 100644 index a9279bca..00000000 --- a/zero.Backoffice/Configuration/BackofficeMaps.cs +++ /dev/null @@ -1,28 +0,0 @@ -using AutoMapper; - -namespace zero.Backoffice.Configuration; - -/// -/// -/// -public class BackofficeMaps -{ - public MapperConfiguration MapperConfiguration { get; internal set; } - - internal List> Expressions { get; } - - - public void Configure(Action expression) - { - Expressions.Add(expression); - } - - - internal void Run(IMapperConfigurationExpression mapperConfig) - { - foreach (var expression in Expressions) - { - expression(mapperConfig); - } - } -} \ No newline at end of file diff --git a/zero.Backoffice/Configuration/BackofficeOptions.cs b/zero.Backoffice/Configuration/BackofficeOptions.cs index 68c7b096..ed5d84b0 100644 --- a/zero.Backoffice/Configuration/BackofficeOptions.cs +++ b/zero.Backoffice/Configuration/BackofficeOptions.cs @@ -1,8 +1,4 @@ -using AutoMapper; -using System.Linq.Expressions; -using zero.Backoffice.Modules.Search; - -namespace zero.Backoffice.Configuration; +namespace zero.Backoffice.Configuration; public class BackofficeOptions { @@ -21,11 +17,6 @@ public class BackofficeOptions /// public ExternalServicesOptions ExternalServices { get; set; } = new(); - /// - /// Configure search maps - /// - public SearchOptions Search { get; set; } = new(); - /// /// Options for configuring the vite development server /// @@ -40,9 +31,4 @@ public class BackofficeOptions /// Language ISO codes which are supported by the zero backoffice /// public string[] SupportedLanguages { get; internal set; } - - /// - /// Configuration for AutoMapper - /// - public BackofficeMaps Mapper { get; private set; } = new(); } \ No newline at end of file diff --git a/zero.Backoffice/Controllers/ZeroBackofficeApiController.cs b/zero.Backoffice/Controllers/ZeroBackofficeApiController.cs deleted file mode 100644 index b5a35646..00000000 --- a/zero.Backoffice/Controllers/ZeroBackofficeApiController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; - -namespace zero.Backoffice.Controllers; - -[ApiController] -//[ServiceFilter(typeof(ModelStateValidationFilterAttribute))] -//[ServiceFilter(typeof(BackofficeFilterAttribute))] -public abstract class ZeroBackofficeApiController : ZeroBackofficeController -{ - protected IMapper Mapper { get; private set; } -} diff --git a/zero.Backoffice/Controllers/ZeroBackofficeController.cs b/zero.Backoffice/Controllers/ZeroBackofficeController.cs deleted file mode 100644 index 2a516afd..00000000 --- a/zero.Backoffice/Controllers/ZeroBackofficeController.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace zero.Backoffice.Controllers; - -[ZeroAuthorize] -[DisableBrowserCache] -public abstract class ZeroBackofficeController : ControllerBase -{ - -} \ No newline at end of file diff --git a/zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs b/zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs index e93c97ce..e45da612 100644 --- a/zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs +++ b/zero.Backoffice/Controllers/ZeroBackofficeControllerExtensions.cs @@ -1,81 +1,16 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using System.IO; +using zero.Api.Controllers; namespace zero.Backoffice.Controllers; public static class ZeroBackofficeControllerExtensions { - /// - /// Transform entities to a preview list - /// - public static List TransformToPreviewModels(this ZeroBackofficeController controller, Dictionary items, Action transform = null) where T : ZeroIdEntity - { - List previews = new(); - - foreach (var item in items) - { - if (item.Value == null) - { - previews.Add(new PickerPreviewModel() - { - HasError = true, - Icon = "fth-alert-circle color-red", - Id = item.Key, - Name = "@errors.preview.notfound", - Text = "@errors.preview.notfound_text" - }); - continue; - } - - PickerPreviewModel model = new() { Id = item.Value.Id }; - - if (item.Value is ZeroEntity) - { - model.Name = (item.Value as ZeroEntity).Name; - } - - transform?.Invoke(item.Value, model); - previews.Add(model); - } - - return previews; - } - - - /// - /// Transform entities to a select list - /// - public static List TransformToSelectModels(this ZeroBackofficeController controller, IEnumerable enumerable, Action transform = null) where T : ZeroIdEntity - { - List items = new(); - - foreach (T item in enumerable) - { - PickerModel model = new() - { - Id = item.Id - }; - - if (item is ZeroEntity) - { - model.Name = (item as ZeroEntity).Name; - model.IsActive = (item as ZeroEntity).IsActive; - } - - transform?.Invoke(item, model); - - items.Add(model); - } - - return items; - } - - /// /// Provides a file stream for download in the browser /// - public static IActionResult DownloadFile(this ZeroBackofficeController controller, Stream stream, string filename) + public static IActionResult DownloadFile(this ZeroApiController controller, Stream stream, string filename) { if (stream == null) { @@ -102,7 +37,7 @@ public static class ZeroBackofficeControllerExtensions /// /// Provides a file stream to the browser /// - public static IActionResult File(this ZeroBackofficeController controller, FileStorage.FileResult file) + public static IActionResult File(this ZeroApiController controller, FileStorage.FileResult file) { if (file == null) { diff --git a/zero.Backoffice/Controllers/ZeroIndexController.cs b/zero.Backoffice/Controllers/ZeroIndexController.cs index 56d8095d..ad875c96 100644 --- a/zero.Backoffice/Controllers/ZeroIndexController.cs +++ b/zero.Backoffice/Controllers/ZeroIndexController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using zero.Api.Controllers; namespace zero.Backoffice.Controllers; diff --git a/zero.Backoffice/Mapper/MapperExtensions.cs b/zero.Backoffice/Mapper/MapperExtensions.cs deleted file mode 100644 index a998f7c8..00000000 --- a/zero.Backoffice/Mapper/MapperExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using AutoMapper; - -namespace zero.Backoffice.Mapper; - -public static class MapperExtensions -{ - public static Paged Map(this IMapper mapper, Paged source) - { - return source.MapTo(srcItem => mapper.Map(srcItem)); - } - - public static Result Map(this IMapper mapper, Result source) - { - TDestination model = mapper.Map(source.Model); - return source.ConvertTo(model); - } -} diff --git a/zero.Backoffice/Mapper/ZeroMapper.cs b/zero.Backoffice/Mapper/ZeroMapper.cs deleted file mode 100644 index 1a307112..00000000 --- a/zero.Backoffice/Mapper/ZeroMapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace zero.Backoffice.Mapper -{ - public class ZeroMapper : AutoMapper.Mapper - { - public ZeroMapper(AutoMapper.IConfigurationProvider provider) : base(provider) - { - - } - } -} diff --git a/zero.Backoffice/Mapper/ZeroMapperProfile.cs b/zero.Backoffice/Mapper/ZeroMapperProfile.cs deleted file mode 100644 index 32c9c61e..00000000 --- a/zero.Backoffice/Mapper/ZeroMapperProfile.cs +++ /dev/null @@ -1,8 +0,0 @@ -using AutoMapper; - -namespace zero.Backoffice.Mapper; - -public class ZeroMapperProfile : Profile -{ - -} diff --git a/zero.Backoffice/Models/BasicModel.cs b/zero.Backoffice/Models/BasicModel.cs deleted file mode 100644 index ff7774bf..00000000 --- a/zero.Backoffice/Models/BasicModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace zero.Backoffice.Models; - -public abstract class BasicModel : ZeroIdEntity where T : ZeroIdEntity -{ - public string Name { get; set; } -} \ No newline at end of file diff --git a/zero.Backoffice/Models/DisplayModel.cs b/zero.Backoffice/Models/DisplayModel.cs deleted file mode 100644 index 48b2a288..00000000 --- a/zero.Backoffice/Models/DisplayModel.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace zero.Backoffice.Models; - -public abstract class DisplayModel : ZeroIdEntity where T : ZeroIdEntity -{ - /// - /// Meta data - /// - public DisplayModelMeta Meta { get; set; } = new(); - - /// - /// Permissions for this entity - /// - public DisplayModelPermissions Permissions { get; set; } = new(); -} - - -public class DisplayModelMeta -{ - /// - /// Wehther this entity is application aware - /// - public bool IsAppAware { get; set; } - - /// - /// Whether this entity can be shared across applications (only for IsAppAware=true) - /// - public bool CanBeShared { get; set; } - - public bool IsShared { get; set; } - - /// - /// The change token maps to a database entity which holds ID and collection of the model to edit - /// If these values do not match the entity on save it is rejected - /// // TODO expiration expiry session.Advanced.GetMetadataFor(user)[Raven.Client.Constants.Documents.Metadata.Expires] = DateTime.UtcNow.AddMinutes(60); - /// - public string Token { get; set; } -} - - -public class DisplayModelPermissions -{ - /// - /// Whether an entity of this type can be created - /// - public bool CanCreate { get; set; } - - /// - /// Whether an entity of this type can be created in the shared app space - /// - public bool CanCreateShared { get; set; } - - /// - /// Whether this entity can be edited or only viewed - /// - public bool CanEdit { get; set; } - - /// - /// Whether this entity can be deleted - /// - public bool CanDelete { get; set; } -} \ No newline at end of file diff --git a/zero.Backoffice/Models/PickerPreviewModel.cs b/zero.Backoffice/Models/PickerPreviewModel.cs deleted file mode 100644 index a3a6709f..00000000 --- a/zero.Backoffice/Models/PickerPreviewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace zero.Backoffice.Models; - -public struct PickerPreviewModel -{ - public string Id { get; set; } - - public string Icon { get; set; } - - public string Text { get; set; } - - public string Name { get; set; } - - public bool HasError { get; set; } -} \ No newline at end of file diff --git a/zero.Backoffice/Models/SaveModel.cs b/zero.Backoffice/Models/SaveModel.cs deleted file mode 100644 index c8238f53..00000000 --- a/zero.Backoffice/Models/SaveModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace zero.Backoffice.Models; - -public abstract class SaveModel : ZeroIdEntity where T : ZeroIdEntity -{ - -} \ No newline at end of file diff --git a/zero.Backoffice/Modules/Countries/Maps/CountryMapperProfile.cs b/zero.Backoffice/Modules/Countries/Maps/CountryMapperProfile.cs deleted file mode 100644 index 992019dd..00000000 --- a/zero.Backoffice/Modules/Countries/Maps/CountryMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace zero.Backoffice.Modules.Countries; - -public class CountryMapperProfile : ZeroMapperProfile -{ - public CountryMapperProfile() - { - CreateMap(); - CreateMap(); - CreateMap(); - } -} diff --git a/zero.Backoffice/Modules/Countries/Maps/CountrySave.cs b/zero.Backoffice/Modules/Countries/Maps/CountrySave.cs deleted file mode 100644 index 1e24752a..00000000 --- a/zero.Backoffice/Modules/Countries/Maps/CountrySave.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace zero.Backoffice.Modules.Countries; - -public class CountrySave : SaveModel -{ - public string Name { get; set; } - - public bool IsPreferred { get; set; } - - public string Code { get; set; } - - public string LanguageId { get; set; } -} \ No newline at end of file diff --git a/zero.Backoffice/Modules/ServiceCollectionExtensions.cs b/zero.Backoffice/Modules/ServiceCollectionExtensions.cs deleted file mode 100644 index 0cf4512b..00000000 --- a/zero.Backoffice/Modules/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using AutoMapper; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using zero.Backoffice.Modules.Countries; -using zero.Backoffice.Modules.Pages; -using zero.Backoffice.Modules.Search; - -namespace zero.Backoffice.Modules; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddZeroBackofficeModules(this IServiceCollection services, IConfiguration config) - { - services.AddScoped(); - services.AddScoped(); - - services.AddScoped, CountryPickerProvider>(); - services.AddSingleton(); - - services.Configure(opts => - { - opts.Indexes.Add(); - opts.Indexes.Add(); - }); - - services.Configure(opts => - { - opts.Mapper.Configure(maps => - { - maps.AddProfile(); - }); - }); - - return services; - } -} \ No newline at end of file diff --git a/zero.Backoffice/Plugin.cs b/zero.Backoffice/Plugin.cs index e12445ed..0680e9e1 100644 --- a/zero.Backoffice/Plugin.cs +++ b/zero.Backoffice/Plugin.cs @@ -1,12 +1,7 @@ -using AutoMapper; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using System.IO; -using zero.Backoffice.Modules; namespace zero.Backoffice; @@ -25,7 +20,6 @@ public class ZeroBackofficePlugin : ZeroPlugin public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) { services.AddOptions().Bind(configuration.GetSection("Zero:Backoffice")).Configure(ConfigureOptions); - services.TryAddEnumerable(ServiceDescriptor.Transient, ZeroBackofficeMvcOptions>()); services.AddHostedService(); services.AddTransient(); @@ -36,7 +30,6 @@ public class ZeroBackofficePlugin : ZeroPlugin //}); services.AddZeroBackofficeUIComposition(); - services.AddZeroBackofficeModules(configuration); //services.AddTransient(); //services.AddTransient(); @@ -50,7 +43,6 @@ public class ZeroBackofficePlugin : ZeroPlugin protected void ConfigureOptions(BackofficeOptions options, IWebHostEnvironment env) { - options.Search.Enabled = true; options.DevServer.WorkingDirectory = Path.Combine(env.ContentRootPath, "..", "Zero.Web.UI", "App"); options.IconSets.Add(new BackofficeIconSet() @@ -63,16 +55,5 @@ public class ZeroBackofficePlugin : ZeroPlugin options.SupportedLanguages = new string[2] { "en-US", "de-DE" }; options.DefaultLanguage = options.SupportedLanguages[0]; - - //Map().Display((x, res, opts) => - //{ - // PageType pageType = opts.Pages.GetByAlias(x.PageTypeAlias); - // if (pageType != null) - // { - // res.Icon = pageType.Icon; - // } - // res.Url = "/pages/edit/" + x.Id; - //}); - //Map("fth-image"); } } \ No newline at end of file diff --git a/zero.Backoffice/Usings.cs b/zero.Backoffice/Usings.cs index 99cfaef8..a47d0c77 100644 --- a/zero.Backoffice/Usings.cs +++ b/zero.Backoffice/Usings.cs @@ -20,12 +20,9 @@ global using zero.Pages; global using zero.Persistence; global using zero.Routing; global using zero.Utils; +global using zero.Mapper; global using zero.Backoffice.Configuration; global using zero.Backoffice.Controllers; -global using zero.Backoffice.Models; -global using zero.Backoffice.Modules; global using zero.Backoffice.UIComposition; -global using zero.Backoffice.DevServer; -global using zero.Backoffice.Mapper; -global using zero.Backoffice.Abstractions; \ No newline at end of file +global using zero.Backoffice.DevServer; \ No newline at end of file diff --git a/zero.Backoffice/Models/_legacy/ZeroBackofficeModel.cs b/zero.Backoffice/ZeroBackofficeModel.cs similarity index 100% rename from zero.Backoffice/Models/_legacy/ZeroBackofficeModel.cs rename to zero.Backoffice/ZeroBackofficeModel.cs diff --git a/zero.Backoffice/ZeroBackofficeMvcOptions.cs b/zero.Backoffice/ZeroBackofficeMvcOptions.cs index a720074e..ab7a37d5 100644 --- a/zero.Backoffice/ZeroBackofficeMvcOptions.cs +++ b/zero.Backoffice/ZeroBackofficeMvcOptions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using zero.Api.Controllers; namespace zero.Backoffice; diff --git a/zero.Backoffice/zero.Backoffice.csproj b/zero.Backoffice/zero.Backoffice.csproj index 021cdec6..f1f741b3 100644 --- a/zero.Backoffice/zero.Backoffice.csproj +++ b/zero.Backoffice/zero.Backoffice.csproj @@ -1,4 +1,4 @@ - + zero.Backoffice @@ -18,15 +18,8 @@ - - - - - - - - + \ No newline at end of file diff --git a/zero.Core/Mapper/MapperExtensions.cs b/zero.Core/Mapper/MapperExtensions.cs new file mode 100644 index 00000000..66239b95 --- /dev/null +++ b/zero.Core/Mapper/MapperExtensions.cs @@ -0,0 +1,50 @@ +namespace zero.Mapper; + +public static class MapperExtensions +{ + //public static TDestination Register(this IZeroMapper mapper, Action map) where TDestination : class, new() + //{ + + //} + + + //public static TDestination Register(this IZeroMapper mapper, Action map, TDestination destination) + //{ + + //} + + + public static TDestination Map(this IZeroMapper mapper, TSource source, TDestination destination) + { + return mapper.Map(source, typeof(TSource), destination); + } + + public static TDestination Map(this IZeroMapper mapper, TSource source) + { + return mapper.Map(source, typeof(TSource), default); + } + + public static Paged Map(this IZeroMapper mapper, Paged source) + { + return source.MapTo(srcItem => mapper.Map(srcItem)); + } + + public static Result Map(this IZeroMapper mapper, Result source) + { + TDestination model = mapper.Map(source.Model); + return source.ConvertTo(model); + } + + + public static Dictionary Map(this IZeroMapper mapper, Dictionary source) + { + Dictionary model = new(); + + foreach ((string key, TSource sourceItem) in source) + { + model.Add(key, sourceItem == null ? default : mapper.Map(sourceItem)); + } + + return model; + } +} diff --git a/zero.Core/Mapper/ServiceCollectionExtensions.cs b/zero.Core/Mapper/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..35ce62d9 --- /dev/null +++ b/zero.Core/Mapper/ServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Mapper; + +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddZeroMapper(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} \ No newline at end of file diff --git a/zero.Core/Mapper/ZeroMapper.cs b/zero.Core/Mapper/ZeroMapper.cs new file mode 100644 index 00000000..214950d5 --- /dev/null +++ b/zero.Core/Mapper/ZeroMapper.cs @@ -0,0 +1,166 @@ +using System.Collections.Concurrent; + +namespace zero.Mapper; + +public class ZeroMapper : IZeroMapper +{ + /// + /// Concurrent cache for all ctor definitions + /// + protected ConcurrentDictionary>> MapDefinitions { get; private set; } = new(); + + /// + /// Concurrent cache for all constructor definitions + /// + protected ConcurrentDictionary>> ConstructorDefinitions { get; private set; } = new(); + + + public ZeroMapper(IEnumerable profiles) + { + foreach (IMapperProfile profile in profiles) + { + profile.Configure(this); + } + } + + + /// + public void Define(Func ctor, Action map) + { + Type sourceType = typeof(TSource); + Type destinationType = typeof(TDestination); + + var sourceMaps = MapDefinitions.GetOrAdd(sourceType, _ => new()); + var sourceCtors = ConstructorDefinitions.GetOrAdd(sourceType, _ => new()); + + sourceCtors[destinationType] = (source, ctx) => ctor((TSource)source, ctx); + sourceMaps[destinationType] = (source, destination, ctx) => map((TSource)source, (TDestination)destination, ctx); + } + + + /// + public TDestination Map(object source, Type sourceType, TDestination destination = default) + { + if (source == null) + { + return default; + } + + Type destinationType = typeof(TDestination); + ZeroMapperContext mapperContext = new(this); + + var constructor = GetConstructor(sourceType, destinationType); + var map = GetMap(sourceType, destinationType); + + if (constructor != null && map != null) + { + destination ??= (TDestination)constructor(source, mapperContext); + + map(source, destination, mapperContext); + + return destination; + } + + // TODO enumerables + + throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {destinationType.FullName}."); + } + + + protected virtual Func GetConstructor(Type sourceType, Type targetType) + { + if (ConstructorDefinitions.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor)) + { + return ctor; + } + + // we *may* run this more than once but it does not matter + + ctor = null; + foreach (var (stype, sctors) in ConstructorDefinitions) + { + if (!stype.IsAssignableFrom(sourceType)) continue; + if (!sctors.TryGetValue(targetType, out ctor)) continue; + + sourceCtor = sctors; + break; + } + + if (ctor == null) + { + return null; + } + + ConstructorDefinitions.AddOrUpdate(sourceType, sourceCtor, (k, v) => + { + // Add missing constructors + foreach (var c in sourceCtor) + { + if (!v.ContainsKey(c.Key)) + { + v.Add(c.Key, c.Value); + } + } + + return v; + }); + + + return ctor; + } + + protected virtual Action GetMap(Type sourceType, Type targetType) + { + if (MapDefinitions.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map)) + { + return map; + } + + // we *may* run this more than once but it does not matter + + map = null; + foreach (var (stype, smap) in MapDefinitions) + { + if (!stype.IsAssignableFrom(sourceType)) continue; + + // TODO: consider looking for assignable types for target too? + if (!smap.TryGetValue(targetType, out map)) continue; + + sourceMap = smap; + break; + } + + if (map == null) return null; + + if (MapDefinitions.ContainsKey(sourceType)) + { + foreach (var m in sourceMap) + { + if (!MapDefinitions[sourceType].TryGetValue(m.Key, out _)) + { + MapDefinitions[sourceType].Add(m.Key, m.Value); + } + } + } + else + { + MapDefinitions[sourceType] = sourceMap; + } + + return map; + } +} + + +public interface IZeroMapper +{ + /// + /// Register a new mapping + /// + void Define(Func ctor, Action map); + + /// + /// Map a source type to the destination type + /// + TDestination Map(object source, Type sourceType, TDestination destination = default); +} \ No newline at end of file diff --git a/zero.Core/Mapper/ZeroMapperContext.cs b/zero.Core/Mapper/ZeroMapperContext.cs new file mode 100644 index 00000000..31b06070 --- /dev/null +++ b/zero.Core/Mapper/ZeroMapperContext.cs @@ -0,0 +1,22 @@ +namespace zero.Mapper; + +public class ZeroMapperContext : IZeroMapperContext +{ + /// + public IZeroMapper Mapper { get; } + + + public ZeroMapperContext(IZeroMapper mapper) + { + Mapper = mapper; + } +} + + +public interface IZeroMapperContext +{ + /// + /// Access to the runtime mapper + /// + IZeroMapper Mapper { get; } +} diff --git a/zero.Core/Mapper/ZeroMapperProfile.cs b/zero.Core/Mapper/ZeroMapperProfile.cs new file mode 100644 index 00000000..59e4ee24 --- /dev/null +++ b/zero.Core/Mapper/ZeroMapperProfile.cs @@ -0,0 +1,16 @@ +namespace zero.Mapper; + +public abstract class ZeroMapperProfile : IMapperProfile +{ + /// + public abstract void Configure(IZeroMapper mapper); +} + + +public interface IMapperProfile +{ + /// + /// Configure maps for this profile + /// + void Configure(IZeroMapper mapper); +} \ No newline at end of file diff --git a/zero.Core/Usings.cs b/zero.Core/Usings.cs index fb666aa5..9fc55ec8 100644 --- a/zero.Core/Usings.cs +++ b/zero.Core/Usings.cs @@ -16,6 +16,7 @@ global using zero.FileStorage; global using zero.Identity; global using zero.Localization; global using zero.Mails; +global using zero.Mapper; global using zero.Media; global using zero.Models; global using zero.Pages; diff --git a/zero.Core/ZeroBuilder.cs b/zero.Core/ZeroBuilder.cs index 7e71990f..7ae1f161 100644 --- a/zero.Core/ZeroBuilder.cs +++ b/zero.Core/ZeroBuilder.cs @@ -50,6 +50,7 @@ public class ZeroBuilder services.AddZeroRendering(); services.AddZeroRouting(Configuration); services.AddZeroStores(); + services.AddZeroMapper(); //if (Environment.GetEnvironmentVariable("DOTNET_WATCH") == "1") //{ diff --git a/zero.sln b/zero.sln index ec30893f..fca430b9 100644 --- a/zero.sln +++ b/zero.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Commerce", "plugins\zero.Commerce\zero.Commerce.csproj", "{63A8AD15-15F0-4555-BD62-C38B4465FFC1}" EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Backoffice.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "zero.Web.UI", "zero.Backoffice.UI\", "{8CE1A69A-35DB-4748-AE1C-86F5BC2E11D4}" ProjectSection(WebsiteProperties) = preProject TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" Debug.AspNetCompiler.VirtualPath = "/localhost_56763" @@ -43,7 +43,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Backoffice", "zero.Bac EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Web", "zero.Web\zero.Web.csproj", "{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Web", "zero.Web\zero.Web.csproj", "{1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Api", "zero.Api\zero.Api.csproj", "{C0442589-3915-43B5-9F78-2D5F9E8D8C94}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -79,6 +81,10 @@ Global {1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E4EE00E-96BB-4382-AE6D-4AA2B8C600B7}.Release|Any CPU.Build.0 = Release|Any CPU + {C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0442589-3915-43B5-9F78-2D5F9E8D8C94}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE