Browse Source

Feature/validator extension (#508)

* Validator extensions.

* Minor fixes.

* Example validator.
pull/509/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
840764f40b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 107
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs
  2. 47
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs
  3. 23
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs
  4. 68
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs
  5. 49
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  6. 29
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  7. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs
  9. 33
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs
  10. 103
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  11. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  12. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs
  13. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  14. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  15. 85
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  16. 129
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  17. 76
      backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs
  18. 12
      backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs
  19. 11
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  20. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs
  21. 218
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  22. 40
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  23. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs
  24. 72
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  25. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs
  26. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs
  27. 29
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs
  28. 94
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  29. 233
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs
  30. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs
  31. 78
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs
  32. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs
  33. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs
  34. 64
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs
  35. 9
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

107
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidator.cs

@ -0,0 +1,107 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries;
namespace Squidex.Extensions.Validation
{
internal sealed class CompositeUniqueValidator : IValidator
{
private readonly string tag;
private readonly IContentRepository contentRepository;
public CompositeUniqueValidator(string tag, IContentRepository contentRepository)
{
this.tag = tag;
this.contentRepository = contentRepository;
}
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is NamedContentData data)
{
var validateableFields = context.Schema.Fields.Where(IsValidateableField);
var filters = new List<FilterNode<ClrValue>>();
foreach (var field in validateableFields)
{
var fieldValue = TryGetValue(field, data);
if (fieldValue != null)
{
filters.Add(ClrFilter.Eq($"data.{field.Name}.iv", fieldValue));
}
}
if (filters.Count > 0)
{
var filter = ClrFilter.And(filters);
var found = await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter);
if (found.Any(x => x.Id != context.ContentId))
{
addError(Enumerable.Empty<string>(), "A content with the same values already exist.");
}
}
}
}
private static ClrValue? TryGetValue(IRootField field, NamedContentData data)
{
var value = JsonValue.Null;
if (data.TryGetValue(field.Name, out var fieldValue))
{
if (fieldValue.TryGetValue(InvariantPartitioning.Key, out var temp) && temp != null)
{
value = temp;
}
}
switch (field.RawProperties)
{
case BooleanFieldProperties _ when value is JsonBoolean boolean:
return boolean.Value;
case BooleanFieldProperties _ when value is JsonNull _:
return ClrValue.Null;
case NumberFieldProperties _ when value is JsonNumber number:
return number.Value;
case NumberFieldProperties _ when value is JsonNull _:
return ClrValue.Null;
case StringFieldProperties _ when value is JsonString @string:
return @string.Value;
case StringFieldProperties _ when value is JsonNull _:
return ClrValue.Null;
case ReferencesFieldProperties _ when value is JsonArray array && array.FirstOrDefault() is JsonString @string:
return @string.Value;
}
return null;
}
private bool IsValidateableField(IRootField field)
{
return field.Partitioning == Partitioning.Invariant && field.RawProperties.Tags.Contains(tag) &&
(field.RawProperties is BooleanFieldProperties ||
field.RawProperties is NumberFieldProperties ||
field.RawProperties is ReferencesFieldProperties ||
field.RawProperties is StringFieldProperties);
}
}
}

47
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs

@ -0,0 +1,47 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
namespace Squidex.Extensions.Validation
{
public sealed class CompositeUniqueValidatorFactory : IValidatorsFactory
{
private const string Prefix = "unique:";
private readonly IContentRepository contentRepository;
public CompositeUniqueValidatorFactory(IContentRepository contentRepository)
{
Guard.NotNull(contentRepository);
this.contentRepository = contentRepository;
}
public IEnumerable<IValidator> CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator)
{
foreach (var validatorTag in ValidatorTags(context.Schema.Properties.Tags))
{
yield return new CompositeUniqueValidator(validatorTag, contentRepository);
}
}
private static IEnumerable<string> ValidatorTags(IEnumerable<string> tags)
{
foreach (var tag in tags)
{
if (tag.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && tag.Length > Prefix.Length)
{
yield return tag;
}
}
}
}
}

23
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorPlugin.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.Validation
{
public sealed class CompositeUniqueValidatorPlugin : IPlugin
{
public void ConfigureServices(IServiceCollection services, IConfiguration config)
{
services.AddSingletonAs<CompositeUniqueValidatorFactory>()
.As<IValidatorsFactory>();
}
}
}

68
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs

@ -1,68 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public static class ContentValidationExtensions
{
public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidateAsync(data);
foreach (var error in validator.Errors)
{
errors.Add(error);
}
}
public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func<string> message)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidateAsync(data);
if (validator.Errors.Count > 0)
{
throw new ValidationException(message(), validator.Errors.ToList());
}
}
public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidatePartialAsync(data);
foreach (var error in validator.Errors)
{
errors.Add(error);
}
}
public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func<string> message)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidatePartialAsync(data);
if (validator.Errors.Count > 0)
{
throw new ValidationException(message(), validator.Errors.ToList());
}
}
}
}

49
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -22,9 +22,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class ContentValidator
{
private readonly Schema schema;
private readonly PartitionResolver partitionResolver;
private readonly ValidationContext context;
private readonly IEnumerable<IValidatorsFactory> factories;
private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>();
public IReadOnlyCollection<ValidationError> Errors
@ -32,14 +32,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
get { return errors; }
}
public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context)
public ContentValidator(PartitionResolver partitionResolver, ValidationContext context, IEnumerable<IValidatorsFactory> factories)
{
Guard.NotNull(schema);
Guard.NotNull(context);
Guard.NotNull(factories);
Guard.NotNull(partitionResolver);
this.schema = schema;
this.context = context;
this.factories = factories;
this.partitionResolver = partitionResolver;
}
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
errors.Add(new ValidationError(message, pathString));
}
public Task ValidatePartialAsync(NamedContentData data)
public Task ValidateInputPartialAsync(NamedContentData data)
{
Guard.NotNull(data);
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return validator.ValidateAsync(data, context, AddError);
}
public Task ValidateAsync(NamedContentData data)
public Task ValidateInputAsync(NamedContentData data)
{
Guard.NotNull(data);
@ -68,11 +68,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return validator.ValidateAsync(data, context, AddError);
}
public Task ValidateContentAsync(NamedContentData data)
{
Guard.NotNull(data);
var validator = new AggregateValidator(CreateContentValidators());
return validator.ValidateAsync(data, context, AddError);
}
private IValidator CreateSchemaValidator(bool isPartial)
{
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(schema.Fields.Count);
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(context.Schema.Fields.Count);
foreach (var field in schema.Fields)
foreach (var field in context.Schema.Fields)
{
fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial));
}
@ -84,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
var partitioning = partitionResolver(field.Partitioning);
var fieldValidator = field.CreateValidator();
var fieldValidator = CreateFieldValidator(field);
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var partitionKey in partitioning.AllKeys)
@ -97,9 +106,29 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
var typeName = partitioning.ToString()!;
return new AggregateValidator(
field.CreateBagValidator()
CreateFieldValidators(field)
.Union(Enumerable.Repeat(
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, typeName), 1)));
}
private IValidator CreateFieldValidator(IField field)
{
return new FieldValidator(CreateValueValidators(field), field);
}
private IEnumerable<IValidator> CreateContentValidators()
{
return factories.SelectMany(x => x.CreateContentValidators(context, CreateFieldValidator));
}
private IEnumerable<IValidator> CreateValueValidators(IField field)
{
return factories.SelectMany(x => x.CreateValueValidators(context, field, CreateFieldValidator));
}
private IEnumerable<IValidator> CreateFieldValidators(IField field)
{
return factories.SelectMany(x => x.CreateFieldValidators(context, field, CreateFieldValidator));
}
}
}

