diff --git a/zero.Backoffice.UI/app/components/ui-property.vue b/zero.Backoffice.UI/app/components/ui-property.vue index 8c43cc03..212ba847 100644 --- a/zero.Backoffice.UI/app/components/ui-property.vue +++ b/zero.Backoffice.UI/app/components/ui-property.vue @@ -12,8 +12,8 @@
-
+ @@ -64,9 +64,9 @@ cursor: not-allowed; } - .ui-error + &:not(.is-vertical) > .ui-error { - margin-top: 5px; + grid-column: span 2 / auto; } } diff --git a/zero.Backoffice/Controllers/ZeroIndexController.cs b/zero.Backoffice/Controllers/ZeroIndexController.cs index 3f7ceebf..3f20309f 100644 --- a/zero.Backoffice/Controllers/ZeroIndexController.cs +++ b/zero.Backoffice/Controllers/ZeroIndexController.cs @@ -36,6 +36,13 @@ public class ZeroIndexController : Controller return RedirectToAction("ZeroBackoffice", "Setup"); } + + + //return View("~/Views/Zero/Index.cshtml", new ZeroBackofficeModel() + //{ + // Meta = GetMetaInfo() + //}); + BackofficeOptions options = Options.For(); int port = options.DevServer.Port; diff --git a/zero.Backoffice/Views/Zero/Index.cshtml b/zero.Backoffice/Views/Zero/Index.cshtml index da6c8558..9bd1e0cf 100644 --- a/zero.Backoffice/Views/Zero/Index.cshtml +++ b/zero.Backoffice/Views/Zero/Index.cshtml @@ -1,9 +1,6 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@model zero.Web.Models.ZeroBackofficeModel -@{ - Layout = null; - string config = await Model.Vue.ConfigAsJson(); -} +@model zero.Backoffice.ZeroBackofficeModel + @@ -25,8 +22,9 @@ zero + @Model.Meta.AppLastModifiedDate
- + @* @Html.Raw(Model.Vue.GetIconSvg()) - \ No newline at end of file +*@ \ No newline at end of file diff --git a/zero.Backoffice/ZeroBackofficeModel.cs b/zero.Backoffice/ZeroBackofficeModel.cs new file mode 100644 index 00000000..75df5874 --- /dev/null +++ b/zero.Backoffice/ZeroBackofficeModel.cs @@ -0,0 +1,18 @@ +namespace zero.Backoffice; + +internal class ZeroBackofficeModel +{ + public ZeroBackofficeMetaInfo Meta { get; set; } +} + + +internal class ZeroVueManifest +{ + +} + + +internal class ZeroVueManifestFile +{ + +} \ No newline at end of file diff --git a/zero.Backoffice/zero.Backoffice.csproj b/zero.Backoffice/zero.Backoffice.csproj index b58d1024..7ff80742 100644 --- a/zero.Backoffice/zero.Backoffice.csproj +++ b/zero.Backoffice/zero.Backoffice.csproj @@ -1,10 +1,11 @@ - + zero.Backoffice 0.1.0 preview net6.0 + true true @@ -17,17 +18,17 @@ - + - + diff --git a/zero.Core/Configuration/Integrations/IntegrationStore.cs b/zero.Core/Configuration/Integrations/IntegrationStore.cs index 48bb952c..88a664c6 100644 --- a/zero.Core/Configuration/Integrations/IntegrationStore.cs +++ b/zero.Core/Configuration/Integrations/IntegrationStore.cs @@ -145,7 +145,7 @@ public class IntegrationStore : IIntegrationStore return Result.Fail("@integration.errors.alreadycreated"); } - return await Operations.Create(model, async x => await Validate(x)); + return await Operations.Create(model, async (x, ctx) => await Validate(x)); } } diff --git a/zero.Core/Identity/RavenUserStore(TUser).cs b/zero.Core/Identity/RavenUserStore(TUser).cs index 86e2bbad..11c16d19 100644 --- a/zero.Core/Identity/RavenUserStore(TUser).cs +++ b/zero.Core/Identity/RavenUserStore(TUser).cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Identity; +using FluentValidation.Results; +using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; using Raven.Client.Documents.Linq; using System.Security.Claims; diff --git a/zero.Core/Localization/CountryStore.cs b/zero.Core/Localization/CountryStore.cs index a4dd5ab3..4b66c357 100644 --- a/zero.Core/Localization/CountryStore.cs +++ b/zero.Core/Localization/CountryStore.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation.Results; namespace zero.Localization; @@ -6,11 +6,10 @@ public class CountryStore : EntityStore, ICountryStore { public CountryStore(IStoreContext context) : base(context) { } - /// - protected override void ValidationRules(ZeroValidator validator) + + public override Task Validate(ZeroValidationContext ctx, Country model) { - validator.RuleFor(x => x.Code).NotEmpty().Length(2).Unique(Session); - validator.RuleFor(x => x.Name).NotEmpty().Length(2, 120); + return new CountryValidator(ctx).ValidateAsync(model); } } diff --git a/zero.Core/Localization/CountryValidator.cs b/zero.Core/Localization/CountryValidator.cs new file mode 100644 index 00000000..520f61fd --- /dev/null +++ b/zero.Core/Localization/CountryValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace zero.Localization; + +public class CountryValidator : ZeroValidator +{ + public CountryValidator(ZeroValidationContext ctx) + { + RuleFor(x => x.Code).NotEmpty().Length(2).Unique(ctx); + RuleFor(x => x.Name).NotEmpty().Length(2, 120); + } +} \ No newline at end of file diff --git a/zero.Core/Localization/LanguageStore.cs b/zero.Core/Localization/LanguageStore.cs index c283b5a0..9a887230 100644 --- a/zero.Core/Localization/LanguageStore.cs +++ b/zero.Core/Localization/LanguageStore.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation.Results; namespace zero.Localization; @@ -6,17 +6,10 @@ public class LanguageStore : EntityStore, ILanguageStore { public LanguageStore(IStoreContext context) : base(context) { } - /// - protected override void ValidationRules(ZeroValidator validator) + + public override Task Validate(ZeroValidationContext ctx, Language model) { - validator.RuleFor(x => x.Name).Length(2, 60); - validator.RuleFor(x => x.Code).Length(2, 10).Culture(); - validator.RuleFor(x => x.IsDefault).Unique(Context.Store).When(x => x.IsDefault).WithMessage("@language.errors.default_unique"); - validator.RuleFor(x => x.IsDefault).ExpectAnyUnique(Context.Store, expectedValue: true).When(x => !x.IsDefault).WithMessage("@language.errors.needs_default"); - validator.RuleFor(x => x.InheritedLanguageId).Must((entity, value) => !entity.Id.Equals(value, StringComparison.InvariantCultureIgnoreCase)).When(x => !x.Id.IsNullOrEmpty()).WithMessage("@language.errors.fallback_invalid"); - validator.RuleFor(x => x.InheritedLanguageId).Equal((string)null).When(x => x.IsDefault).WithMessage("@language.errors.default_no_fallback"); - validator.RuleFor(x => x.InheritedLanguageId).Exists(Context.Store); - validator.RuleFor(x => x.IsOptional).Equal(false).When(x => x.IsDefault).WithMessage("@language.errors.default_not_optional"); + return new LanguageValidator(ctx).ValidateAsync(model); } } diff --git a/zero.Core/Localization/LanguageValidator.cs b/zero.Core/Localization/LanguageValidator.cs new file mode 100644 index 00000000..0c72a1ab --- /dev/null +++ b/zero.Core/Localization/LanguageValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace zero.Localization; + +public class LanguageValidator : ZeroValidator +{ + public LanguageValidator(ZeroValidationContext ctx) + { + RuleFor(x => x.Name).Length(2, 60); + RuleFor(x => x.Code).Length(2, 10).Culture(); + RuleFor(x => x.IsDefault).Unique(ctx).When(x => x.IsDefault).WithMessage("@language.errors.default_unique"); + RuleFor(x => x.IsDefault).ExpectAnyUnique(ctx, expectedValue: true).When(x => !x.IsDefault).WithMessage("@language.errors.needs_default"); + RuleFor(x => x.InheritedLanguageId).Must((entity, value) => !entity.Id.Equals(value, StringComparison.InvariantCultureIgnoreCase)).When(x => !x.Id.IsNullOrEmpty()).WithMessage("@language.errors.fallback_invalid"); + RuleFor(x => x.InheritedLanguageId).Equal((string)null).When(x => x.IsDefault).WithMessage("@language.errors.default_no_fallback"); + RuleFor(x => x.InheritedLanguageId).Exists(ctx); + RuleFor(x => x.IsOptional).Equal(false).When(x => x.IsDefault).WithMessage("@language.errors.default_not_optional"); + } +} \ No newline at end of file diff --git a/zero.Core/Localization/TranslationStore.cs b/zero.Core/Localization/TranslationStore.cs index ff2557cf..a9716d28 100644 --- a/zero.Core/Localization/TranslationStore.cs +++ b/zero.Core/Localization/TranslationStore.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation.Results; namespace zero.Localization; @@ -14,10 +14,9 @@ public class TranslationStore : EntityStore, ITranslationStore } - protected override void ValidationRules(ZeroValidator validator) + public override Task Validate(ZeroValidationContext ctx, Translation model) { - validator.RuleFor(x => x.Key).NotEmpty().Length(2, 300).Unique(Context.Store); - validator.RuleFor(x => x.Value).MaximumLength(10 * 1000); + return new TranslationValidator(ctx).ValidateAsync(model); } } diff --git a/zero.Core/Localization/TranslationValidator.cs b/zero.Core/Localization/TranslationValidator.cs new file mode 100644 index 00000000..1230c6bd --- /dev/null +++ b/zero.Core/Localization/TranslationValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace zero.Localization; + +public class TranslationValidator : ZeroValidator +{ + public TranslationValidator(ZeroValidationContext ctx) + { + RuleFor(x => x.Key).NotEmpty().Length(2, 300).Unique(ctx); + RuleFor(x => x.Value).MaximumLength(10 * 1000); + } +} \ No newline at end of file diff --git a/zero.Core/Mails/MailTemplateValidator.cs b/zero.Core/Mails/MailTemplateValidator.cs new file mode 100644 index 00000000..f72cd166 --- /dev/null +++ b/zero.Core/Mails/MailTemplateValidator.cs @@ -0,0 +1,29 @@ +using FluentValidation; + +namespace zero.Mails; + +public class MailTemplateValidator : ZeroValidator +{ + public MailTemplateValidator(ZeroValidationContext ctx, IMailDispatcher dispatcher) + { + RuleFor(x => x.Name).NotEmpty().Length(2, 80); + RuleFor(x => x.Key).NotEmpty().Unique(ctx); + RuleFor(x => x.Subject).NotEmpty().Length(2, 120); + RuleFor(x => x.Preheader).MaximumLength(240); + + RuleFor(x => x.SenderEmail).Email(); + RuleFor(x => x.SenderName).MaximumLength(120); + + RuleFor(x => x.RecipientEmail).Emails(); + RuleFor(x => x.Cc).Emails(); + RuleFor(x => x.Bcc).Emails(); + + if (dispatcher != null) + { + RuleFor(x => x.SenderEmail).MustAsync(async (value, ct) => + { + return await dispatcher.IsSenderSupported(value); + }).WithMessage("@mailTemplate.errors.senderNotAllowed"); + } + } +} \ No newline at end of file diff --git a/zero.Core/Mails/MailTemplatesStore.cs b/zero.Core/Mails/MailTemplatesStore.cs index 3e54fe7a..2aab401d 100644 --- a/zero.Core/Mails/MailTemplatesStore.cs +++ b/zero.Core/Mails/MailTemplatesStore.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation.Results; using Raven.Client.Documents; namespace zero.Mails; @@ -22,17 +22,9 @@ public class MailTemplatesStore : EntityStore, IMailTemplatesStore /// - protected override void ValidationRules(ZeroValidator validator) + public override Task Validate(ZeroValidationContext ctx, MailTemplate model) { - validator.RuleFor(x => x.SenderEmail).Email(); - - if (Dispatcher != null) - { - validator.RuleFor(x => x.SenderEmail).MustAsync(async (value, ct) => - { - return await Dispatcher.IsSenderSupported(value); - }).WithMessage("@mailTemplate.errors.senderNotAllowed"); - } + return new MailTemplateValidator(ctx, Dispatcher).ValidateAsync(model); } } diff --git a/zero.Core/Media/MediaStore.cs b/zero.Core/Media/MediaStore.cs index fc7e494f..b974d057 100644 --- a/zero.Core/Media/MediaStore.cs +++ b/zero.Core/Media/MediaStore.cs @@ -29,10 +29,10 @@ public class MediaStore : TreeEntityStore, IMediaStore validator.RuleFor(x => x.Name).Length(2, 120); validator.RuleFor(x => x.IsActive).Equal(true); - validator.When(x => x.IsFolder, () => - { - validator.RuleFor(x => x.ParentId).Exists(Context.Store); - }); + //validator.When(x => x.IsFolder, () => + //{ + // validator.RuleFor(x => x.ParentId).Exists(Context.Store); + //}); } } diff --git a/zero.Core/Stores/EntityStore.cs b/zero.Core/Stores/EntityStore.cs index 62ea53b5..4d475cb1 100644 --- a/zero.Core/Stores/EntityStore.cs +++ b/zero.Core/Stores/EntityStore.cs @@ -62,10 +62,10 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, I public virtual string GetChangeToken(T model) => Operations.GetChangeToken(model); /// - public virtual Task> Create(T model) => Operations.Create(model, async m => await Validate(m)); + public virtual Task> Create(T model) => Operations.Create(model, async (m, ctx) => await Validate(ctx, m)); /// - public virtual Task> Update(T model) => Operations.Update(model, async m => await Validate(m)); + public virtual Task> Update(T model) => Operations.Update(model, async (m, ctx) => await Validate(ctx, m)); /// public virtual Task>> Sort(string[] sortedIds) => Operations.Sort(sortedIds); @@ -74,13 +74,21 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, I public virtual Task> Delete(T model) => Operations.Delete(model); /// - public virtual async Task Validate(T model) + public virtual async Task Validate(ZeroValidationContext ctx, T model) { ZeroValidator validator = new(); ValidationRules(validator); return await validator.ValidateAsync(model); } + /// + public Task Validate(T model) => Validate(new ZeroValidationContext() + { + Context = Context, + Session = Session, + Operation = ValidationOp.Unknown + }, model); + /// /// Create rules for validation @@ -162,7 +170,7 @@ public interface IEntityStore where T : ZeroIdEntity, ISupportsFlavors, ISupp /// /// Validates an entity in this collection /// - Task Validate(T model); + Task Validate(ZeroValidationContext ctx, T model); /// /// Creates an entity with an optional validator diff --git a/zero.Core/Stores/StoreOperations.Write.cs b/zero.Core/Stores/StoreOperations.Write.cs index f3d46022..9adcc3d5 100644 --- a/zero.Core/Stores/StoreOperations.Write.cs +++ b/zero.Core/Stores/StoreOperations.Write.cs @@ -5,13 +5,13 @@ namespace zero.Stores; public partial class StoreOperations : IStoreOperations { /// - public virtual Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); + public virtual Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); /// - public virtual Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); + public virtual Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); /// - protected virtual async Task> Save(T model, Func> validate = null) where T : ZeroIdEntity, new() + protected virtual async Task> Save(T model, Func> validate = null) where T : ZeroIdEntity, new() { if (model == null) { @@ -52,7 +52,13 @@ public partial class StoreOperations : IStoreOperations // run validator if (validate != null) { - ValidationResult validation = await validate(model); + ValidationResult validation = await validate(model, new ZeroValidationContext() + { + Context = Context, + Session = Session, + Operation = isUpdate ? ValidationOp.Update : ValidationOp.Create + }); + if (!validation.IsValid) { return Result.Fail(validation); diff --git a/zero.Core/Stores/StoreOperations.cs b/zero.Core/Stores/StoreOperations.cs index 892cfeaf..410ac123 100644 --- a/zero.Core/Stores/StoreOperations.cs +++ b/zero.Core/Stores/StoreOperations.cs @@ -189,12 +189,12 @@ public interface IStoreOperations /// /// Creates an entity with an optional validator /// - Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new(); + Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new(); /// /// Updates an entity with an optional validator /// - Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new(); + Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new(); /// Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, ISupportsSorting, new(); diff --git a/zero.Core/Validation/ValidatorExtensions.cs b/zero.Core/Validation/ValidatorExtensions.cs index 2be1dcc4..3903cb35 100644 --- a/zero.Core/Validation/ValidatorExtensions.cs +++ b/zero.Core/Validation/ValidatorExtensions.cs @@ -91,65 +91,6 @@ public static class ValidatorExtensions } - /// - /// Check if this value is unique within a collection - /// - public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => - { - bool any = await store.Session().Advanced.AsyncDocumentQuery() - .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) - .WhereEquals(context.PropertyName.ToPascalCaseId(), value) - .AnyAsync(cancellation); - - return !any; - }).WithMessage("@errors.forms.not_unique"); - } - - - /// - /// Check if this value is at least set once to the expected value within a collection - /// - public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroStore store, TProperty expectedValue) where T : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => - { - return await store.Session().Advanced.AsyncDocumentQuery() - .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) - .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) - .AnyAsync(cancellation); - }).WithMessage("@errors.forms.not_unique_alone"); - } - - - /// - /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) - /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity - { - return ruleBuilder.Exists(store); - } - - - /// - /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) - /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity where TReference : ZeroEntity - { - return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => - { - if (id.IsNullOrWhiteSpace()) - { - return true; - } - - TReference model = await store.Session().LoadAsync(id); - return model != null; - }).WithMessage("@errors.forms.reference_notfound"); - } - - /// /// Validates a culture identifier /// @@ -180,11 +121,11 @@ public static class ValidatorExtensions /// /// Check if this value is unique within a collection /// - public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity + public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity { return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => { - bool any = await session.Advanced.AsyncDocumentQuery() + bool any = await ctx.Session.Advanced.AsyncDocumentQuery() .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) .WhereEquals(context.PropertyName.ToPascalCaseId(), value) .AnyAsync(cancellation); @@ -197,11 +138,11 @@ public static class ValidatorExtensions /// /// Check if this value is at least set once to the expected value within a collection /// - public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroDocumentSession session, TProperty expectedValue) where T : ZeroEntity + public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx, TProperty expectedValue) where T : ZeroEntity { return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => { - return await session.Advanced.AsyncDocumentQuery() + return await ctx.Session.Advanced.AsyncDocumentQuery() .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) .AnyAsync(cancellation); @@ -212,16 +153,16 @@ public static class ValidatorExtensions /// /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity { - return ruleBuilder.Exists(session); + return ruleBuilder.Exists(ctx); } /// /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity where TReference : ZeroEntity + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity where TReference : ZeroEntity { return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => { @@ -230,8 +171,7 @@ public static class ValidatorExtensions return true; } - TReference model = await session.LoadAsync(id); - return model != null; + return await ctx.Session.Query().AnyAsync(x => x.Id == id); }).WithMessage("@errors.forms.reference_notfound"); } } diff --git a/zero.Core/Validation/ZeroValidationContext.cs b/zero.Core/Validation/ZeroValidationContext.cs new file mode 100644 index 00000000..30f1d17d --- /dev/null +++ b/zero.Core/Validation/ZeroValidationContext.cs @@ -0,0 +1,17 @@ +namespace zero.Validation; + +public class ZeroValidationContext +{ + public IZeroContext Context { get; set; } + + public IZeroDocumentSession Session { get; set; } + + public ValidationOp Operation { get; set; } +} + +public enum ValidationOp +{ + Unknown = 0, + Create = 1, + Update = 2 +} \ No newline at end of file