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
@@ -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
}