29
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs → backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs

@ -15,19 +15,22 @@ using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class FieldValueValidatorsFactory : IFieldVisitor<IEnumerable<IValidator>>
internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumerable<IValidator>>
{
private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory();
private readonly FieldValidatorFactory createFieldValidator;
private FieldValueValidatorsFactory()
private DefaultFieldValueValidatorsFactory(FieldValidatorFactory createFieldValidator)
{
this.createFieldValidator = createFieldValidator;
}
public static IEnumerable<IValidator> CreateValidators(IField field)
public static IEnumerable<IValidator> CreateValidators(IField field, FieldValidatorFactory createFieldValidator)
{
Guard.NotNull(field);
return field.Accept(Instance);
var visitor = new DefaultFieldValueValidatorsFactory(createFieldValidator);
return field.Accept(visitor);
}
public IEnumerable<IValidator> Visit(IArrayField field)
@ -41,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
foreach (var nestedField in field.Fields)
{
nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator());
nestedSchema[nestedField.Name] = (false, createFieldValidator(nestedField));
}
yield return new CollectionItemValidator(new ObjectValidator<IJsonValue>(nestedSchema, false, "field"));
@ -58,8 +61,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
yield return new UniqueValuesValidator<Guid>();
}
yield return new AssetsValidator(field.Properties);
}
public IEnumerable<IValidator> Visit(IField<BooleanFieldProperties> field)
@ -115,11 +116,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues);
}
if (field.Properties.IsUnique)
{
yield return new UniqueValidator();
}
}
public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> field)
@ -133,8 +129,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
yield return new UniqueValuesValidator<Guid>();
}
yield return new ReferencesValidator(field.Properties.SchemaIds);
}
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field)
@ -158,11 +152,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues);
}
if (field.Properties.IsUnique)
{
yield return new UniqueValidator();
}
}
public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> field)

19
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs → backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs

@ -5,27 +5,26 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public static class Extensions
public sealed class DefaultValidatorsFactory : IValidatorsFactory
{
public static FieldValidator CreateValidator(this IField field)
public IEnumerable<IValidator> CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
{
return new FieldValidator(CreateValueValidators(field), field);
if (field is IField<UIFieldProperties>)
{
yield return NoValueValidator.Instance;
}
}
private static IEnumerable<IValidator> CreateValueValidators(IField field)
public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
{
return FieldValueValidatorsFactory.CreateValidators(field);
}
public static IEnumerable<IValidator> CreateBagValidator(this IField field)
{
return FieldBagValidatorsFactory.CreateValidators(field);
return DefaultFieldValueValidatorsFactory.CreateValidators(field, createFieldValidator);
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs → backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidator.cs

@ -8,7 +8,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public delegate void AddError(IEnumerable<string> path, string message);

33
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public delegate IValidator FieldValidatorFactory(IField field);
public interface IValidatorsFactory
{
IEnumerable<IValidator> CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
{
yield break;
}
IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
{
yield break;
}
IEnumerable<IValidator> CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator)
{
yield break;
}
}
}

103
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs

@ -6,85 +6,55 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckContents(Guid schemaId, FilterNode<ClrValue> filter);
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckContentsByIds(HashSet<Guid> ids);
public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids);
public sealed class ValidationContext
{
private readonly Guid contentId;
private readonly Guid schemaId;
private readonly CheckContents checkContent;
private readonly CheckContentsByIds checkContentByIds;
private readonly CheckAssets checkAsset;
private readonly ImmutableQueue<string> propertyPath;
public ImmutableQueue<string> Path
{
get { return propertyPath; }
}
public ImmutableQueue<string> Path { get; }
public Guid ContentId
{
get { return contentId; }
}
public NamedId<Guid> AppId { get; }
public Guid SchemaId
{
get { return schemaId; }
}
public NamedId<Guid> SchemaId { get; }
public Schema Schema { get; }
public Guid ContentId { get; }
public bool IsOptional { get; }
public ValidationMode Mode { get; }
public ValidationContext(
NamedId<Guid> appId,
NamedId<Guid> schemaId,
Schema schema,
Guid contentId,
Guid schemaId,
CheckContents checkContent,
CheckContentsByIds checkContentsByIds,
CheckAssets checkAsset,
ValidationMode mode = ValidationMode.Default)
: this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue<string>.Empty, false, mode)
: this(appId, schemaId, schema, contentId, ImmutableQueue<string>.Empty, false, mode)
{
}
private ValidationContext(
NamedId<Guid> appId,
NamedId<Guid> schemaId,
Schema schema,
Guid contentId,
Guid schemaId,
CheckContents checkContent,
CheckContentsByIds checkContentByIds,
CheckAssets checkAsset,
ImmutableQueue<string> propertyPath,
ImmutableQueue<string> path,
bool isOptional,
ValidationMode mode = ValidationMode.Default)
{
Guard.NotNull(checkAsset);
Guard.NotNull(checkContent);
Guard.NotNull(checkContentByIds);
this.propertyPath = propertyPath;
this.checkContent = checkContent;
this.checkContentByIds = checkContentByIds;
this.checkAsset = checkAsset;
this.contentId = contentId;
this.schemaId = schemaId;
AppId = appId;
ContentId = contentId;
IsOptional = isOptional;
Mode = mode;
Path = path;
IsOptional = isOptional;
Schema = schema;
SchemaId = schemaId;
}
public ValidationContext Optimized(bool isOptimized = true)
@ -96,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return this;
}
return Clone(propertyPath, IsOptional, mode);
return Clone(Path, IsOptional, mode);
}
public ValidationContext Optional(bool isOptional)
@ -106,38 +76,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return this;
}
return Clone(propertyPath, isOptional, Mode);
return Clone(Path, isOptional, Mode);
}
public ValidationContext Nested(string property)
{
return Clone(propertyPath.Enqueue(property), IsOptional, Mode);
return Clone(Path.Enqueue(property), IsOptional, Mode);
}
private ValidationContext Clone(ImmutableQueue<string> path, bool isOptional, ValidationMode mode)
{
return new ValidationContext(
contentId,
schemaId,
checkContent,
checkContentByIds,
checkAsset,
path, isOptional, mode);
}
public Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> GetContentIdsAsync(HashSet<Guid> ids)
{
return checkContentByIds(ids);
}
public Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> GetContentIdsAsync(Guid schemaId, FilterNode<ClrValue> filter)
{
return checkContent(schemaId, filter);
}
public Task<IReadOnlyList<IAssetInfo>> GetAssetInfosAsync(IEnumerable<Guid> assetId)
{
return checkAsset(assetId);
return new ValidationContext(AppId, SchemaId, Schema, ContentId, path, isOptional, mode);
}
}
}

