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 public sealed class ContentValidator
{ {
private readonly Schema schema;
private readonly PartitionResolver partitionResolver; private readonly PartitionResolver partitionResolver;
private readonly ValidationContext context; private readonly ValidationContext context;
private readonly IEnumerable<IValidatorsFactory> factories;
private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>(); private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>();
public IReadOnlyCollection<ValidationError> Errors public IReadOnlyCollection<ValidationError> Errors
@ -32,14 +32,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
get { return errors; } 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(context);
Guard.NotNull(factories);
Guard.NotNull(partitionResolver); Guard.NotNull(partitionResolver);
this.schema = schema;
this.context = context; this.context = context;
this.factories = factories;
this.partitionResolver = partitionResolver; this.partitionResolver = partitionResolver;
} }
@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
errors.Add(new ValidationError(message, pathString)); errors.Add(new ValidationError(message, pathString));
} }
public Task ValidatePartialAsync(NamedContentData data) public Task ValidateInputPartialAsync(NamedContentData data)
{ {
Guard.NotNull(data); Guard.NotNull(data);
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return validator.ValidateAsync(data, context, AddError); return validator.ValidateAsync(data, context, AddError);
} }
public Task ValidateAsync(NamedContentData data) public Task ValidateInputAsync(NamedContentData data)
{ {
Guard.NotNull(data); Guard.NotNull(data);
@ -68,11 +68,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return validator.ValidateAsync(data, context, AddError); 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) 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)); 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 partitioning = partitionResolver(field.Partitioning);
var fieldValidator = field.CreateValidator(); var fieldValidator = CreateFieldValidator(field);
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(); var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var partitionKey in partitioning.AllKeys) foreach (var partitionKey in partitioning.AllKeys)
@ -97,9 +106,29 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
var typeName = partitioning.ToString()!; var typeName = partitioning.ToString()!;
return new AggregateValidator( return new AggregateValidator(
field.CreateBagValidator() CreateFieldValidators(field)
.Union(Enumerable.Repeat( .Union(Enumerable.Repeat(
new ObjectValidator<IJsonValue>(fieldsValidators, isPartial, typeName), 1))); 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 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); Guard.NotNull(field);
return field.Accept(Instance); var visitor = new DefaultFieldValueValidatorsFactory(createFieldValidator);
return field.Accept(visitor);
} }
public IEnumerable<IValidator> Visit(IArrayField field) public IEnumerable<IValidator> Visit(IArrayField field)
@ -41,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
foreach (var nestedField in field.Fields) 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")); 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 UniqueValuesValidator<Guid>();
} }
yield return new AssetsValidator(field.Properties);
} }
public IEnumerable<IValidator> Visit(IField<BooleanFieldProperties> field) 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); yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues);
} }
if (field.Properties.IsUnique)
{
yield return new UniqueValidator();
}
} }
public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> field) 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 UniqueValuesValidator<Guid>();
} }
yield return new ReferencesValidator(field.Properties.SchemaIds);
} }
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field) 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); yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues);
} }
if (field.Properties.IsUnique)
{
yield return new UniqueValidator();
}
} }
public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> field) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Domain.Apps.Core.ValidateContent.Validators;
namespace Squidex.Domain.Apps.Core.ValidateContent 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); return DefaultFieldValueValidatorsFactory.CreateValidators(field, createFieldValidator);
}
public static IEnumerable<IValidator> CreateBagValidator(this IField field)
{
return FieldBagValidatorsFactory.CreateValidators(field);
} }
} }
} }

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.Collections.Generic;
using System.Threading.Tasks; 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); 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;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent 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 public sealed class ValidationContext
{ {
private readonly Guid contentId; public ImmutableQueue<string> Path { get; }
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 Guid ContentId public NamedId<Guid> AppId { get; }
{
get { return contentId; }
}
public Guid SchemaId public NamedId<Guid> SchemaId { get; }
{
get { return schemaId; } public Schema Schema { get; }
}
public Guid ContentId { get; }
public bool IsOptional { get; } public bool IsOptional { get; }
public ValidationMode Mode { get; } public ValidationMode Mode { get; }
public ValidationContext( public ValidationContext(
NamedId<Guid> appId,
NamedId<Guid> schemaId,
Schema schema,
Guid contentId, Guid contentId,
Guid schemaId,
CheckContents checkContent,
CheckContentsByIds checkContentsByIds,
CheckAssets checkAsset,
ValidationMode mode = ValidationMode.Default) 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( private ValidationContext(
NamedId<Guid> appId,
NamedId<Guid> schemaId,
Schema schema,
Guid contentId, Guid contentId,
Guid schemaId, ImmutableQueue<string> path,
CheckContents checkContent,
CheckContentsByIds checkContentByIds,
CheckAssets checkAsset,
ImmutableQueue<string> propertyPath,
bool isOptional, bool isOptional,
ValidationMode mode = ValidationMode.Default) ValidationMode mode = ValidationMode.Default)
{ {
Guard.NotNull(checkAsset); AppId = appId;
Guard.NotNull(checkContent); ContentId = contentId;
Guard.NotNull(checkContentByIds); IsOptional = isOptional;
this.propertyPath = propertyPath;
this.checkContent = checkContent;
this.checkContentByIds = checkContentByIds;
this.checkAsset = checkAsset;
this.contentId = contentId;
this.schemaId = schemaId;
Mode = mode; Mode = mode;
Path = path;
IsOptional = isOptional; Schema = schema;
SchemaId = schemaId;
} }
public ValidationContext Optimized(bool isOptimized = true) public ValidationContext Optimized(bool isOptimized = true)
@ -96,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return this; return this;
} }
return Clone(propertyPath, IsOptional, mode); return Clone(Path, IsOptional, mode);
} }
public ValidationContext Optional(bool isOptional) public ValidationContext Optional(bool isOptional)
@ -106,38 +76,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return this; return this;
} }
return Clone(propertyPath, isOptional, Mode); return Clone(Path, isOptional, Mode);
} }
public ValidationContext Nested(string property) 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) private ValidationContext Clone(ImmutableQueue<string> path, bool isOptional, ValidationMode mode)
{ {
return new ValidationContext( return new ValidationContext(AppId, SchemaId, Schema, ContentId, path, isOptional, mode);
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);
} }
} }
} }

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 namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public delegate Task<IReadOnlyList<IAssetInfo>> CheckAssets(IEnumerable<Guid> ids);
public sealed class AssetsValidator : IValidator public sealed class AssetsValidator : IValidator
{ {
private readonly AssetsFieldProperties properties; 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.properties = properties;
this.checkAssets = checkAssets;
} }
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) 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) if (value is ICollection<Guid> assetIds && assetIds.Count > 0)
{ {
var assets = await context.GetAssetInfosAsync(assetIds); var assets = await checkAssets(assetIds);
var index = 0; var index = 0;
foreach (var assetId in assetIds) 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;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators 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) public PatternValidator(string pattern, string? errorMessage = null)
{ {
Guard.NotNullOrEmpty(pattern);
this.errorMessage = errorMessage; this.errorMessage = errorMessage;
regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout); 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckContentsByIds(HashSet<Guid> ids);
public sealed class ReferencesValidator : IValidator public sealed class ReferencesValidator : IValidator
{ {
private readonly IEnumerable<Guid>? schemaIds; 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.schemaIds = schemaIds;
this.checkReferences = checkReferences;
} }
public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) 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) if (value is ICollection<Guid> contentIds)
{ {
var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); var foundIds = await checkReferences(contentIds.ToHashSet());
foreach (var id in contentIds) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,8 +13,17 @@ using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public delegate Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> CheckUniqueness(FilterNode<ClrValue> filter);
public sealed class UniqueValidator : IValidator 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) public async Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{ {
if (context.Mode == ValidationMode.Optimized) if (context.Mode == ValidationMode.Optimized)
@ -38,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (filter != null) if (filter != null)
{ {
var found = await context.GetContentIdsAsync(context.SchemaId, filter); var found = await checkUniqueness(filter);
if (found.Any(x => x.Id != context.ContentId)) 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 NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting; 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.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards; 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.Entities.Contents.State;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
@ -28,33 +26,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public class ContentDomainObject : LogSnapshotDomainObject<ContentState> 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 IContentWorkflow contentWorkflow;
private readonly ContentOperationContext context;
public ContentDomainObject( public ContentDomainObject(IStore<Guid> store, IContentWorkflow contentWorkflow, ContentOperationContext context, ISemanticLog log)
IStore<Guid> store,
ISemanticLog log,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(appProvider); Guard.NotNull(context);
Guard.NotNull(scriptEngine);
Guard.NotNull(assetRepository);
Guard.NotNull(contentWorkflow); Guard.NotNull(contentWorkflow);
Guard.NotNull(contentRepository);
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentWorkflow = contentWorkflow; this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository; this.context = context;
} }
public override Task<object?> ExecuteAsync(IAggregateCommand command) public override Task<object?> ExecuteAsync(IAggregateCommand command)
@ -66,15 +48,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, async c => 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) if (!c.DoNotScript)
{ {
c.Data = await ctx.ExecuteScriptAndTransformAsync(s => s.Create, c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Create,
new ScriptContext new ScriptContext
{ {
Operation = "Create", 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) if (!c.DoNotValidate)
{ {
await ctx.ValidateAsync(c.Data, c.OptimizeValidation); await context.ValidateContentAsync(c.Data);
} }
if (c.Publish) if (c.Publish)
{ {
await ctx.ExecuteScriptAsync(s => s.Change, await context.ExecuteScriptAsync(s => s.Change,
new ScriptContext new ScriptContext
{ {
Operation = "Published", Operation = "Published",
@ -111,11 +98,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
case CreateContentDraft createContentDraft: case CreateContentDraft createContentDraft:
return UpdateReturnAsync(createContentDraft, async c => 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); CreateDraft(c, status);
@ -125,9 +112,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContentDraft deleteContentDraft: case DeleteContentDraft deleteContentDraft:
return UpdateReturnAsync(deleteContentDraft, async c => 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); DeleteDraft(c);
@ -155,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
try 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) if (c.DueTime.HasValue)
{ {
@ -167,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var change = GetChange(c); var change = GetChange(c);
await ctx.ExecuteScriptAsync(s => s.Change, await context.ExecuteScriptAsync(s => s.Change,
new ScriptContext new ScriptContext
{ {
Operation = change.ToString(), Operation = change.ToString(),
@ -197,11 +184,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
case DeleteContent deleteContent: case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c => 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 new ScriptContext
{ {
Operation = "Delete", Operation = "Delete",
@ -226,18 +213,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!currentData!.Equals(newData)) 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) if (partial)
{ {
await ctx.ValidatePartialAsync(command.Data, false); await context.ValidateInputPartialAsync(command.Data);
} }
else 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 new ScriptContext
{ {
Operation = "Create", Operation = "Create",
@ -247,6 +234,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
StatusOld = default StatusOld = default
}); });
await context.ValidateContentAsync(newData);
Update(command, 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 = return context.LoadAsync(appId, schemaId, command, message, optimized);
await ContentOperationContext.CreateAsync(appId, schemaId, command,
appProvider, assetRepository, contentRepository, scriptEngine, message);
return operationContext;
} }
} }
} }

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

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.DefaultValues; 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.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps; 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.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; 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 namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentOperationContext public sealed class ContentOperationContext
{ {
private IContentRepository contentRepository; private readonly IScriptEngine scriptEngine;
private IAssetRepository assetRepository; private readonly IAppProvider appProvider;
private IScriptEngine scriptEngine; private readonly IEnumerable<IValidatorsFactory> factories;
private ISchemaEntity schemaEntity; private ISchemaEntity schema;
private IAppEntity appEntity; private IAppEntity app;
private ContentCommand command; private ContentCommand command;
private Guid schemaId; private ValidationContext validationContext;
private Func<string> message; 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 public ISchemaEntity Schema
{ {
get { return schemaEntity; } get { return schema; }
} }
public static async Task<ContentOperationContext> CreateAsync( public async Task LoadAsync(NamedId<Guid> appId, NamedId<Guid> schemaId, ContentCommand command, Func<string> message, bool optimized)
Guid appId,
Guid schemaId,
ContentCommand command,
IAppProvider appProvider,
IAssetRepository assetRepository,
IContentRepository contentRepository,
IScriptEngine scriptEngine,
Func<string> message)
{ {
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."); throw new InvalidOperationException("Cannot resolve app.");
} }
if (schemaEntity == null) if (schema == null)
{ {
throw new InvalidOperationException("Cannot resolve schema."); throw new InvalidOperationException("Cannot resolve schema.");
} }
var context = new ContentOperationContext this.app = app;
{ this.schema = schema;
appEntity = appEntity, this.command = command;
assetRepository = assetRepository, this.message = message;
command = command,
contentRepository = contentRepository, validationContext = new ValidationContext(appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized);
message = message,
schemaId = schemaId,
schemaEntity = schemaEntity,
scriptEngine = scriptEngine
};
return context;
} }
public Task GenerateDefaultValuesAsync(NamedContentData data) public Task GenerateDefaultValuesAsync(NamedContentData data)
{ {
data.GenerateDefaultValues(schemaEntity.SchemaDef, appEntity.PartitionResolver()); data.GenerateDefaultValues(schema.SchemaDef, app.PartitionResolver());
return Task.CompletedTask; 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) 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) private void Enrich(ScriptContext context)
{ {
context.ContentId = command.ContentId; context.ContentId = command.ContentId;
context.AppId = appEntity.Id; context.AppId = app.Id;
context.AppName = appEntity.Name; context.AppName = app.Name;
context.User = command.User; 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) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Queries namespace Squidex.Infrastructure.Queries
{ {
public static class ClrFilter public static class ClrFilter
@ -14,11 +16,21 @@ namespace Squidex.Infrastructure.Queries
return new LogicalFilter<ClrValue>(LogicalFilterType.And, filters); 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) public static LogicalFilter<ClrValue> Or(params FilterNode<ClrValue>[] filters)
{ {
return new LogicalFilter<ClrValue>(LogicalFilterType.Or, 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) public static NegateFilter<ClrValue> Not(FilterNode<ClrValue> filter)
{ {
return new NegateFilter<ClrValue>(filter); return new NegateFilter<ClrValue>(filter);

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

@ -8,11 +8,13 @@
using System; using System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.Contents.Text.Lucene; 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.History;
using Squidex.Domain.Apps.Entities.Search; using Squidex.Domain.Apps.Entities.Search;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -36,6 +38,15 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ContentDomainObject>() services.AddTransientAs<ContentDomainObject>()
.AsSelf(); .AsSelf();
services.AddTransientAs<ContentOperationContext>()
.AsSelf();
services.AddSingletonAs<DefaultValidatorsFactory>()
.As<IValidatorsFactory>();
services.AddSingletonAs<DependencyValidatorsFactory>()
.As<IValidatorsFactory>();
services.AddSingletonAs<ContentHistoryEventsCreator>() services.AddSingletonAs<ContentHistoryEventsCreator>()
.As<IHistoryEventsCreator>(); .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()); var sut = Field(new ArrayFieldProperties());
await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors, ValidationTestExtensions.ValidContext); await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors);
Assert.Empty(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>(); 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] [Fact]
public void Should_instantiate_field() public void Should_instantiate_field()
{ {
@ -93,22 +31,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
Assert.Equal("my-assets", sut.Name); 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] [Fact]
public async Task Should_not_add_error_if_assets_are_null_and_valid() public async Task Should_not_add_error_if_assets_are_null_and_valid()
{ {
var sut = Field(new AssetsFieldProperties()); var sut = Field(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors, ctx); await sut.ValidateAsync(CreateValue(null), errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -118,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); 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); Assert.Empty(errors);
} }
@ -128,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); 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); Assert.Empty(errors);
} }
@ -138,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new AssetsFieldProperties { IsRequired = true }); var sut = Field(new AssetsFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors, ctx); await sut.ValidateAsync(CreateValue(null), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Field is required." }); new[] { "Field is required." });
@ -149,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new AssetsFieldProperties { IsRequired = true }); var sut = Field(new AssetsFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(), errors, ctx); await sut.ValidateAsync(CreateValue(), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Field is required." }); new[] { "Field is required." });
@ -171,7 +99,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new AssetsFieldProperties { MinItems = 3 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." }); new[] { "Must not have more than 1 item(s)." });
} }
[Fact] [Fact]
public async Task Should_add_error_if_asset_are_not_valid() 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);
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 = Field(new AssetsFieldProperties()); var sut = Field(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(assetId), errors, ctx.Optimized()); var id = Guid.NewGuid();
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 });
await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors, ctx); await sut.ValidateAsync(CreateValue(id, id), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." }); 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) private static IJsonValue CreateValue(params Guid[]? ids)
{ {
return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); 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.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; 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 LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
private readonly List<ValidationError> errors = new List<ValidationError>(); private readonly List<ValidationError> errors = new List<ValidationError>();
private readonly ValidationContext context = ValidationTestExtensions.ValidContext;
private Schema schema = new Schema("my-schema"); private Schema schema = new Schema("my-schema");
[Fact] [Fact]
@ -34,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("unknown", .AddField("unknown",
new ContentFieldData()); new ContentFieldData());
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -55,7 +53,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 1000)); .AddValue("iv", 1000));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -76,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -95,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data = var data =
new NamedContentData(); new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -114,7 +112,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data = var data =
new NamedContentData(); new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -132,7 +130,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data = var data =
new NamedContentData(); new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -153,7 +151,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("de", 1) .AddValue("de", 1)
.AddValue("xx", 1)); .AddValue("xx", 1));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -180,7 +178,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData() new ContentFieldData()
.AddValue("es", "value")); .AddValue("es", "value"));
await data.ValidateAsync(context, schema, optionalConfig.ToResolver(), errors); await data.ValidateAsync(optionalConfig.ToResolver(), errors, schema);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -197,7 +195,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -215,7 +213,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("unknown", .AddField("unknown",
new ContentFieldData()); new ContentFieldData());
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -236,7 +234,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 1000)); .AddValue("iv", 1000));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -257,7 +255,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -276,7 +274,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data = var data =
new NamedContentData(); new NamedContentData();
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -290,7 +288,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data = var data =
new NamedContentData(); new NamedContentData();
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -307,7 +305,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("de", 1) .AddValue("de", 1)
.AddValue("xx", 1)); .AddValue("xx", 1));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -328,7 +326,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddValue("es", 1) .AddValue("es", 1)
.AddValue("it", 1)); .AddValue("it", 1));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -354,7 +352,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Object().Add("my-nested", 1), JsonValue.Object().Add("my-nested", 1),
JsonValue.Object()))); JsonValue.Object())));
await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
@ -372,7 +370,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
var data = var data =
new NamedContentData(); new NamedContentData();
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -391,7 +389,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Array( JsonValue.Array(
JsonValue.Object()))); JsonValue.Object())));
await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); await data.ValidateAsync(languagesConfig.ToResolver(), errors, schema);
Assert.Empty(errors); 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -93,17 +92,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Invalid json type, expected number." }); 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) private static IJsonValue CreateValue(double v)
{ {
return JsonValue.Create(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 System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
@ -37,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties()); var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(CreateValue(ref1), errors, Context()); await sut.ValidateAsync(CreateValue(ref1), errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -47,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties()); var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors, Context()); await sut.ValidateAsync(CreateValue(null), errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -57,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); 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); 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 }); var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true });
await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); await sut.ValidateAsync(CreateValue(ref1, ref1), errors);
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)));
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -87,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); 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( errors.Should().BeEquivalentTo(
new[] { "Field is required." }); new[] { "Field is required." });
@ -98,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true });
await sut.ValidateAsync(CreateValue(), errors, Context()); await sut.ValidateAsync(CreateValue(), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Field is required." }); new[] { "Field is required." });
@ -109,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ReferencesFieldProperties()); var sut = Field(new ReferencesFieldProperties());
await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); await sut.ValidateAsync(JsonValue.Create("invalid"), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Invalid json type, expected array of guid strings." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." }); 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] [Fact]
public async Task Should_add_error_if_reference_contains_duplicate_values() public async Task Should_add_error_if_reference_contains_duplicate_values()
{ {
var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId });
await sut.ValidateAsync(CreateValue(ref1, ref1), errors, await sut.ValidateAsync(CreateValue(ref1, ref1), errors);
ValidationTestExtensions.References(
(schemaId, ref1)));
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." }); 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()); 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) private static RootField<ReferencesFieldProperties> Field(ReferencesFieldProperties properties)
{ {
return Fields.References(1, "my-refs", Partitioning.Invariant, 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -115,17 +114,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "Custom Error Message." }); 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) private static IJsonValue CreateValue(string? v)
{ {
return JsonValue.Create(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()); var sut = Field(new TagsFieldProperties());
await sut.ValidateAsync(CreateValue("tag"), errors, ValidationTestExtensions.ValidContext); await sut.ValidateAsync(CreateValue("tag"), errors);
Assert.Empty(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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -35,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new UIFieldProperties()); var sut = Field(new UIFieldProperties());
await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); await sut.ValidateAsync(Undefined.Value, errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -76,12 +75,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.AddField("my-ui2", new ContentFieldData() .AddField("my-ui2", new ContentFieldData()
.AddValue("iv", null)); .AddValue("iv", null));
var validationContext = ValidationTestExtensions.ValidContext; var dataErrors = new List<ValidationError>();
var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext);
await validator.ValidateAsync(data); await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema);
validator.Errors.Should().BeEquivalentTo( dataErrors.Should().BeEquivalentTo(
new[] new[]
{ {
new ValidationError("Value must not be defined.", "my-ui1"), new ValidationError("Value must not be defined.", "my-ui1"),
@ -105,20 +103,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
JsonValue.Object() JsonValue.Object()
.Add("my-ui", null)))); .Add("my-ui", null))));
var validationContext = var dataErrors = new List<ValidationError>();
new ValidationContext(
Guid.NewGuid(),
Guid.NewGuid(),
(c, s) => null!,
(s) => null!,
(c) => null!);
var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); await data.ValidateAsync(x => InvariantPartitioning.Instance, dataErrors, schema);
await validator.ValidateAsync(data); dataErrors.Should().BeEquivalentTo(
new[]
validator.Errors.Should().BeEquivalentTo( {
new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); new ValidationError("Value must not be defined.", "my-array[1].my-ui")
});
} }
private static NestedField<UIFieldProperties> Field(UIFieldProperties properties) 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
public static class ValidationTestExtensions 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 NamedId<Guid> AppId = NamedId.Of(Guid.NewGuid(), "my-app");
private static readonly Task<IReadOnlyList<IAssetInfo>> EmptyAssets = Task.FromResult<IReadOnlyList<IAssetInfo>>(new List<IAssetInfo>()); 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(), public static Task ValidateAsync(this IValidator validator, object? value, IList<string> errors,
(x, y) => EmptyReferences, Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
(x) => EmptyReferences, {
(x) => EmptyAssets); 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, var context = CreateContext(schema, mode, updater);
CreateContext(context),
CreateFormatter(errors)); 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( var context = CreateContext(schema, mode, updater);
value,
CreateContext(context).Optional(true), var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1));
CreateFormatter(errors));
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) var context = CreateContext(schema, mode, updater);
.ValidateAsync(
value, var validator = new ContentValidator(partitionResolver, context, Enumerable.Repeat(Factory, 1));
CreateContext(context),
CreateFormatter(errors)); 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) => return (field, message) =>
{ {
@ -64,23 +88,21 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
}; };
} }
private static ValidationContext CreateContext(ValidationContext? context) public static ValidationContext CreateContext(Schema? schema = null, ValidationMode mode = ValidationMode.Default, Func<ValidationContext, ValidationContext>? updater = null)
{
return context ?? ValidContext;
}
public static ValidationContext Assets(params IAssetInfo[] assets)
{ {
var actual = Task.FromResult<IReadOnlyList<IAssetInfo>>(assets.ToList()); var context = new ValidationContext(
AppId,
return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); SchemaId,
} schema ?? new Schema(SchemaId.Name),
Guid.NewGuid(),
mode);
public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) if (updater != null)
{ {
var actual = Task.FromResult<IReadOnlyList<(Guid Id, Guid SchemaId)>>(referencesIds.ToList()); 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); 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); 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(); var sut = new RequiredStringValidator();
await sut.ValidateOptionalAsync(string.Empty, errors); await sut.ValidateAsync(string.Empty, errors, updater: c => c.Optional(true));
Assert.Empty(errors); 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(); var sut = new RequiredValidator();
await sut.ValidateOptionalAsync(null, errors); await sut.ValidateAsync(null, errors, updater: c => c.Optional(true));
Assert.Empty(errors); 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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Xunit; using Xunit;
@ -17,18 +16,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{ {
public class UniqueValidatorTests 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 Guid schemaId = Guid.NewGuid();
private readonly List<string> errors = new List<string>();
[Fact] [Fact]
public async Task Should_add_error_if_string_value_not_found() public async Task Should_add_error_if_string_value_not_found()
{ {
var sut = new UniqueValidator();
var filter = string.Empty; 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( errors.Should().BeEquivalentTo(
new[] { "property: Another content with the same value exists." }); new[] { "property: Another content with the same value exists." });
@ -39,11 +37,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
[Fact] [Fact]
public async Task Should_add_error_if_double_value_not_found() public async Task Should_add_error_if_double_value_not_found()
{ {
var sut = new UniqueValidator();
var filter = string.Empty; 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( errors.Should().BeEquivalentTo(
new[] { "property: Another content with the same value exists." }); new[] { "property: Another content with the same value exists." });
@ -54,59 +52,45 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
[Fact] [Fact]
public async Task Should_not_add_error_if_string_value_not_found_but_in_optimized_mode() 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(null, errors);
await sut.ValidateAsync("hi", errors, Context(Guid.NewGuid(), f => filter = f, ValidationMode.Optimized));
Assert.Empty(errors); Assert.Empty(errors);
} }
[Fact] [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); Assert.Empty(errors);
} }
[Fact] [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); 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, return new CheckUniqueness(filterNode =>
(schema, filterNode) => {
{ filter?.Invoke(filterNode.ToString());
filter(filterNode.ToString());
return Task.FromResult<IReadOnlyList<(Guid, Guid)>>(new List<(Guid, Guid)> { (schemaId, id) });
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");
} }
} }
} }

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using NodaTime; using NodaTime;
@ -13,10 +14,9 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps; 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.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -34,7 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly Guid contentId = Guid.NewGuid(); private readonly Guid contentId = Guid.NewGuid();
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); 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 IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow()));
private readonly ISchemaEntity schema; private readonly ISchemaEntity schema;
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
@ -106,7 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
patched = patch.MergeInto(data); 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); sut.Setup(Id);
} }

Loading…
Cancel
Save