diff --git a/zero.Core/Extensions/RavenAsyncDocumentQueryExtensions.cs b/zero.Core/Extensions/RavenAsyncDocumentQueryExtensions.cs new file mode 100644 index 00000000..8ba58bca --- /dev/null +++ b/zero.Core/Extensions/RavenAsyncDocumentQueryExtensions.cs @@ -0,0 +1,55 @@ +using Raven.Client.Documents.Session; +using System; +using System.Collections.Generic; +using zero.Core.Api; +using zero.Core.Entities; + +namespace zero.Core.Extensions +{ + public static class RavenAsyncDocumentQueryExtensions + { + static Type _appAwareEntity = typeof(IAppAwareEntity); + + public static IAsyncDocumentQuery Scope(this IAsyncDocumentQuery source, string appId, bool includeShared = true) + { + if (appId.IsNullOrEmpty() || !_appAwareEntity.IsAssignableFrom(typeof(T))) + { + return source; + } + + HashSet ids = new HashSet(); + ids.Add(appId); + + if (includeShared) + { + ids.Add(Constants.Database.SharedAppId); + } + + return source.WhereIn(nameof(IAppAwareEntity.AppId), ids); + } + + + public static IAsyncDocumentQuery Scope(this IAsyncDocumentQuery source, ApiScope scope) + { + if (scope == null || scope.IsShared) + { + return source; + } + + if (scope.AppId.IsNullOrEmpty() || !_appAwareEntity.IsAssignableFrom(typeof(T))) + { + return source; + } + + HashSet ids = new HashSet(); + ids.Add(scope.AppId); + + if (scope.IncludeShared) + { + ids.Add(Constants.Database.SharedAppId); + } + + return source.WhereIn(nameof(IAppAwareEntity.AppId), ids); + } + } +} diff --git a/zero.Core/Extensions/StringExtensions.cs b/zero.Core/Extensions/StringExtensions.cs index e981bff8..18efe918 100644 --- a/zero.Core/Extensions/StringExtensions.cs +++ b/zero.Core/Extensions/StringExtensions.cs @@ -89,6 +89,15 @@ namespace zero.Core.Extensions return input; } + public static string ToPascalCase(this string input) + { + if (!String.IsNullOrEmpty(input) && input.Length > 1) + { + return Char.ToUpperInvariant(input[0]) + input.Substring(1); + } + return input; + } + public static string ToCamelCaseId(this string input) { if (String.IsNullOrEmpty(input)) @@ -105,5 +114,23 @@ namespace zero.Core.Extensions return String.Join(".", parts.Select(x => x.ToCamelCase())); } + + + public static string ToPascalCaseId(this string input) + { + if (String.IsNullOrEmpty(input)) + { + return input; + } + + if (input.Length < 2) + { + return input.ToUpperInvariant(); + } + + string[] parts = input.Split('.'); + + return String.Join(".", parts.Select(x => x.ToPascalCase())); + } } } diff --git a/zero.Core/Extensions/ValidatorExtensions.cs b/zero.Core/Extensions/ValidatorExtensions.cs index 82a7860c..469535a9 100644 --- a/zero.Core/Extensions/ValidatorExtensions.cs +++ b/zero.Core/Extensions/ValidatorExtensions.cs @@ -100,19 +100,21 @@ namespace zero.Core.Extensions /// /// Check if this value is unique within a collection /// - public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IBackofficeStore store, bool includeShared = true) where T : IZeroIdEntity + public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IBackofficeStore store) where T : IZeroIdEntity { return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => { + bool includeShared = typeof(IAppAwareShareableEntity).IsAssignableFrom(typeof(T)); + using IAsyncDocumentSession session = store.Raven.OpenAsyncSession(); - int count = await session.Advanced.AsyncDocumentQuery() - .WhereNotEquals("Id", ((T)entity).Id, true) - .WhereEquals(context.Rule.PropertyName, value, true) - .CountAsync(); + bool any = await session.Advanced.AsyncDocumentQuery() + .Scope(store.AppContext.AppId, includeShared) + .WhereNotEquals(nameof(IZeroIdEntity.Id), entity.Id) + .WhereEquals(context.Rule.PropertyName.ToPascalCaseId(), value) + .AnyAsync(cancellation); - return count < 1; - //.Scope(store.AppContext.AppId, includeShared) + return !any; }).WithMessage("@errors.forms.not_unique"); } } diff --git a/zero.Core/Validation/CountryValidator.cs b/zero.Core/Validation/CountryValidator.cs index 30f8088b..511d2b6a 100644 --- a/zero.Core/Validation/CountryValidator.cs +++ b/zero.Core/Validation/CountryValidator.cs @@ -2,30 +2,14 @@ using zero.Core.Api; using zero.Core.Entities; using zero.Core.Extensions; -using Raven.Client.Documents.Session; -using Raven.Client.Documents; namespace zero.Core.Validation { public class CountryValidator : ZeroValidator { - public CountryValidator(IBackofficeStore store) : base(store) + public CountryValidator(IBackofficeStore store) { - RuleFor(x => x.Code).Length(2); - - RuleFor(x => x.Code).Unique(store); - - //RuleFor(x => x.Code).Query(store, async (query, entity) => - //{ - // return !(await query.Scope(store.AppContext.AppId, true).AnyAsync(x => x.Id != entity.Id && x.Code == value)); - //}); - - //RuleFor(x => x.Code).MustAsync(async (entity, value, ct) => - //{ - // using IAsyncDocumentSession session = Raven.OpenAsyncSession(); - // return !(await session.Query().Scope(store.AppContext.AppId, true).AnyAsync(x => x.Id != entity.Id && x.Code == value)); - //}); - + RuleFor(x => x.Code).Length(2).Unique(store); RuleFor(x => x.Name).Length(2, 120); } } diff --git a/zero.Core/Validation/PropertyValidators/UniqueValidator.cs b/zero.Core/Validation/PropertyValidators/UniqueValidator.cs deleted file mode 100644 index 1969a9c7..00000000 --- a/zero.Core/Validation/PropertyValidators/UniqueValidator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentValidation.Validators; -using System; -using System.Collections.Generic; -using System.Text; - -namespace zero.Core.Validation -{ - public class UniqueValidator : PropertyValidator - { - public UniqueValidator() : base("@errors.forms.not_unique") - { - - } - - protected override bool IsValid(PropertyValidatorContext context) - { - throw new NotImplementedException(); - } - } -} diff --git a/zero.Core/Validation/ZeroValidator.cs b/zero.Core/Validation/ZeroValidator.cs index 1c30d065..832a6f29 100644 --- a/zero.Core/Validation/ZeroValidator.cs +++ b/zero.Core/Validation/ZeroValidator.cs @@ -10,17 +10,6 @@ namespace zero.Core.Validation { public abstract class ZeroValidator : AbstractValidator, IValidator where TImplementation : TInterface { - protected IBackofficeStore Store { get; private set; } - - protected IDocumentStore Raven { get; private set; } - - - public ZeroValidator(IBackofficeStore store) - { - Store = store; - Raven = store.Raven; - } - public ValidationResult Validate(TInterface instance) { if (!(instance is TImplementation)) diff --git a/zero.Web.UI/App/components/forms/form.vue b/zero.Web.UI/App/components/forms/form.vue index 79434a17..0e067d3a 100644 --- a/zero.Web.UI/App/components/forms/form.vue +++ b/zero.Web.UI/App/components/forms/form.vue @@ -169,6 +169,7 @@ { this.setState('success'); this.setDirty(false); + this.setErrors(null); resolve(response); if (response.model && this.route && this.$route.name !== this.route) diff --git a/zero.Web/Resources/Localization/zero.en-us.json b/zero.Web/Resources/Localization/zero.en-us.json index 413a6c2e..9719653f 100644 --- a/zero.Web/Resources/Localization/zero.en-us.json +++ b/zero.Web/Resources/Localization/zero.en-us.json @@ -167,7 +167,7 @@ "emails_invalid": "The email address(es) is/are not valid", "hex_format": "The input is not in a valid HEX #aabbcc format", "url_format": "The input is no well-formed URL", - "not_unique": "The property \"{PropertyName}\" must be unique in this collection" + "not_unique": "The property must be unique in this collection" } },