12
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs

@ -15,13 +15,21 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids);
public sealed class AssetsValidator : IValidator
{
private readonly AssetsFieldProperties properties;
private readonly CheckAssets checkAssets;
public AssetsValidator(AssetsFieldProperties properties)
public AssetsValidator(AssetsFieldProperties properties, CheckAssets checkAssets)
{
Guard.NotNull(properties);
Guard.NotNull(checkAssets);
this.properties = properties;
this.checkAssets = checkAssets;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
@ -33,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (value is ICollection<Guid> assetIds && assetIds.Count > 0)
{
var assets = await context.GetAssetInfosAsync(assetIds);
var assets = await checkAssets(assetIds);
var index = 0;
foreach (var assetId in assetIds)

3
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs

@ -8,6 +8,7 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
@ -19,6 +20,8 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
public PatternValidator(string pattern, string? errorMessage = null)
{
Guard.NotNullOrEmpty(pattern);
this.errorMessage = errorMessage;
regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout);

12
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs

@ -9,16 +9,24 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckContentsByIds(HashSet<Guid> ids);
public sealed class ReferencesValidator : IValidator
{
private readonly IEnumerable<Guid>? schemaIds;
private readonly CheckContentsByIds checkReferences;
public ReferencesValidator(IEnumerable<Guid>? schemaIds)
public ReferencesValidator(IEnumerable<Guid>? schemaIds, CheckContentsByIds checkReferences)
{
Guard.NotNull(checkReferences);
this.schemaIds = schemaIds;
this.checkReferences = checkReferences;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
@ -30,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (value is ICollection<Guid> contentIds)
{
var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet());
var foundIds = await checkReferences(contentIds.ToHashSet());
foreach (var id in contentIds)
{

12
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -12,8 +13,17 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckUniqueness(FilterNode<ClrValue> filter);
public sealed class UniqueValidator : IValidator
{
private readonly CheckUniqueness checkUniqueness;
public UniqueValidator(CheckUniqueness checkUniqueness)
{
this.checkUniqueness = checkUniqueness;
}
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (context.Mode == ValidationMode.Optimized)
@ -38,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (filter != null)
{
var found = await context.GetContentIdsAsync(context.SchemaId, filter);
var found = await checkUniqueness(filter);
if (found.Any(x => x.Id != context.ContentId))
{

85
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -10,10 +10,8 @@ using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
@ -28,33 +26,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentDomainObject : LogSnapshotDomainObject<ContentState>
{
private readonly IAppProvider appProvider;
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly IScriptEngine scriptEngine;
private readonly IContentWorkflow contentWorkflow;
private readonly ContentOperationContext context;
public ContentDomainObject(
IStore<Guid> store,
ISemanticLog log,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository)
public ContentDomainObject(IStore<Guid> store, IContentWorkflow contentWorkflow, ContentOperationContext context, ISemanticLog log)
: base(store, log)
{
Guard.NotNull(appProvider);
Guard.NotNull(scriptEngine);
Guard.NotNull(assetRepository);
Guard.NotNull(context);
Guard.NotNull(contentWorkflow);
Guard.NotNull(contentRepository);
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
this.context = context;
}
public override Task<object?> ExecuteAsync(IAggregateCommand command)
@ -66,15 +48,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
case CreateContent createContent:
return CreateReturnAsync(createContent, async c =>
{
var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, c, () => "Failed to create content.");
await LoadContext(c.AppId, c.SchemaId, c, () => "Failed to create content.", c.OptimizeValidation);
await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c);
await GuardContent.CanCreate(context.Schema, contentWorkflow, c);
var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
var status = await contentWorkflow.GetInitialStatusAsync(context.Schema);
if (!c.DoNotValidate)
{
await context.ValidateInputAsync(c.Data);
}
if (!c.DoNotScript)
{
c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create,
c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Create,
new ScriptContext
{
Operation = "Create",
@ -84,16 +71,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
});
}
await ctx.GenerateDefaultValuesAsync(c.Data);
await context.GenerateDefaultValuesAsync(c.Data);
if (!c.DoNotValidate)
{
await ctx.ValidateAsync(c.Data, c.OptimizeValidation);
await context.ValidateContentAsync(c.Data);
}
if (c.Publish)
{
await ctx.ExecuteScriptAsync(s => s.Change,
await context.ExecuteScriptAsync(s => s.Change,
new ScriptContext
{
Operation = "Published",
@ -111,11 +98,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
case CreateContentDraft createContentDraft:
return UpdateReturnAsync(createContentDraft, async c =>
{
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to create draft.");
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to create draft.");
GuardContent.CanCreateDraft(c, ctx.Schema, Snapshot);
GuardContent.CanCreateDraft(c, context.Schema, Snapshot);
var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
var status = await contentWorkflow.GetInitialStatusAsync(context.Schema);
CreateDraft(c, status);
@ -125,9 +112,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContentDraft deleteContentDraft:
return UpdateReturnAsync(deleteContentDraft, async c =>
{
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete draft.");
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to delete draft.");
GuardContent.CanDeleteDraft(c, ctx.Schema, Snapshot);
GuardContent.CanDeleteDraft(c, context.Schema, Snapshot);
DeleteDraft(c);
@ -155,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
try
{
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to change content.");
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to change content.");
await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c);
await GuardContent.CanChangeStatus(context.Schema, Snapshot, contentWorkflow, c);
if (c.DueTime.HasValue)
{
@ -167,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var change = GetChange(c);
await ctx.ExecuteScriptAsync(s => s.Change,
await context.ExecuteScriptAsync(s => s.Change,
new ScriptContext
{
Operation = change.ToString(),
@ -197,11 +184,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c =>
{
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, c, () => "Failed to delete content.");
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c, () => "Failed to delete content.");
GuardContent.CanDelete(ctx.Schema, c);
GuardContent.CanDelete(context.Schema, c);
await ctx.ExecuteScriptAsync(s => s.Delete,
await context.ExecuteScriptAsync(s => s.Delete,
new ScriptContext
{
Operation = "Delete",
@ -226,18 +213,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!currentData!.Equals(newData))
{
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, command, () => "Failed to update content.");
await LoadContext(Snapshot.AppId, Snapshot.SchemaId, command, () => "Failed to update content.");
if (partial)
{
await ctx.ValidatePartialAsync(command.Data, false);
await context.ValidateInputPartialAsync(command.Data);
}
else
{
await ctx.ValidateAsync(command.Data, false);
await context.ValidateInputAsync(command.Data);
}
newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update,
newData = await context.ExecuteScriptAndTransformAsync(s => s.Update,
new ScriptContext
{
Operation = "Create",
@ -247,6 +234,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
StatusOld = default
});
await context.ValidateContentAsync(newData);
Update(command, newData);
}
@ -337,13 +326,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private async Task<ContentOperationContext> CreateContext(Guid appId, Guid schemaId, ContentCommand command, Func<string> message)
private Task LoadContext(NamedId<Guid> appId, NamedId<Guid> schemaId, ContentCommand command, Func<string> message, bool optimized = false)
{
var operationContext =
await ContentOperationContext.CreateAsync(appId, schemaId, command,
appProvider, assetRepository, contentRepository, scriptEngine, message);
return operationContext;
return context.LoadAsync(appId, schemaId, command, message, optimized);
}
}
}

129
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.DefaultValues;
@ -14,86 +15,100 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
#pragma warning disable IDE0016 // Use 'throw' expression
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentOperationContext
{
private IContentRepository contentRepository;
private IAssetRepository assetRepository;
private IScriptEngine scriptEngine;
private ISchemaEntity schemaEntity;
private IAppEntity appEntity;
private readonly IScriptEngine scriptEngine;
private readonly IAppProvider appProvider;
private readonly IEnumerable<IValidatorsFactory> factories;
private ISchemaEntity schema;
private IAppEntity app;
private ContentCommand command;
private Guid schemaId;
private ValidationContext validationContext;
private Func<string> message;
public ContentOperationContext(IAppProvider appProvider, IEnumerable<IValidatorsFactory> factories, IScriptEngine scriptEngine)
{
this.appProvider = appProvider;
this.factories = factories;
this.scriptEngine = scriptEngine;
}
public ISchemaEntity Schema
{
get { return schemaEntity; }
get { return schema; }
}
public static async Task<ContentOperationContext> CreateAsync(
Guid appId,
Guid schemaId,
ContentCommand command,
IAppProvider appProvider,
IAssetRepository assetRepository,
IContentRepository contentRepository,
IScriptEngine scriptEngine,
Func<string> message)
public async Task LoadAsync(NamedId<Guid> appId, NamedId<Guid> schemaId, ContentCommand command, Func<string> message, bool optimized)
{
var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(appId, schemaId);
var (app, schema) = await appProvider.GetAppWithSchemaAsync(appId.Id, schemaId.Id);
if (appEntity == null)
if (app == null)
{
throw new InvalidOperationException("Cannot resolve app.");
}
if (schemaEntity == null)
if (schema == null)
{
throw new InvalidOperationException("Cannot resolve schema.");
}
var context = new ContentOperationContext
{
appEntity = appEntity,
assetRepository = assetRepository,
command = command,
contentRepository = contentRepository,
message = message,
schemaId = schemaId,
schemaEntity = schemaEntity,
scriptEngine = scriptEngine
};
return context;
this.app = app;
this.schema = schema;
this.command = command;
this.message = message;
validationContext = new ValidationContext(appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized);
}
public Task GenerateDefaultValuesAsync(NamedContentData data)
{
data.GenerateDefaultValues(schemaEntity.SchemaDef, appEntity.PartitionResolver());
data.GenerateDefaultValues(schema.SchemaDef, app.PartitionResolver());
return Task.CompletedTask;
}
public Task ValidateAsync(NamedContentData data, bool optimized)
public async Task ValidateInputAsync(NamedContentData data)
{
var ctx = CreateValidationContext(optimized);
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories);
return data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message);
await validator.ValidateInputAsync(data);
CheckErrors(validator);
}
public Task ValidatePartialAsync(NamedContentData data, bool optimized)
public async Task ValidateInputPartialAsync(NamedContentData data)
{
var ctx = CreateValidationContext(optimized);
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories);
await validator.ValidateInputPartialAsync(data);
CheckErrors(validator);
}
public async Task ValidateContentAsync(NamedContentData data)
{
var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories);
await validator.ValidateContentAsync(data);
CheckErrors(validator);
}
return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message);
private void CheckErrors(ContentValidator validator)
{
if (validator.Errors.Count > 0)
{
throw new ValidationException(message(), validator.Errors.ToList());
}
}
public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptContext context)
@ -127,38 +142,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
private void Enrich(ScriptContext context)
{
context.ContentId = command.ContentId;
context.AppId = appEntity.Id;
context.AppName = appEntity.Name;
context.AppId = app.Id;
context.AppName = app.Name;
context.User = command.User;
}
private ValidationContext CreateValidationContext(bool optimized)
{
return new ValidationContext(command.ContentId, schemaId,
QueryContentsAsync,
QueryContentsAsync,
QueryAssetsAsync)
.Optimized(optimized);
}
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)
{
return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds));
}
private async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryContentsAsync(Guid filterSchemaId, FilterNode<ClrValue> filterNode)
{
return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode);
}
private async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryContentsAsync(HashSet<Guid> ids)
{
return await contentRepository.QueryIdsAsync(appEntity.Id, ids, SearchScope.All);
}
private string GetScript(Func<SchemaScripts, string> script)
{
return script(schemaEntity.SchemaDef.Scripts);
return script(schema.SchemaDef.Scripts);
}
}
}

76
backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs

@ -0,0 +1,76 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Validation
{
public sealed class DependencyValidatorsFactory : IValidatorsFactory
{
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
public DependencyValidatorsFactory(IAssetRepository assetRepository, IContentRepository contentRepository)
{
Guard.NotNull(assetRepository);
Guard.NotNull(contentRepository);
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
{
if (field is IField<AssetsFieldProperties> assetsField)
{
var checkAssets = new CheckAssets(async ids =>
{
return await assetRepository.QueryAsync(context.AppId.Id, new HashSet<Guid>(ids));
});
yield return new AssetsValidator(assetsField.Properties, checkAssets);
}
if (field is IField<ReferencesFieldProperties> referencesField)
{
var checkReferences = new CheckContentsByIds(async ids =>
{
return await contentRepository.QueryIdsAsync(context.AppId.Id, ids, SearchScope.All);
});
yield return new ReferencesValidator(referencesField.Properties.SchemaIds, checkReferences);
}
if (field is IField<NumberFieldProperties> numberField && numberField.Properties.IsUnique)
{
var checkUniqueness = new CheckUniqueness(async filter =>
{
return await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter);
});
yield return new UniqueValidator(checkUniqueness);
}
if (field is IField<StringFieldProperties> stringField && stringField.Properties.IsUnique)
{
var checkUniqueness = new CheckUniqueness(async filter =>
{
return await contentRepository.QueryIdsAsync(context.AppId.Id, context.SchemaId.Id, filter);
});
yield return new UniqueValidator(checkUniqueness);
}
}
}
}

12
backend/src/Squidex.Infrastructure/Queries/ClrFilter.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries
{
public static class ClrFilter
@ -14,11 +16,21 @@ namespace Squidex.Infrastructure.Queries
return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters);
}
public static LogicalFilter<ClrValue> And(IReadOnlyList<FilterNode<ClrValue>> filters)
{
return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters);
}
public static LogicalFilter<ClrValue> Or(params FilterNode<ClrValue>[] filters)
{
return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters);
}
public static LogicalFilter<ClrValue> Or(IReadOnlyList<FilterNode<ClrValue>> filters)
{
return new LogicalFilter<ClrValue>(LogicalFilterType.Or, filters);
}
public static NegateFilter<ClrValue> Not(FilterNode<ClrValue> filter)
{
return new NegateFilter<ClrValue>(filter);

11
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -8,11 +8,13 @@
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.Contents.Text.Lucene;
using Squidex.Domain.Apps.Entities.Contents.Validation;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Search;
using Squidex.Infrastructure.EventSourcing;
@ -36,6 +38,15 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ContentDomainObject>()
.AsSelf();
services.AddTransientAs<ContentOperationContext>()
.AsSelf();
services.AddSingletonAs<DefaultValidatorsFactory>()
.As<IValidatorsFactory>();
services.AddSingletonAs<DependencyValidatorsFactory>()
.As<IValidatorsFactory>();
services.AddSingletonAs<ContentHistoryEventsCreator>()
.As<IHistoryEventsCreator>();

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ArrayFieldProperties());
await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext);
await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors);
Assert.Empty(errors);
}

218
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -23,68 +23,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
private readonly List<string> errors = new List<string>();
public sealed class AssetInfo : IAssetInfo
{
public Guid AssetId { get; set; }
public string FileName { get; set; }
public string FileHash { get; set; }
public string Slug { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; }
public AssetMetadata Metadata { get; set; }
public AssetType Type { get; set; }
}
private readonly AssetInfo document = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyDocument.pdf",
FileSize = 1024 * 4,
Type = AssetType.Unknown
};
private readonly AssetInfo image1 = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyImage.png",
FileSize = 1024 * 8,
Type = AssetType.Image,
Metadata =
new AssetMetadata()
.SetPixelWidth(800)
.SetPixelHeight(600)
};
private readonly AssetInfo image2 = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyImage.png",
FileSize = 1024 * 8,
Type = AssetType.Image,
Metadata =
new AssetMetadata()
.SetPixelWidth(800)
.SetPixelHeight(600)
};
private readonly ValidationContext ctx;
public AssetsFieldTests()
{
ctx = ValidationTestExtensions.Assets(image1, image2, document);
}
[Fact]
public void Should_instantiate_field()
{
@ -93,22 +31,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
Assert.Equal("my-assets", sut.Name);
}
[Fact]
public async Task Should_not_add_error_if_assets_are_valid()
{
var sut = Field(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(document.AssetId), errors, ctx);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_assets_are_null_and_valid()
{
var sut = Field(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors, ctx);
await sut.ValidateAsync(CreateValue(null), errors);
Assert.Empty(errors);
}
@ -118,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 });
await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
Assert.Empty(errors);
}
@ -128,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { AllowDuplicates = true });
await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
Assert.Empty(errors);
}
@ -138,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors, ctx);
await sut.ValidateAsync(CreateValue(null), errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
@ -149,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(), errors, ctx);
await sut.ValidateAsync(CreateValue(), errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
@ -171,7 +99,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MinItems = 3 });
await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." });
@ -182,151 +110,25 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties { MaxItems = 1 });
await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." });
}
[Fact]
public async Task Should_add_error_if_asset_are_not_valid()
{
var assetId = Guid.NewGuid();
var sut = Field(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(assetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { $"[1]: Id '{assetId}' not found." });
}
[Fact]
public async Task Should_not_add_error_if_asset_are_not_valid_but_in_optimized_mode()
public async Task Should_add_error_if_values_contains_duplicate()
{
var assetId = Guid.NewGuid();
var sut = Field(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(assetId), errors, ctx.Optimized());
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_document_is_too_small()
{
var sut = Field(new AssetsFieldProperties { MinSize = 5 * 1024 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." });
}
[Fact]
public async Task Should_add_error_if_document_is_too_big()
{
var sut = Field(new AssetsFieldProperties { MaxSize = 5 * 1024 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." });
}
[Fact]
public async Task Should_add_error_if_document_is_not_an_image()
{
var sut = Field(new AssetsFieldProperties { MustBeImage = true });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[1]: Not an image." });
}
[Fact]
public async Task Should_add_error_if_values_contains_duplicate()
{
var sut = Field(new AssetsFieldProperties { MustBeImage = true });
var id = Guid.NewGuid();
await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx);
await sut.ValidateAsync(CreateValue(id, id), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." });
}
[Fact]
public async Task Should_add_error_if_image_width_is_too_small()
{
var sut = Field(new AssetsFieldProperties { MinWidth = 1000 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." });
}
[Fact]
public async Task Should_add_error_if_image_width_is_too_big()
{
var sut = Field(new AssetsFieldProperties { MaxWidth = 700 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." });
}
[Fact]
public async Task Should_add_error_if_image_height_is_too_small()
{
var sut = Field(new AssetsFieldProperties { MinHeight = 800 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." });
}
[Fact]
public async Task Should_add_error_if_image_height_is_too_big()
{
var sut = Field(new AssetsFieldProperties { MaxHeight = 500 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." });
}
[Fact]
public async Task Should_add_error_if_image_has_invalid_aspect_ratio()
{
var sut = Field(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[] { "[2]: Aspect ratio not '1:1'." });
}
[Fact]
public async Task Should_add_error_if_image_has_invalid_extension()
{
var sut = Field(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors, ctx);
errors.Should().BeEquivalentTo(
new[]
{
"[1]: Invalid file extension.",
"[2]: Invalid file extension."
});
}
private static IJsonValue CreateValue(params Guid[]? ids)
{
return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray());

40
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -11,7 +11,6 @@ using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
@ -23,7 +22,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly List<ValidationError> errors = new List<ValidationError>();
private readonly ValidationContext context = ValidationTestExtensions.ValidContext;
private Schema schema = new Schema("my-schema");
[Fact]
@ -34,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("unknown",
new ContentFieldData());
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -55,7 +53,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddValue("iv", 1000));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -76,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1)
.AddValue("it", 1));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -95,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -114,7 +112,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -132,7 +130,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -153,7 +151,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("de", 1)
.AddValue("xx", 1));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -180,7 +178,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddValue("es", "value"));
await data.ValidateAsync(context, schema, optionalConfig.ToResolver(), errors);
await data.ValidateAsync(optionalConfig.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -197,7 +195,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1)
.AddValue("it", 1));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -215,7 +213,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("unknown",
new ContentFieldData());
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -236,7 +234,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData()
.AddValue("iv", 1000));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -257,7 +255,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1)
.AddValue("it", 1));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -276,7 +274,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData();
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -290,7 +288,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData();
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -307,7 +305,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("de", 1)
.AddValue("xx", 1));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -328,7 +326,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1)
.AddValue("it", 1));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -354,7 +352,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Object().Add("my-nested", 1),
JsonValue.Object())));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo(
new List<ValidationError>
@ -372,7 +370,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data =
new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors);
}
@ -391,7 +389,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Array(
JsonValue.Object())));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors);
await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors);
}

