validation for languages+countries+translations+mails

This commit is contained in:
2022-01-23 18:32:22 +01:00
parent e2e1614fbd
commit 34cafb399b
22 changed files with 182 additions and 132 deletions
@@ -12,8 +12,8 @@
<div class="ui-property-content">
<slot></slot>
<ui-error v-if="field" :field="field" />
</div>
<ui-error v-if="field" :field="field" />
<slot name="after"></slot>
</div>
</template>
@@ -64,9 +64,9 @@
cursor: not-allowed;
}
.ui-error
&:not(.is-vertical) > .ui-error
{
margin-top: 5px;
grid-column: span 2 / auto;
}
}
@@ -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<BackofficeOptions>();
int port = options.DevServer.Port;
+5 -7
View File
@@ -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
<!DOCTYPE html>
<html>
<head>
@@ -25,8 +22,9 @@
<title>zero</title>
</head>
<body class="theme-light theme-rounded">
@Model.Meta.AppLastModifiedDate
<div id="app"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
@* <svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
@Html.Raw(Model.Vue.GetIconSvg())
</svg>
<script id="zeroconfig" type="application/json">
@@ -39,6 +37,6 @@
<environment include="Development">
<script type="module" src="http://localhost:@(Model.Port)/vite/client"></script>
<script type="module" src="http://localhost:@(Model.Port)/app/app.js"></script>
</environment>
</environment>*@
</body>
</html>
+2 -2
View File
@@ -1,4 +1,4 @@
@using zero.Core.Extensions
@*@using zero.Core.Extensions
@inject zero.Core.Options.IZeroOptions Options
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
@@ -27,4 +27,4 @@
</script>
<script src="~/Assets/setup.js" asp-append-version="true"></script>
</body>
</html>
</html>*@
+18
View File
@@ -0,0 +1,18 @@
namespace zero.Backoffice;
internal class ZeroBackofficeModel
{
public ZeroBackofficeMetaInfo Meta { get; set; }
}
internal class ZeroVueManifest
{
}
internal class ZeroVueManifestFile
{
}
+6 -5
View File
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<PackageId>zero.Backoffice</PackageId>
<Version>0.1.0</Version>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>
@@ -17,17 +18,17 @@
<None Remove="Modules_legacy\**" />
</ItemGroup>
<ItemGroup>
<!--<ItemGroup>
<None Remove="Resources\Countries\countries.de-de.json" />
<None Remove="Resources\Countries\countries.en-us.json" />
<None Remove="Resources\Localization\zero.de-de.json" />
<None Remove="Resources\Localization\zero.en-us.json" />
</ItemGroup>
</ItemGroup>-->
<ItemGroup>
<EmbeddedResource Include="Resources\backoffice.tpl.html" />
<Content Include="Resources\Countries\countries.de-de.json">
<!--<Content Include="Resources\Countries\countries.de-de.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Resources\Countries\countries.en-us.json">
@@ -38,7 +39,7 @@
</Content>
<Content Include="Resources\Localization\zero.en-us.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</Content>-->
</ItemGroup>
<ItemGroup>
@@ -145,7 +145,7 @@ public class IntegrationStore : IIntegrationStore
return Result<Integration>.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));
}
}
+2 -1
View File
@@ -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;
+4 -5
View File
@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation.Results;
namespace zero.Localization;
@@ -6,11 +6,10 @@ public class CountryStore : EntityStore<Country>, ICountryStore
{
public CountryStore(IStoreContext context) : base(context) { }
/// <inheritdoc />
protected override void ValidationRules(ZeroValidator<Country> validator)
public override Task<ValidationResult> 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);
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
namespace zero.Localization;
public class CountryValidator : ZeroValidator<Country>
{
public CountryValidator(ZeroValidationContext ctx)
{
RuleFor(x => x.Code).NotEmpty().Length(2).Unique(ctx);
RuleFor(x => x.Name).NotEmpty().Length(2, 120);
}
}
+4 -11
View File
@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation.Results;
namespace zero.Localization;
@@ -6,17 +6,10 @@ public class LanguageStore : EntityStore<Language>, ILanguageStore
{
public LanguageStore(IStoreContext context) : base(context) { }
/// <inheritdoc />
protected override void ValidationRules(ZeroValidator<Language> validator)
public override Task<ValidationResult> 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);
}
}
@@ -0,0 +1,18 @@
using FluentValidation;
namespace zero.Localization;
public class LanguageValidator : ZeroValidator<Language>
{
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");
}
}
+3 -4
View File
@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation.Results;
namespace zero.Localization;
@@ -14,10 +14,9 @@ public class TranslationStore : EntityStore<Translation>, ITranslationStore
}
protected override void ValidationRules(ZeroValidator<Translation> validator)
public override Task<ValidationResult> 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);
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
namespace zero.Localization;
public class TranslationValidator : ZeroValidator<Translation>
{
public TranslationValidator(ZeroValidationContext ctx)
{
RuleFor(x => x.Key).NotEmpty().Length(2, 300).Unique(ctx);
RuleFor(x => x.Value).MaximumLength(10 * 1000);
}
}
+29
View File
@@ -0,0 +1,29 @@
using FluentValidation;
namespace zero.Mails;
public class MailTemplateValidator : ZeroValidator<MailTemplate>
{
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");
}
}
}
+3 -11
View File
@@ -1,4 +1,4 @@
using FluentValidation;
using FluentValidation.Results;
using Raven.Client.Documents;
namespace zero.Mails;
@@ -22,17 +22,9 @@ public class MailTemplatesStore : EntityStore<MailTemplate>, IMailTemplatesStore
/// <inheritdoc />
protected override void ValidationRules(ZeroValidator<MailTemplate> validator)
public override Task<ValidationResult> 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);
}
}
+4 -4
View File
@@ -29,10 +29,10 @@ public class MediaStore : TreeEntityStore<Media>, 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);
//});
}
}
+12 -4
View File
@@ -62,10 +62,10 @@ public abstract class EntityStore<T> : IEntityStore<T> where T : ZeroIdEntity, I
public virtual string GetChangeToken(T model) => Operations.GetChangeToken(model);
/// <inheritdoc />
public virtual Task<Result<T>> Create(T model) => Operations.Create(model, async m => await Validate(m));
public virtual Task<Result<T>> Create(T model) => Operations.Create(model, async (m, ctx) => await Validate(ctx, m));
/// <inheritdoc />
public virtual Task<Result<T>> Update(T model) => Operations.Update(model, async m => await Validate(m));
public virtual Task<Result<T>> Update(T model) => Operations.Update(model, async (m, ctx) => await Validate(ctx, m));
/// <inheritdoc />
public virtual Task<Result<IOrderedEnumerable<T>>> Sort(string[] sortedIds) => Operations.Sort<T>(sortedIds);
@@ -74,13 +74,21 @@ public abstract class EntityStore<T> : IEntityStore<T> where T : ZeroIdEntity, I
public virtual Task<Result<T>> Delete(T model) => Operations.Delete(model);
/// <inheritdoc />
public virtual async Task<ValidationResult> Validate(T model)
public virtual async Task<ValidationResult> Validate(ZeroValidationContext ctx, T model)
{
ZeroValidator<T> validator = new();
ValidationRules(validator);
return await validator.ValidateAsync(model);
}
/// <inheritdoc />
public Task<ValidationResult> Validate(T model) => Validate(new ZeroValidationContext()
{
Context = Context,
Session = Session,
Operation = ValidationOp.Unknown
}, model);
/// <summary>
/// Create rules for validation
@@ -162,7 +170,7 @@ public interface IEntityStore<T> where T : ZeroIdEntity, ISupportsFlavors, ISupp
/// <summary>
/// Validates an entity in this collection
/// </summary>
Task<ValidationResult> Validate(T model);
Task<ValidationResult> Validate(ZeroValidationContext ctx, T model);
/// <summary>
/// Creates an entity with an optional validator
+10 -4
View File
@@ -5,13 +5,13 @@ namespace zero.Stores;
public partial class StoreOperations : IStoreOperations
{
/// <inheritdoc />
public virtual Task<Result<T>> Create<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new() => Save(model, validate);
public virtual Task<Result<T>> Create<T>(T model, Func<T, ZeroValidationContext, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new() => Save(model, validate);
/// <inheritdoc />
public virtual Task<Result<T>> Update<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new() => Save(model, validate);
public virtual Task<Result<T>> Update<T>(T model, Func<T, ZeroValidationContext, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new() => Save(model, validate);
/// <inheritdoc />
protected virtual async Task<Result<T>> Save<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new()
protected virtual async Task<Result<T>> Save<T>(T model, Func<T, ZeroValidationContext, Task<ValidationResult>> 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<T>.Fail(validation);
+2 -2
View File
@@ -189,12 +189,12 @@ public interface IStoreOperations
/// <summary>
/// Creates an entity with an optional validator
/// </summary>
Task<Result<T>> Create<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new();
Task<Result<T>> Create<T>(T model, Func<T, ZeroValidationContext, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new();
/// <summary>
/// Updates an entity with an optional validator
/// </summary>
Task<Result<T>> Update<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new();
Task<Result<T>> Update<T>(T model, Func<T, ZeroValidationContext, Task<ValidationResult>> validate = null) where T : ZeroIdEntity, new();
/// <inheritdoc />
Task<Result<IOrderedEnumerable<T>>> Sort<T>(string[] sortedIds) where T : ZeroIdEntity, ISupportsSorting, new();
+8 -68
View File
@@ -91,65 +91,6 @@ public static class ValidatorExtensions
}
/// <summary>
/// Check if this value is unique within a collection
/// </summary>
public static IRuleBuilderOptions<T, TProperty> Unique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroStore store) where T : ZeroEntity
{
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
{
bool any = await store.Session().Advanced.AsyncDocumentQuery<T>()
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
.WhereEquals(context.PropertyName.ToPascalCaseId(), value)
.AnyAsync(cancellation);
return !any;
}).WithMessage("@errors.forms.not_unique");
}
/// <summary>
/// Check if this value is at least set once to the expected value within a collection
/// </summary>
public static IRuleBuilderOptions<T, TProperty> ExpectAnyUnique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroStore store, TProperty expectedValue) where T : ZeroEntity
{
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
{
return await store.Session().Advanced.AsyncDocumentQuery<T>()
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
.WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue)
.AnyAsync(cancellation);
}).WithMessage("@errors.forms.not_unique_alone");
}
/// <summary>
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
/// </summary>
public static IRuleBuilderOptions<T, string> Exists<T>(this IRuleBuilder<T, string> ruleBuilder, IZeroStore store) where T : ZeroEntity
{
return ruleBuilder.Exists<T, T>(store);
}
/// <summary>
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
/// </summary>
public static IRuleBuilderOptions<T, string> Exists<T, TReference>(this IRuleBuilder<T, string> 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<TReference>(id);
return model != null;
}).WithMessage("@errors.forms.reference_notfound");
}
/// <summary>
/// Validates a culture identifier
/// </summary>
@@ -180,11 +121,11 @@ public static class ValidatorExtensions
/// <summary>
/// Check if this value is unique within a collection
/// </summary>
public static IRuleBuilderOptions<T, TProperty> Unique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity
public static IRuleBuilderOptions<T, TProperty> Unique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity
{
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
{
bool any = await session.Advanced.AsyncDocumentQuery<T>()
bool any = await ctx.Session.Advanced.AsyncDocumentQuery<T>()
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
.WhereEquals(context.PropertyName.ToPascalCaseId(), value)
.AnyAsync(cancellation);
@@ -197,11 +138,11 @@ public static class ValidatorExtensions
/// <summary>
/// Check if this value is at least set once to the expected value within a collection
/// </summary>
public static IRuleBuilderOptions<T, TProperty> ExpectAnyUnique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroDocumentSession session, TProperty expectedValue) where T : ZeroEntity
public static IRuleBuilderOptions<T, TProperty> ExpectAnyUnique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, ZeroValidationContext ctx, TProperty expectedValue) where T : ZeroEntity
{
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
{
return await session.Advanced.AsyncDocumentQuery<T>()
return await ctx.Session.Advanced.AsyncDocumentQuery<T>()
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
.WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue)
.AnyAsync(cancellation);
@@ -212,16 +153,16 @@ public static class ValidatorExtensions
/// <summary>
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
/// </summary>
public static IRuleBuilderOptions<T, string> Exists<T>(this IRuleBuilder<T, string> ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity
public static IRuleBuilderOptions<T, string> Exists<T>(this IRuleBuilder<T, string> ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity
{
return ruleBuilder.Exists<T, T>(session);
return ruleBuilder.Exists<T, T>(ctx);
}
/// <summary>
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
/// </summary>
public static IRuleBuilderOptions<T, string> Exists<T, TReference>(this IRuleBuilder<T, string> ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity where TReference : ZeroEntity
public static IRuleBuilderOptions<T, string> Exists<T, TReference>(this IRuleBuilder<T, string> 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<TReference>(id);
return model != null;
return await ctx.Session.Query<TReference>().AnyAsync(x => x.Id == id);
}).WithMessage("@errors.forms.reference_notfound");
}
}
@@ -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
}