From 9b4ece1e0af1ab0f7dca1c4d506ac0361a949727 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 20 May 2019 15:11:54 +0200 Subject: [PATCH] Improvement for validation. --- .../Schemas/FieldExtensions.cs | 16 ++++ .../ConvertContent/FieldConverters.cs | 2 +- .../ConvertContent/ValueConverters.cs | 2 +- .../GenerateEdmSchema/EdmSchemaExtensions.cs | 2 +- .../JsonSchemaExtensions.cs | 3 +- .../GenerateJsonSchema/JsonTypeVisitor.cs | 3 +- .../ValidateContent/ContentValidator.cs | 22 +++-- .../ValidateContent/Extensions.cs | 31 +++++++ .../FieldBagValidatorsFactory.cs | 85 +++++++++++++++++++ ...tory.cs => FieldValueValidatorsFactory.cs} | 16 ++-- .../ValidateContent/Undefined.cs | 24 ++++++ .../Validators/AggregateValidator.cs | 33 +++++++ .../Validators/FieldValidator.cs | 41 ++++++--- .../Validators/NoValueValidator.cs | 2 +- .../Validators/ObjectValidator.cs | 12 ++- .../Validators/RequiredStringValidator.cs | 7 +- .../Validators/RequiredValidator.cs | 2 +- .../GraphQL/Types/ContentDataGraphType.cs | 3 +- .../Contents/GraphQL/Types/NestedGraphType.cs | 2 +- .../content/content-field.component.html | 6 +- .../content/shared/array-item.component.ts | 2 +- .../shared/field-editor.component.html | 4 +- .../pages/schema/field-wizard.component.html | 2 +- .../schemas/pages/schema/field.component.html | 32 +++---- .../forms/field-form-common.component.html | 2 +- .../app/shared/services/schemas.service.ts | 2 +- .../app/shared/services/schemas.types.ts | 4 +- src/Squidex/app/shared/state/schemas.forms.ts | 2 +- .../GenerateJsonSchema/JsonSchemaTests.cs | 2 +- .../ValidateContent/ContentValidationTests.cs | 18 ++++ .../ValidateContent/UIFieldTests.cs | 74 ++++++++++++++-- .../ValidationTestExtensions.cs | 9 +- .../Validators/NoValueValidatorTests.cs | 5 +- 33 files changed, 388 insertions(+), 84 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs rename src/Squidex.Domain.Apps.Core.Operations/ValidateContent/{ValidatorsFactory.cs => FieldValueValidatorsFactory.cs} (92%) create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs index 3447e9271..1d817dc1e 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs @@ -7,6 +7,7 @@ using Squidex.Infrastructure; using System.Collections.Generic; +using System.Linq; using NamedIdStatic = Squidex.Infrastructure.NamedId; namespace Squidex.Domain.Apps.Core.Schemas @@ -18,6 +19,21 @@ namespace Squidex.Domain.Apps.Core.Schemas return NamedIdStatic.Of(field.Id, field.Name); } + public static IEnumerable NonHidden(this FieldCollection fields, bool withHidden = false) where T : IField + { + return fields.Ordered.ForApi(withHidden); + } + + public static IEnumerable ForApi(this IEnumerable fields, bool withHidden = false) where T : IField + { + return fields.Where(x => IsForApi(x, withHidden)); + } + + public static bool IsForApi(this T field, bool withHidden = false) where T : IField + { + return (withHidden || !field.IsHidden) && !(field is IField); + } + public static Schema ReorderFields(this Schema schema, List ids, long? parentId = null) { if (parentId != null) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs index 1d3b67e74..253af69a7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent public static FieldConverter ExcludeHidden() { - return (data, field) => field.IsHidden ? null : data; + return (data, field) => !field.IsForApi() ? null : data; } public static FieldConverter ExcludeChangedTypes() diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs index 2db70c052..583c287a5 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent public static ValueConverter ExcludeHidden() { - return (value, field) => field.IsHidden ? Value.Unset : value; + return (value, field) => !field.IsForApi() ? Value.Unset : value; } public static ValueConverter ExcludeChangedTypes() diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs index 45cbd8fd7..55fa21cb7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema foreach (var field in schema.FieldsByName.Values) { - if (!withHidden && field.IsHidden) + if (!field.IsForApi(withHidden)) { continue; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs index 2e3a041c2..c41f465d2 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Linq; using NJsonSchema; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; @@ -25,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver); var jsonSchema = new JsonSchema4 { Type = JsonObjectType.Object }; - foreach (var field in schema.Fields.Where(x => !x.IsHidden)) + foreach (var field in schema.Fields.ForApi()) { var partitionObject = Builder.Object(); var partition = partitionResolver(field.Partitioning); diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs index 68bc679be..124b3eaec 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -7,7 +7,6 @@ using System; using System.Collections.ObjectModel; -using System.Linq; using NJsonSchema; using Squidex.Domain.Apps.Core.Schemas; @@ -26,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { var item = Builder.Object(); - foreach (var nestedField in field.Fields.Where(x => !x.IsHidden)) + foreach (var nestedField in field.Fields.ForApi()) { var childProperty = nestedField.Accept(this); diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index e67652f9c..c2027c85b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -21,7 +21,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { public sealed class ContentValidator { - private static readonly ContentFieldData DefaultFieldData = new ContentFieldData(); private readonly Schema schema; private readonly PartitionResolver partitionResolver; private readonly ValidationContext context; @@ -35,6 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) { Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(context, nameof(context)); Guard.NotNull(partitionResolver, nameof(partitionResolver)); this.schema = schema; @@ -71,19 +71,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var fieldsValidators = new Dictionary(schema.Fields.Count); - foreach (var field in schema.FieldsByName) + foreach (var field in schema.Fields) { - fieldsValidators[field.Key] = (!field.Value.RawProperties.IsRequired, CreateFieldValidator(field.Value, isPartial)); + fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); } - return new ObjectValidator(fieldsValidators, isPartial, "field", DefaultFieldData); + return new ObjectValidator(fieldsValidators, isPartial, "field"); } private IValidator CreateFieldValidator(IRootField field, bool isPartial) { var partitioning = partitionResolver(field.Partitioning); - var fieldValidator = new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field); + var fieldValidator = field.CreateValidator(); var fieldsValidators = new Dictionary(); foreach (var partition in partitioning) @@ -91,11 +91,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator); } - var isLanguage = field.Partitioning.Equals(Partitioning.Language); + return new AggregateValidator( + field.CreateBagValidator() + .Union(Enumerable.Repeat( + new ObjectValidator(fieldsValidators, isPartial, TypeName(field)), 1))); + } - var type = isLanguage ? "language" : "invariant value"; + private static string TypeName(IRootField field) + { + var isLanguage = field.Partitioning.Equals(Partitioning.Language); - return new ObjectValidator(fieldsValidators, isPartial, type, JsonValue.Null); + return isLanguage ? "language" : "invariant value"; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs new file mode 100644 index 000000000..9da804a22 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Extensions.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class Extensions + { + public static FieldValidator CreateValidator(this IField field) + { + return new FieldValidator(CreateValueValidators(field), field); + } + + private static IEnumerable CreateValueValidators(IField field) + { + return FieldValueValidatorsFactory.CreateValidators(field); + } + + public static IEnumerable CreateBagValidator(this IField field) + { + return FieldBagValidatorsFactory.CreateValidators(field); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs new file mode 100644 index 000000000..79747c6b8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldBagValidatorsFactory.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class FieldBagValidatorsFactory : IFieldVisitor> + { + private static readonly FieldBagValidatorsFactory Instance = new FieldBagValidatorsFactory(); + + private FieldBagValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(IField field) + { + Guard.NotNull(field, nameof(field)); + + return field.Accept(Instance); + } + + public IEnumerable Visit(IArrayField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield break; + } + + public IEnumerable Visit(IField field) + { + yield return NoValueValidator.Instance; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs similarity index 92% rename from src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs rename to src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs index 3feff106e..a73183637 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Linq; using NodaTime; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent.Validators; @@ -16,11 +15,11 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ValidateContent { - public sealed class ValidatorsFactory : IFieldVisitor> + public sealed class FieldValueValidatorsFactory : IFieldVisitor> { - private static readonly ValidatorsFactory Instance = new ValidatorsFactory(); + private static readonly FieldValueValidatorsFactory Instance = new FieldValueValidatorsFactory(); - private ValidatorsFactory() + private FieldValueValidatorsFactory() { } @@ -42,10 +41,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent foreach (var nestedField in field.Fields) { - nestedSchema[nestedField.Name] = (false, new FieldValidator(nestedField.Accept(this).ToArray(), nestedField)); + nestedSchema[nestedField.Name] = (false, nestedField.CreateValidator()); } - yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field", JsonValue.Null)); + yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); } public IEnumerable Visit(IField field) @@ -186,7 +185,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - yield return NoValueValidator.Instance; + if (field is INestedField) + { + yield return NoValueValidator.Instance; + } } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs new file mode 100644 index 000000000..a15507007 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Undefined.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class Undefined + { + public static readonly object Value = new object(); + + public static bool IsUndefined(this object other) + { + return ReferenceEquals(other, Value); + } + + public static bool IsNullOrUndefined(this object other) + { + return other == null || other.IsUndefined(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs new file mode 100644 index 000000000..16b842801 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// 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; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AggregateValidator : IValidator + { + private readonly IValidator[] validators; + + public AggregateValidator(IEnumerable validators) + { + this.validators = validators?.ToArray(); + } + + public Task ValidateAsync(object value, ValidationContext context, AddError addError) + { + if (validators?.Length > 0) + { + return Task.WhenAll(validators.Select(x => x.ValidateAsync(value, context, addError))); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index 40bf1a89d..eafa95d22 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -6,8 +6,10 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators @@ -17,9 +19,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators private readonly IValidator[] validators; private readonly IField field; - public FieldValidator(IValidator[] validators, IField field) + public FieldValidator(IEnumerable validators, IField field) { - this.validators = validators; + Guard.NotNull(field, nameof(field)); + + this.validators = validators.ToArray(); + this.field = field; } @@ -27,21 +32,35 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { try { - object typedValue = null; + var typedValue = value; - if (value is IJsonValue jsonValue) + if (value == null) { - typedValue = jsonValue.Type == JsonValueType.Null ? null : JsonValueConverter.ConvertValue(field, jsonValue); + typedValue = Undefined.Value; } - - var tasks = new List(); - - foreach (var validator in validators) + else if (value is IJsonValue jsonValue) { - tasks.Add(validator.ValidateAsync(typedValue, context, addError)); + if (jsonValue.Type == JsonValueType.Null) + { + typedValue = null; + } + else + { + typedValue = JsonValueConverter.ConvertValue(field, jsonValue); + } } - await Task.WhenAll(tasks); + if (validators?.Length > 0) + { + var tasks = new List(); + + foreach (var validator in validators) + { + tasks.Add(validator.ValidateAsync(typedValue, context, addError)); + } + + await Task.WhenAll(tasks); + } } catch { diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs index cf83164d4..835a10d31 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public Task ValidateAsync(object value, ValidationContext context, AddError addError) { - if (value != null) + if (!value.IsUndefined()) { addError(context.Path, "Value must not be defined."); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs index 429962996..6c2b26246 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -12,21 +12,25 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public sealed class ObjectValidator : IValidator { + private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); private readonly IDictionary schema; private readonly bool isPartial; private readonly string fieldType; - private readonly TValue fieldDefault; - public ObjectValidator(IDictionary schema, bool isPartial, string fieldType, TValue fieldDefault) + public ObjectValidator(IDictionary schema, bool isPartial, string fieldType) { this.schema = schema; - this.fieldDefault = fieldDefault; this.fieldType = fieldType; this.isPartial = isPartial; } public async Task ValidateAsync(object value, ValidationContext context, AddError addError) { + if (value == null) + { + value = DefaultValue; + } + if (value is IReadOnlyDictionary values) { foreach (var fieldData in values) @@ -52,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators continue; } - fieldValue = fieldDefault; + fieldValue = default; } var (isOptional, validator) = field.Value; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs index 022d2318f..129f88dab 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -26,12 +26,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators return TaskHelper.Done; } - if (value == null || (value is string stringValue && validateEmptyStrings && string.IsNullOrWhiteSpace(stringValue))) + if (value.IsNullOrUndefined() || IsEmptyString(value)) { addError(context.Path, "Field is required."); } return TaskHelper.Done; } + + private bool IsEmptyString(object value) + { + return value is string typed && validateEmptyStrings && string.IsNullOrWhiteSpace(typed); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs index 6d2d308a6..6a92a3671 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public Task ValidateAsync(object value, ValidationContext context, AddError addError) { - if (value == null && !context.IsOptional) + if (value.IsNullOrUndefined() && !context.IsOptional) { addError(context.Path, "Field is required."); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index 4ea0623a7..c25cd48bf 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Linq; using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; @@ -26,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = $"{schemaType}DataDto"; - foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden)) + foreach (var field in schema.SchemaDef.Fields.ForApi()) { var (resolvedType, valueResolver) = model.GetGraphType(schema, field); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs index 35c0a1b2e..afbb92b38 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = $"{schemaType}{fieldName}ChildDto"; - foreach (var nestedField in field.Fields.Where(x => !x.IsHidden)) + foreach (var nestedField in field.Fields.ForApi()) { var fieldInfo = model.GetGraphType(schema, nestedField); diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 75c5816b5..a6098056f 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -1,6 +1,6 @@
-
+
- -
+
({ field, control: this.itemForm.get(field.name)! })).filter(x => !x.field.properties.isNormalField || !!x.control); + this.fieldControls = this.field.nested.map(field => ({ field, control: this.itemForm.get(field.name)! })).filter(x => !x.field.properties.isContentField || !!x.control); } } diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 9dff90fd7..506a43f4b 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -1,5 +1,5 @@ -
- +
+ diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html index ad414cce5..b47989637 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html @@ -44,7 +44,7 @@ placeholder="Enter field name" sqxFocusOnInit />
-
+
-
+
diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index bf23df6a0..11172cf8b 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -71,7 +71,7 @@ export class SchemaDetailsDto extends SchemaDto { protected onCloned() { if (this.fields) { - let fields = this.fields.filter(x => x.properties.isListField && x.properties.isNormalField); + let fields = this.fields.filter(x => x.properties.isListField && x.properties.isContentField); if (fields.length === 0 && this.fields.length > 0) { fields = [this.fields[0]]; diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/src/Squidex/app/shared/services/schemas.types.ts index e1a4e6a6b..c9df2017c 100644 --- a/src/Squidex/app/shared/services/schemas.types.ts +++ b/src/Squidex/app/shared/services/schemas.types.ts @@ -158,7 +158,7 @@ export abstract class FieldPropertiesDto { return true; } - public get isNormalField() { + public get isContentField() { return true; } @@ -407,7 +407,7 @@ export class UIFieldPropertiesDto extends FieldPropertiesDto { return false; } - public get isNormalField() { + public get isContentField() { return false; } diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/src/Squidex/app/shared/state/schemas.forms.ts index b434ae8e9..5442beb61 100644 --- a/src/Squidex/app/shared/state/schemas.forms.ts +++ b/src/Squidex/app/shared/state/schemas.forms.ts @@ -182,7 +182,7 @@ export class EditSchemaForm extends Form { - public isNormalField = value$(this.form.get('type')!).pipe(map(x => x !== 'UI')); + public isContentField = value$(this.form.get('type')!).pipe(map(x => x !== 'UI')); constructor(formBuilder: FormBuilder) { super(formBuilder.group({ diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index 8c87279ec..3fcacafd0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema void CheckField(IField field) { - if (field is IField || field.IsHidden) + if (!field.IsForApi()) { Assert.DoesNotContain(field.Name, jsonProperties); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 1c1601934..496fc96ca 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -122,6 +122,24 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent }); } + [Fact] + public async Task Should_add_error_if_required_data_string_field_is_not_in_bag() + { + schema = schema.AddString(1, "my-field", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new NamedContentData(); + + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Field is required.", "my-field") + }); + } + [Fact] public async Task Should_add_error_if_data_contains_invalid_language() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs index f7f46592e..deb78c479 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -5,10 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -27,23 +31,24 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent } [Fact] - public async Task Should_not_add_error_if_value_is_null() + public async Task Should_not_add_error_if_value_is_undefined() { var sut = Field(new UIFieldProperties()); - await sut.ValidateAsync(null, errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); Assert.Empty(errors); } [Fact] - public async Task Should_not_add_error_if_value_is_json_null() + public async Task Should_add_error_if_value_is_json_null() { var sut = Field(new UIFieldProperties()); await sut.ValidateAsync(JsonValue.Null, errors); - Assert.Empty(errors); + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); } [Fact] @@ -57,9 +62,66 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Value must not be defined." }); } - private static RootField Field(UIFieldProperties properties) + [Fact] + public async Task Should_add_error_if_field_object_is_defined() + { + var schema = + new Schema("my-schema") + .AddUI(1, "my-ui1", Partitioning.Invariant) + .AddUI(2, "my-ui2", Partitioning.Invariant); + + var data = + new NamedContentData() + .AddField("my-ui1", new ContentFieldData()) + .AddField("my-ui2", new ContentFieldData() + .AddValue("iv", null)); + + var validationContext = ValidationTestExtensions.ValidContext; + var validator = new ContentValidator(schema, new PartitionResolver(x => InvariantPartitioning.Instance), validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] { + new ValidationError("Value must not be defined.", "my-ui1"), + new ValidationError("Value must not be defined.", "my-ui2") + }); + } + + [Fact] + public async Task Should_add_error_if_array_item_field_is_defined() + { + var schema = + new Schema("my-schema") + .AddArray(1, "my-array", Partitioning.Invariant, array => array + .AddUI(101, "my-ui")); + + var data = + new NamedContentData() + .AddField("my-array", new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("my-ui", null)))); + + var validationContext = + new ValidationContext( + Guid.NewGuid(), + Guid.NewGuid(), + (c, s) => null, + (c) => null); + + var validator = new ContentValidator(schema, new PartitionResolver(x => InvariantPartitioning.Instance), validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); + } + + private static NestedField Field(UIFieldProperties properties) { - return Fields.UI(1, "my-ui", Partitioning.Invariant, properties); + return new NestedField(1, "my-ui", properties); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index ccd2aa9ee..52dc64bce 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { @@ -38,18 +37,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent CreateFormatter(errors)); } - public static Task ValidateAsync(this IField field, IJsonValue value, IList errors, ValidationContext context = null) + public static Task ValidateAsync(this IField field, object value, IList errors, ValidationContext context = null) { - return new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field) + return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) .ValidateAsync( value, CreateContext(context), CreateFormatter(errors)); } - public static Task ValidateOptionalAsync(this IField field, IJsonValue value, IList errors, ValidationContext context = null) + public static Task ValidateOptionalAsync(this IField field, object value, IList errors, ValidationContext context = null) { - return new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field) + return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) .ValidateAsync( value, CreateContext(context).Optional(true), diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs index 428924d69..4c4e0c028 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -19,11 +20,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators private readonly List errors = new List(); [Fact] - public async Task Should_not_add_error_if_value_is_null() + public async Task Should_not_add_error_if_value_is_undefined() { var sut = NoValueValidator.Instance; - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(Undefined.Value, errors); Assert.Empty(errors); }