12
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
@ -93,17 +92,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Invalid json type, expected number." });
}
[Fact]
public async Task Should_add_error_if_unique_constraint_failed()
{
var sut = Field(new NumberFieldProperties { IsUnique = true });
await sut.ValidateAsync(CreateValue(12.5), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid())));
errors.Should().BeEquivalentTo(
new[] { "Another content with the same value exists." });
}
private static IJsonValue CreateValue(double v)
{
return JsonValue.Create(v);

72
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs

@ -11,7 +11,6 @@ using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
@ -37,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(CreateValue(ref1), errors, Context());
await sut.ValidateAsync(CreateValue(ref1), errors);
Assert.Empty(errors);
}
@ -47,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors, Context());
await sut.ValidateAsync(CreateValue(null), errors);
Assert.Empty(errors);
}
@ -57,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 });
await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context());
await sut.ValidateAsync(CreateValue(ref1, ref2), errors);
Assert.Empty(errors);
}
@ -67,17 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true });
await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context());
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_schemas_not_defined()
{
var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1)));
await sut.ValidateAsync(CreateValue(ref1, ref1), errors);
Assert.Empty(errors);
}
@ -87,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors, Context());
await sut.ValidateAsync(CreateValue(null), errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
@ -98,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true });
await sut.ValidateAsync(CreateValue(), errors, Context());
await sut.ValidateAsync(CreateValue(), errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
@ -109,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context());
await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid json type, expected array of guid strings." });
@ -120,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 });
await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context());
await sut.ValidateAsync(CreateValue(ref1, ref2), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." });
@ -131,52 +120,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 });
await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context());
await sut.ValidateAsync(CreateValue(ref1, ref2), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." });
}
[Fact]
public async Task Should_add_error_if_reference_are_not_valid()
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References());
errors.Should().BeEquivalentTo(
new[] { $"Contains invalid reference '{ref1}'." });
}
[Fact]
public async Task Should_not_add_error_if_reference_are_not_valid_but_in_optimized_mode()
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References().Optimized());
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_reference_schema_is_not_valid()
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1)));
errors.Should().BeEquivalentTo(
new[] { $"Contains reference '{ref1}' to invalid schema." });
}
[Fact]
public async Task Should_add_error_if_reference_contains_duplicate_values()
{
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(ref1, ref1), errors,
ValidationTestExtensions.References(
(schemaId, ref1)));
await sut.ValidateAsync(CreateValue(ref1, ref1), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." });
@ -187,13 +142,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray());
}
private ValidationContext Context()
{
return ValidationTestExtensions.References(
(schemaId, ref1),
(schemaId, ref2));
}
private static RootField<ReferencesFieldProperties> Field(ReferencesFieldProperties properties)
{
return Fields.References(1, "my-refs", Partitioning.Invariant, properties);

12
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
@ -115,17 +114,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Custom Error Message." });
}
[Fact]
public async Task Should_add_error_if_unique_constraint_failed()
{
var sut = Field(new StringFieldProperties { IsUnique = true });
await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid())));
errors.Should().BeEquivalentTo(
new[] { "Another content with the same value exists." });
}
private static IJsonValue CreateValue(string? v)
{
return JsonValue.Create(v);

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs

@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new TagsFieldProperties());
await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext);
await sut.ValidateAsync(CreateValue("tag"), errors);
Assert.Empty(errors);
}

29
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
@ -35,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new UIFieldProperties());
await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext);
await sut.ValidateAsync(Undefined.Value, errors);
Assert.Empty(errors);
}
@ -76,12 +75,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("my-ui2", new ContentFieldData()
.AddValue("iv", null));
var validationContext = ValidationTestExtensions.ValidContext;
var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext);
var dataErrors = new List<ValidationError>();
await validator.ValidateAsync(data);
await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema);
validator.Errors.Should().BeEquivalentTo(
dataErrors.Should().BeEquivalentTo(
new[]
{
new ValidationError("Value must not be defined.", "my-ui1"),
@ -105,20 +103,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Object()
.Add("my-ui", null))));
var validationContext =
new ValidationContext(
Guid.NewGuid(),
Guid.NewGuid(),
(c, s) => null!,
(s) => null!,
(c) => null!);
var dataErrors = new List<ValidationError>();
var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext);
await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema);
await validator.ValidateAsync(data);
validator.Errors.Should().BeEquivalentTo(
new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") });
dataErrors.Should().BeEquivalentTo(
new[]
{
new ValidationError("Value must not be defined.", "my-array[1].my-ui")
});
}
private static NestedField<UIFieldProperties> Field(UIFieldProperties properties)

94
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs

@ -9,47 +9,71 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public static class ValidationTestExtensions
{
private static readonly Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> EmptyReferences = Task.FromResult<IReadOnlyList<(Guid SchemaId, Guid Id)>>(new List<(Guid SchemaId, Guid Id)>());
private static readonly Task<IReadOnlyList<IAssetInfo>> EmptyAssets = Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>());
private static readonly NamedId<Guid> AppId = NamedId.Of(Guid.NewGuid(), "my-app");
private static readonly NamedId<Guid> SchemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private static readonly IValidatorsFactory Factory = new DefaultValidatorsFactory();
public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(),
(x, y) => EmptyReferences,
(x) => EmptyReferences,
(x) => EmptyAssets);
public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
{
var context = CreateContext(schema, mode, updater);
return validator.ValidateAsync(value, context, CreateFormatter(errors));
}
public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors, ValidationContext? context = null)
public static Task ValidateAsync(this IField field, object? value, IList<string> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
{
return validator.ValidateAsync(value,
CreateContext(context),
CreateFormatter(errors));
var context = CreateContext(schema, mode, updater);
var validators = Factory.CreateValueValidators(context, field, null!);
return new FieldValidator(validators.ToArray(), field)
.ValidateAsync(value, context, CreateFormatter(errors));
}
public static Task ValidateOptionalAsync(this IValidator validator, object? value, IList<string> errors, ValidationContext? context = null)
public static async Task ValidatePartialAsync(this NamedContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
{
return validator.ValidateAsync(
value,
CreateContext(context).Optional(true),
CreateFormatter(errors));
var context = CreateContext(schema, mode, updater);
var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1));
await validator.ValidateInputPartialAsync(data);
foreach (var error in validator.Errors)
{
errors.Add(error);
}
}
public static Task ValidateAsync(this IField field, object? value, IList<string> errors, ValidationContext? context = null)
public static async Task ValidateAsync(this NamedContentData data, PartitionResolver partitionResolver, IList<ValidationError> errors,
Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
{
return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field)
.ValidateAsync(
value,
CreateContext(context),
CreateFormatter(errors));
var context = CreateContext(schema, mode, updater);
var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1));
await validator.ValidateInputAsync(data);
foreach (var error in validator.Errors)
{
errors.Add(error);
}
}
private static AddError CreateFormatter(IList<string> errors)
public static AddError CreateFormatter(IList<string> errors)
{
return (field, message) =>
{
@ -64,23 +88,21 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
};
}
private static ValidationContext CreateContext(ValidationContext? context)
{
return context ?? ValidContext;
}
public static ValidationContext Assets(params IAssetInfo[] assets)
public static ValidationContext CreateContext(Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
{
var actual = Task.FromResult<IReadOnlyList<IAssetInfo>>(assets.ToList());
return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual);
}
var context = new ValidationContext(
AppId,
SchemaId,
schema ?? new Schema(SchemaId.Name),
Guid.NewGuid(),
mode);
public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds)
{
var actual = Task.FromResult<IReadOnlyList<(Guid Id, Guid SchemaId)>>(referencesIds.ToList());
if (updater != null)
{
context = updater(context);
}
return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets);
return context;
}
}
}

233
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs

@ -0,0 +1,233 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
public class AssetsValidatorTests
{
private readonly List<string> errors = new List<string>();
public sealed class AssetInfo : IAssetInfo
{
public Guid AssetId { get; set; }
public string FileName { get; set; }
public string FileHash { get; set; }
public string Slug { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; }
public AssetMetadata Metadata { get; set; }
public AssetType Type { get; set; }
}
private readonly AssetInfo document = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyDocument.pdf",
FileSize = 1024 * 4,
Type = AssetType.Unknown
};
private readonly AssetInfo image1 = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyImage.png",
FileSize = 1024 * 8,
Type = AssetType.Image,
Metadata =
new AssetMetadata()
.SetPixelWidth(800)
.SetPixelHeight(600)
};
private readonly AssetInfo image2 = new AssetInfo
{
AssetId = Guid.NewGuid(),
FileName = "MyImage.png",
FileSize = 1024 * 8,
Type = AssetType.Image,
Metadata =
new AssetMetadata()
.SetPixelWidth(800)
.SetPixelHeight(600)
};
[Fact]
public async Task Should_not_add_error_if_assets_are_valid()
{
var sut = Validator(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(document.AssetId), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_asset_are_not_valid()
{
var assetId = Guid.NewGuid();
var sut = Validator(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(assetId), errors);
errors.Should().BeEquivalentTo(
new[] { $"[1]: Id '{assetId}' not found." });
}
[Fact]
public async Task Should_not_add_error_if_asset_are_not_valid_but_in_optimized_mode()
{
var assetId = Guid.NewGuid();
var sut = Validator(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(assetId), errors, updater: c => c.Optimized());
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_document_is_too_small()
{
var sut = Validator(new AssetsFieldProperties { MinSize = 5 * 1024 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[1]: \'4 kB\' less than minimum of \'5 kB\'." });
}
[Fact]
public async Task Should_add_error_if_document_is_too_big()
{
var sut = Validator(new AssetsFieldProperties { MaxSize = 5 * 1024 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[2]: \'8 kB\' greater than maximum of \'5 kB\'." });
}
[Fact]
public async Task Should_add_error_if_document_is_not_an_image()
{
var sut = Validator(new AssetsFieldProperties { MustBeImage = true });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[1]: Not an image." });
}
[Fact]
public async Task Should_add_error_if_image_width_is_too_small()
{
var sut = Validator(new AssetsFieldProperties { MinWidth = 1000 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[2]: Width \'800px\' less than minimum of \'1000px\'." });
}
[Fact]
public async Task Should_add_error_if_image_width_is_too_big()
{
var sut = Validator(new AssetsFieldProperties { MaxWidth = 700 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[2]: Width \'800px\' greater than maximum of \'700px\'." });
}
[Fact]
public async Task Should_add_error_if_image_height_is_too_small()
{
var sut = Validator(new AssetsFieldProperties { MinHeight = 800 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[2]: Height \'600px\' less than minimum of \'800px\'." });
}
[Fact]
public async Task Should_add_error_if_image_height_is_too_big()
{
var sut = Validator(new AssetsFieldProperties { MaxHeight = 500 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[2]: Height \'600px\' greater than maximum of \'500px\'." });
}
[Fact]
public async Task Should_add_error_if_image_has_invalid_aspect_ratio()
{
var sut = Validator(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[] { "[2]: Aspect ratio not '1:1'." });
}
[Fact]
public async Task Should_add_error_if_image_has_invalid_extension()
{
var sut = Validator(new AssetsFieldProperties { AllowedExtensions = ReadOnlyCollection.Create("mp4") });
await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors);
errors.Should().BeEquivalentTo(
new[]
{
"[1]: Invalid file extension.",
"[2]: Invalid file extension."
});
}
private static object CreateValue(params Guid[] ids)
{
return ids.ToList();
}
private IValidator Validator(AssetsFieldProperties properties)
{
return new AssetsValidator(properties, new CheckAssets(ids =>
{
return Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo> { document, image1, image2 });
}));
}
}
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs

@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
var sut = new CollectionValidator(true, 1, 3);
await sut.ValidateOptionalAsync(null, errors);
await sut.ValidateAsync(null, errors, updater: c => c.Optional(true));
Assert.Empty(errors);
}

78
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs

@ -0,0 +1,78 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
public class ReferencesValidatorTests
{
private readonly List<string> errors = new List<string>();
private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid ref1 = Guid.NewGuid();
private readonly Guid ref2 = Guid.NewGuid();
[Fact]
public async Task Should_add_error_if_references_are_not_valid()
{
var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences());
await sut.ValidateAsync(CreateValue(ref1), errors);
errors.Should().BeEquivalentTo(
new[] { $"Contains invalid reference '{ref1}'." });
}
[Fact]
public async Task Should_not_add_error_if_reference_are_not_valid_but_in_optimized_mode()
{
var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences());
await sut.ValidateAsync(CreateValue(ref1), errors, updater: c => c.Optimized());
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_schemas_not_defined()
{
var sut = new ReferencesValidator(null, FoundReferences((Guid.NewGuid(), ref2)));
await sut.ValidateAsync(CreateValue(ref2), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_reference_schema_is_not_valid()
{
var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences((Guid.NewGuid(), ref2)));
await sut.ValidateAsync(CreateValue(ref2), errors);
errors.Should().BeEquivalentTo(
new[] { $"Contains reference '{ref2}' to invalid schema." });
}
private static List<Guid> CreateValue(params Guid[] ids)
{
return ids.ToList();
}
private static CheckContentsByIds FoundReferences(params (Guid SchemaId, Guid Id)[] references)
{
return new CheckContentsByIds(x => Task.FromResult<IReadOnlyList<(Guid SchemaId, Guid Id)>>(references.ToList()));
}
}
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
var sut = new RequiredStringValidator();
await sut.ValidateOptionalAsync(string.Empty, errors);
await sut.ValidateAsync(string.Empty, errors, updater: c => c.Optional(true));
Assert.Empty(errors);
}

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
var sut = new RequiredValidator();
await sut.ValidateOptionalAsync(null, errors);
await sut.ValidateAsync(null, errors, updater: c => c.Optional(true));
Assert.Empty(errors);
}

64
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs

@ -9,7 +9,6 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Xunit;
@ -17,18 +16,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
public class UniqueValidatorTests
{
private readonly List<string> errors = new List<string>();
private readonly Guid contentId = Guid.NewGuid();
private readonly Guid schemaId = Guid.NewGuid();
private readonly List<string> errors = new List<string>();
[Fact]
public async Task Should_add_error_if_string_value_not_found()
{
var sut = new UniqueValidator();
var filter = string.Empty;
await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Default));
var sut = new UniqueValidator(Check(Guid.NewGuid(), f => filter = f));
await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv"));
errors.Should().BeEquivalentTo(
new[] { "property: Another content with the same value exists." });
@ -39,11 +37,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
[Fact]
public async Task Should_add_error_if_double_value_not_found()
{
var sut = new UniqueValidator();
var filter = string.Empty;
await sut.ValidateAsync(12.5, errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Default));
var sut = new UniqueValidator(Check(Guid.NewGuid(), f => filter = f));
await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv"));
errors.Should().BeEquivalentTo(
new[] { "property: Another content with the same value exists." });
@ -54,59 +52,45 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
[Fact]
public async Task Should_not_add_error_if_string_value_not_found_but_in_optimized_mode()
{
var sut = new UniqueValidator();
var sut = new UniqueValidator(Check(Guid.NewGuid()));
var filter = string.Empty;
await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Optimized));
await sut.ValidateAsync(null, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_string_value_found()
public async Task Should_not_add_error_if_string_value_found_with_same_content_id()
{
var sut = new UniqueValidator();
var ctx = ValidationTestExtensions.CreateContext();
var filter = string.Empty;
var sut = new UniqueValidator(Check(ctx.ContentId));
await sut.ValidateAsync("hi", errors, Context(contentId, f => filter = f, ValidationMode.Default));
await sut.ValidateAsync("hi", ctx, ValidationTestExtensions.CreateFormatter(errors));
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_double_value_found()
public async Task Should_not_add_error_if_double_value_found_with_same_content_id()
{
var sut = new UniqueValidator();
var ctx = ValidationTestExtensions.CreateContext();
var filter = string.Empty;
var sut = new UniqueValidator(Check(ctx.ContentId));
await sut.ValidateAsync(12.5, errors, Context(contentId, f => filter = f, ValidationMode.Default));
await sut.ValidateAsync(12.5, ctx, ValidationTestExtensions.CreateFormatter(errors));
Assert.Empty(errors);
}
private ValidationContext Context(Guid id, Action<string> filter, ValidationMode mode)
private CheckUniqueness Check(Guid id, Action<string>? filter = null)
{
return new ValidationContext(contentId, schemaId,
(schema, filterNode) =>
{
filter(filterNode.ToString());
return Task.FromResult<IReadOnlyList<(Guid, Guid)>>(new List<(Guid, Guid)> { (schemaId, id) });
},
(ids) =>
{
return Task.FromResult<IReadOnlyList<(Guid, Guid)>>(new List<(Guid, Guid)> { (schemaId, id) });
},
ids =>
{
return Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>());
},
mode)
.Nested("property")
.Nested("iv");
return new CheckUniqueness(filterNode =>
{
filter?.Invoke(filterNode.ToString());
return Task.FromResult<IReadOnlyList<(Guid, Guid)>>(new List<(Guid, Guid)> { (schemaId, id) });
});
}
}
}

9
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using NodaTime;
@ -13,10 +14,9 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
@ -34,7 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly Guid contentId = Guid.NewGuid();
private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow()));
private readonly ISchemaEntity schema;
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
@ -106,7 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
patched = patch.MergeInto(data);
sut = new ContentDomainObject(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), scriptEngine, contentWorkflow, contentRepository);
var context = new ContentOperationContext(appProvider, Enumerable.Repeat(new DefaultValidatorsFactory(), 1), scriptEngine);
sut = new ContentDomainObject(Store, contentWorkflow, context, A.Dummy<ISemanticLog>());
sut.Setup(Id);
}

Loading…
Cancel
Save