From 7bc262fff376004bfb0f932ad250beb8be45b6ac Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 12 Oct 2021 19:14:28 +0200 Subject: [PATCH] Unique array fields. (#768) * Unique array fields. * Fix clear indicator. --- backend/i18n/frontend_en.json | 2 + backend/i18n/frontend_it.json | 2 + backend/i18n/frontend_nl.json | 2 + backend/i18n/frontend_zh.json | 2 + backend/i18n/source/backend_en.json | 1 + backend/i18n/source/frontend_en.json | 2 + .../Schemas/ArrayField.cs | 7 +- .../Schemas/ArrayFieldProperties.cs | 3 + .../Schemas/ComponentsFieldProperties.cs | 2 + .../Schemas/Fields.cs | 9 +- .../DefaultFieldValueValidatorsFactory.cs | 10 + .../ValidateContent/JsonValueConverter.cs | 4 +- .../Validators/UniqueObjectValuesValidator.cs | 59 ++ backend/src/Squidex.Shared/Texts.it.resx | 3 + backend/src/Squidex.Shared/Texts.nl.resx | 3 + backend/src/Squidex.Shared/Texts.resx | 3 + backend/src/Squidex.Shared/Texts.zh.resx | 3 + .../Models/Fields/ArrayFieldPropertiesDto.cs | 6 + .../Fields/ComponentsFieldPropertiesDto.cs | 5 + .../Model/Schemas/ArrayFieldTests.cs | 6 +- .../Model/Schemas/SchemaFieldTests.cs | 4 +- .../Model/Schemas/SchemaTests.cs | 6 +- .../ConvertContent/ValueConvertersTests.cs | 4 +- .../DefaultValues/DefaultValuesTests.cs | 30 +- .../SchemaSynchronizerTests.cs | 6 +- .../ReferenceExtractionTests.cs | 12 +- .../ValidateContent/ArrayFieldTests.cs | 49 +- .../ValidateContent/AssetsFieldTests.cs | 4 +- .../ValidateContent/BooleanFieldTests.cs | 4 +- .../ValidateContent/ComponentFieldTests.cs | 16 +- .../ValidateContent/ComponentsFieldTests.cs | 33 +- .../ValidateContent/ContentValidationTests.cs | 108 ++-- .../ValidateContent/DateTimeFieldTests.cs | 4 +- .../ValidateContent/GeolocationFieldTests.cs | 4 +- .../ValidateContent/JsonFieldTests.cs | 4 +- .../ValidateContent/NumberFieldTests.cs | 4 +- .../ValidateContent/ReferencesFieldTests.cs | 4 +- .../ValidateContent/StringFieldTests.cs | 4 +- .../ValidateContent/TagsFieldTests.cs | 4 +- .../ValidateContent/UIFieldTests.cs | 26 +- .../Validators/UniqueObjectValuesValidator.cs | 118 ++++ .../Validators/UniqueValidatorTests.cs | 60 +- .../shared/forms/field-editor.component.ts | 4 +- .../schema/fields/field-wizard.component.html | 3 +- .../pages/schema/fields/field.component.html | 1 + .../forms/field-form-common.component.ts | 7 +- .../fields/forms/field-form-ui.component.ts | 7 +- .../field-form-validation.component.html | 4 +- .../forms/field-form-validation.component.ts | 7 +- .../fields/forms/field-form.component.html | 7 +- .../fields/forms/field-form.component.ts | 7 +- .../types/array-validation.component.html | 8 + .../components-validation.component.html | 8 + .../types/number-validation.component.ts | 9 +- .../types/string-validation.component.ts | 9 +- .../pages/schema/schema-page.component.html | 18 +- .../angular/forms/validators.spec.ts | 564 ++++++++++-------- .../app/framework/angular/forms/validators.ts | 41 +- frontend/app/shared/services/schemas.types.ts | 2 + .../shared/state/contents.forms.visitors.ts | 8 + frontend/app/shared/state/schemas.forms.ts | 2 + 61 files changed, 888 insertions(+), 470 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 6aa12476c..f9c75c249 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -798,6 +798,7 @@ "schemas.fieldTypes.array.countMax": "Max Items", "schemas.fieldTypes.array.countMin": "Min Items", "schemas.fieldTypes.array.description": "List of embedded objects.", + "schemas.fieldTypes.array.uniqueFields": "Unique Fields", "schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values", "schemas.fieldTypes.assets.count": "Count", "schemas.fieldTypes.assets.countMax": "Max Assets", @@ -1006,6 +1007,7 @@ "validation.patternmessage": "{message}", "validation.required": "{field|upper} is required.", "validation.requiredTrue": "{field|upper} is required.", + "validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.", "validation.uniquestrings": "{field|upper} must not contain duplicate values.", "validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.", "validation.validdatetime": "{field|upper} is not a valid date time.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 1935fb3a5..7baf0d6cb 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -798,6 +798,7 @@ "schemas.fieldTypes.array.countMax": "Max num. Elementi", "schemas.fieldTypes.array.countMin": "Min num. Elementi", "schemas.fieldTypes.array.description": "Lista di oggetti incorporati.", + "schemas.fieldTypes.array.uniqueFields": "Unique Fields", "schemas.fieldTypes.assets.allowDuplicates": "Consente valori duplicati", "schemas.fieldTypes.assets.count": "Conteggio", "schemas.fieldTypes.assets.countMax": "Max num di Risorse", @@ -1006,6 +1007,7 @@ "validation.patternmessage": "{message}", "validation.required": "{field|upper} è obbligatorio.", "validation.requiredTrue": "{field|upper} è obbligatorio.", + "validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.", "validation.uniquestrings": "{field|upper} non deve contenere valori duplicati.", "validation.validarrayvalues": "{field|upper} contiene valori non validicontains an invalid value: {invalidvalue}.", "validation.validdatetime": "{field|upper} non è una data e ora valida.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 11179f499..a8383f776 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -798,6 +798,7 @@ "schemas.fieldTypes.array.countMax": "Max. aantal items", "schemas.fieldTypes.array.countMin": "Min. items", "schemas.fieldTypes.array.description": "Lijst met ingesloten objecten.", + "schemas.fieldTypes.array.uniqueFields": "Unique Fields", "schemas.fieldTypes.assets.allowDuplicates": "Dubbele waarden toestaan", "schemas.fieldTypes.assets.count": "Tellen", "schemas.fieldTypes.assets.countMax": "Max. bestanden", @@ -1006,6 +1007,7 @@ "validation.patternmessage": "{message}", "validation.required": "{field|upper} is vereist.", "validation.requiredTrue": "{field|upper} is vereist.", + "validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.", "validation.uniquestrings": "{field|upper} mag geen dubbele waarden bevatten.", "validation.validarrayvalues": "{field|upper} bevat een ongeldige waarde: {invalidvalue}.", "validation.validdatetime": "{field|upper} is geen geldige datum en tijd.", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index c6bde0dea..bf349ed5e 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -798,6 +798,7 @@ "schemas.fieldTypes.array.countMax": "最大项目数", "schemas.fieldTypes.array.countMin": "最小项目", "schemas.fieldTypes.array.description": "嵌入对象列表。", + "schemas.fieldTypes.array.uniqueFields": "Unique Fields", "schemas.fieldTypes.assets.allowDuplicates": "允许重复值", "schemas.fieldTypes.assets.count": "计数", "schemas.fieldTypes.assets.countMax": "最大资源", @@ -1006,6 +1007,7 @@ "validation.patternmessage": "{message}", "validation.required": "{field|upper} 是必需的。", "validation.requiredTrue": "{field|upper} 是必需的。", + "validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.", "validation.uniquestrings": "{field|upper} 不得包含重复值。", "validation.validarrayvalues": "{field|upper} 包含无效值:{invalidvalue}。", "validation.validdatetime": "{field|upper} 不是有效的日期时间。", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 4d3b55c77..21d2689df 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -183,6 +183,7 @@ "contents.validation.regexTooSlow": "Regex is too slow.", "contents.validation.required": "Field is required.", "contents.validation.unique": "Another content with the same value exists.", + "contents.validation.uniqueObjectValues": "Must not contain items with duplicate '{field}' fields.", "contents.validation.unknownField": "Not a known {fieldType}.", "contents.validation.wordCount": "Must have exactly {count} word(s).", "contents.validation.wordsBetween": "Must have between {min} and {max} word(s).", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 6aa12476c..f9c75c249 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -798,6 +798,7 @@ "schemas.fieldTypes.array.countMax": "Max Items", "schemas.fieldTypes.array.countMin": "Min Items", "schemas.fieldTypes.array.description": "List of embedded objects.", + "schemas.fieldTypes.array.uniqueFields": "Unique Fields", "schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values", "schemas.fieldTypes.assets.count": "Count", "schemas.fieldTypes.assets.countMax": "Max Assets", @@ -1006,6 +1007,7 @@ "validation.patternmessage": "{message}", "validation.required": "{field|upper} is required.", "validation.requiredTrue": "{field|upper} is required.", + "validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.", "validation.uniquestrings": "{field|upper} must not contain duplicate values.", "validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.", "validation.validdatetime": "{field|upper} is not a valid date time.", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs index 1e11d9d9f..47f33d8d4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -30,13 +30,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public FieldCollection FieldCollection { get; private set; } = FieldCollection.Empty; - public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) - : base(id, name, partitioning, properties, settings) - { - } - public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) - : this(id, name, partitioning, properties, settings) + : base(id, name, partitioning, properties, settings) { FieldCollection = new FieldCollection(fields); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs index 7256cb2d7..8364b5f7a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using Squidex.Infrastructure.Collections; namespace Squidex.Domain.Apps.Core.Schemas { @@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public int? MaxItems { get; init; } + public ImmutableList? UniqueFields { get; init; } + public override T Accept(IFieldPropertiesVisitor visitor, TArgs args) { return visitor.Visit(this, args); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs index 519347a86..8e4696c12 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public int? MaxItems { get; init; } + public ImmutableList? UniqueFields { get; init; } + public DomainId SchemaId { init diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs index d3762d740..07adb5931 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs @@ -11,15 +11,10 @@ namespace Squidex.Domain.Apps.Core.Schemas { public static class Fields { - public static ArrayField Array(long id, string name, Partitioning partitioning, params NestedField[] fields) - { - return new ArrayField(id, name, partitioning, fields); - } - public static ArrayField Array(long id, string name, Partitioning partitioning, - ArrayFieldProperties? properties = null, IFieldSettings? settings = null) + ArrayFieldProperties? properties = null, IFieldSettings? settings = null, params NestedField[] fields) { - return new ArrayField(id, name, partitioning, properties, settings); + return new ArrayField(id, name, partitioning, fields, properties, settings); } public static RootField Assets(long id, string name, Partitioning partitioning, diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index f0528adde..937ada2bc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -45,6 +45,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } + if (properties.UniqueFields?.Count > 0) + { + yield return new UniqueObjectValuesValidator(properties.UniqueFields); + } + var nestedValidators = new Dictionary(field.Fields.Count); foreach (var nestedField in field.Fields) @@ -97,6 +102,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } + if (properties.UniqueFields?.Count > 0) + { + yield return new UniqueObjectValuesValidator(properties.UniqueFields); + } + yield return new CollectionItemValidator(ComponentValidator(args.Factory)); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 8c4e54f2d..c80951217 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -222,7 +222,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { if (value is JsonArray array) { - var result = new List(array.Count); + var result = new List(array.Count); for (var i = 0; i < array.Count; i++) { @@ -245,7 +245,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); } - private static (object? Result, JsonError? Error) ConvertToComponent(IJsonValue value, + private static (Component? Result, JsonError? Error) ConvertToComponent(IJsonValue value, ResolvedComponents components, ImmutableList? allowedIds) { if (value is not JsonObject obj) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs new file mode 100644 index 000000000..b7f50e871 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// 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.Contents; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Translations; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class UniqueObjectValuesValidator : IValidator + { + private readonly IEnumerable fields; + + public UniqueObjectValuesValidator(IEnumerable fields) + { + this.fields = fields; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is IEnumerable objects && objects.Count() > 1) + { + Validate(context, addError, objects); + } + else if (value is IEnumerable components && components.Count() > 1) + { + Validate(context, addError, components.Select(x => x.Data)); + } + + return Task.CompletedTask; + } + + private void Validate(ValidationContext context, AddError addError, IEnumerable items) + { + var duplicates = new HashSet(10); + + foreach (var field in fields) + { + duplicates.Clear(); + + foreach (var item in items) + { + if (item.TryGetValue(field, out var fieldValue) && !duplicates.Add(fieldValue)) + { + addError(context.Path, T.Get("contents.validation.uniqueObjectValues", new { field })); + break; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 11c8d00be..82288bb53 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -634,6 +634,9 @@ Esiste un altro contenuto con lo stesso valore. + + Must not contain items with duplicate '{field}' fields. + Non è noto {fieldType}. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 69bf31b6b..f9d86d001 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -634,6 +634,9 @@ Er bestaat een andere inhoud met dezelfde waarde. + + Must not contain items with duplicate '{field}' fields. + Onbekend {fieldType}. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index c31761c3f..7b77959b7 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -634,6 +634,9 @@ Another content with the same value exists. + + Must not contain items with duplicate '{field}' fields. + Not a known {fieldType}. diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index 1625b6173..2e9fe5a0a 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -634,6 +634,9 @@ 存在另一个具有相同值的内容。 + + Must not contain items with duplicate '{field}' fields. + 不是已知的 {fieldType}。 diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs index 6bc508a6f..3691c93b7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields @@ -22,6 +23,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public int? MaxItems { get; set; } + /// + /// The fields that must be unique. + /// + public ImmutableList? UniqueFields { get; set; } + public override FieldProperties ToProperties() { var result = SimpleMapper.Map(this, new ArrayFieldProperties()); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs index 048c7f4c0..912d899fc 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs @@ -29,6 +29,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public ImmutableList? SchemaIds { get; set; } + /// + /// The fields that must be unique. + /// + public ImmutableList? UniqueFields { get; set; } + public override FieldProperties ToProperties() { var result = SimpleMapper.Map(this, new ComponentsFieldProperties()); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs index 02e9fbebe..2ff8772c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var parent_1 = parent_0.AddField(CreateField(1)); - Assert.Throws(() => parent_1.AddNumber(2, "my-field-1")); + Assert.Throws(() => parent_1.AddNumber(2, "myField1")); } [Fact] @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var parent_1 = parent_0.AddField(CreateField(1)); - Assert.Throws(() => parent_1.AddNumber(1, "my-field-2")); + Assert.Throws(() => parent_1.AddNumber(1, "myField2")); } [Fact] @@ -238,7 +238,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas private static NestedField CreateField(int id) { - return Fields.Number(id, $"my-field-{id}"); + return Fields.Number(id, $"myField{id}"); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs index b6a5f8aec..e3853c3d7 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs @@ -24,12 +24,12 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas .Select(x => new[] { x }) .ToList()!; - private readonly RootField field_0 = Fields.Number(1, "my-field", Partitioning.Invariant); + private readonly RootField field_0 = Fields.Number(1, "myField", Partitioning.Invariant); [Fact] public void Should_instantiate_field() { - Assert.Equal("my-field", field_0.Name); + Assert.Equal("myField", field_0.Name); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index d975e1616..d3d92f2a3 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var schema_1 = schema_0.AddField(CreateField(1)); - Assert.Throws(() => schema_1.AddNumber(2, "my-field-1", Partitioning.Invariant)); + Assert.Throws(() => schema_1.AddNumber(2, "myField1", Partitioning.Invariant)); } [Fact] @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { var schema_1 = schema_0.AddField(CreateField(1)); - Assert.Throws(() => schema_1.AddNumber(1, "my-field-2", Partitioning.Invariant)); + Assert.Throws(() => schema_1.AddNumber(1, "myField2", Partitioning.Invariant)); } [Fact] @@ -498,7 +498,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas private static RootField CreateField(int id) { - return Fields.Number(id, $"my-field-{id}", Partitioning.Invariant); + return Fields.Number(id, $"myField{id}", Partitioning.Invariant); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs index db01389b2..87f71bcff 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent [InlineData("*")] public void Should_convert_nested_asset_ids_to_urls(string path) { - var field = Fields.Array(1, "parent", Partitioning.Invariant, Fields.Assets(11, "assets")); + var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets")); var source = JsonValue.Array(id1, id2); @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent [InlineData("other.assets")] public void Should_not_convert_nested_asset_ids_if_field_name_does_not_match(string path) { - var field = Fields.Array(1, "parent", Partitioning.Invariant, Fields.Assets(11, "assets")); + var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets")); var source = JsonValue.Array(id1, id2); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs index 2a9eaf9ea..ad0a363c4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs @@ -29,13 +29,13 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues { schema = new Schema("my-schema") - .AddString(1, "my-string", Partitioning.Language, + .AddString(1, "myString", Partitioning.Language, new StringFieldProperties { DefaultValue = "en-string" }) - .AddNumber(2, "my-number", Partitioning.Invariant, + .AddNumber(2, "myNumber", Partitioning.Invariant, new NumberFieldProperties()) - .AddDateTime(3, "my-datetime", Partitioning.Invariant, + .AddDateTime(3, "myDatetime", Partitioning.Invariant, new DateTimeFieldProperties { DefaultValue = now }) - .AddBoolean(4, "my-boolean", Partitioning.Invariant, + .AddBoolean(4, "myBoolean", Partitioning.Invariant, new BooleanFieldProperties { DefaultValue = true }); } @@ -44,23 +44,23 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues { var data = new ContentData() - .AddField("my-string", + .AddField("myString", new ContentFieldData() .AddLocalized("de", "de-string")) - .AddField("my-number", + .AddField("myNumber", new ContentFieldData() .AddInvariant(456)); data.GenerateDefaultValues(schema, languagesConfig.ToResolver()); - Assert.Equal(456, ((JsonNumber)data["my-number"]!["iv"]).Value); + Assert.Equal(456, ((JsonNumber)data["myNumber"]!["iv"]).Value); - Assert.Equal("de-string", data["my-string"]!["de"].ToString()); - Assert.Equal("en-string", data["my-string"]!["en"].ToString()); + Assert.Equal("de-string", data["myString"]!["de"].ToString()); + Assert.Equal("en-string", data["myString"]!["en"].ToString()); - Assert.Equal(now.ToString(), data["my-datetime"]!["iv"].ToString()); + Assert.Equal(now.ToString(), data["myDatetime"]!["iv"].ToString()); - Assert.True(((JsonBoolean)data["my-boolean"]!["iv"]).Value); + Assert.True(((JsonBoolean)data["myBoolean"]!["iv"]).Value); } [Fact] @@ -68,17 +68,17 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues { var data = new ContentData() - .AddField("my-string", + .AddField("myString", new ContentFieldData() .AddLocalized("de", string.Empty)) - .AddField("my-number", + .AddField("myNumber", new ContentFieldData() .AddInvariant(456)); data.GenerateDefaultValues(schema, languagesConfig.ToResolver()); - Assert.Equal(string.Empty, data["my-string"]!["de"].ToString()); - Assert.Equal("en-string", data["my-string"]!["en"].ToString()); + Assert.Equal(string.Empty, data["myString"]!["de"].ToString()); + Assert.Equal("en-string", data["myString"]!["en"].ToString()); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs index 88e2cfa61..81121aff1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -19,9 +19,9 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization public class SchemaSynchronizerTests { private readonly Func idGenerator; - private readonly NamedId stringId = NamedId.Of(13L, "my-value"); - private readonly NamedId nestedId = NamedId.Of(141L, "my-value"); - private readonly NamedId arrayId = NamedId.Of(14L, "11-array"); + private readonly NamedId stringId = NamedId.Of(13L, "myValue"); + private readonly NamedId nestedId = NamedId.Of(141L, "myValue"); + private readonly NamedId arrayId = NamedId.Of(14L, "11Array"); private int fields = 50; public SchemaSynchronizerTests() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs index ccd469c43..91fbd6bfd 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -204,7 +204,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds [Fact] public void Should_return_empty_list_from_non_references_field() { - var sut = Fields.String(1, "my-string", Partitioning.Invariant); + var sut = Fields.String(1, "myString", Partitioning.Invariant); var result = sut.GetReferencedIds(JsonValue.Create("invalid"), components).ToArray(); @@ -218,7 +218,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds var id1 = DomainId.NewGuid(); var id2 = DomainId.NewGuid(); - var arrayField = Fields.Array(1, "my-array", Partitioning.Invariant, field); + var arrayField = Fields.Array(1, "myArray", Partitioning.Invariant, null, null, field); var value = JsonValue.Array( @@ -305,14 +305,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds public static IEnumerable ReferencingNestedFields() { - yield return new object[] { Fields.References(1, "my-refs") }; - yield return new object[] { Fields.Assets(1, "my-assets") }; + yield return new object[] { Fields.References(1, "myRefs") }; + yield return new object[] { Fields.Assets(1, "myAssets") }; } public static IEnumerable ReferencingFields() { - yield return new object[] { Fields.References(1, "my-refs", Partitioning.Invariant) }; - yield return new object[] { Fields.Assets(1, "my-assets", Partitioning.Invariant) }; + yield return new object[] { Fields.References(1, "myRefs", Partitioning.Invariant) }; + yield return new object[] { Fields.Assets(1, "myAssets", Partitioning.Invariant) }; } private static HashSet RandomIds() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs index 91f3871a8..376788ed3 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs @@ -6,11 +6,11 @@ // ========================================================================== using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ArrayFieldProperties()); - Assert.Equal("my-array", sut.Name); + Assert.Equal("myArray", sut.Name); } [Fact] @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ArrayFieldProperties()); - await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors); + await sut.ValidateAsync(CreateValue(Object()), errors); Assert.Empty(errors); } @@ -53,7 +53,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + await sut.ValidateAsync(CreateValue(Object(), Object()), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_value_has_not_duplicates() + { + var sut = Field(new ArrayFieldProperties { UniqueFields = ImmutableList.Create("myString") }); + + await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "2")), errors); Assert.Empty(errors); } @@ -96,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ArrayFieldProperties { MinItems = 3 }); - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + await sut.ValidateAsync(CreateValue(Object(), Object()), errors); errors.Should().BeEquivalentTo( new[] { "Must have at least 3 item(s)." }); @@ -107,20 +117,41 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ArrayFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors); + await sut.ValidateAsync(CreateValue(Object(), Object()), errors); errors.Should().BeEquivalentTo( new[] { "Must not have more than 1 item(s)." }); } - private static IJsonValue CreateValue(params JsonObject[]? ids) + [Fact] + public async Task Should_add_error_if_value_has_duplicates() + { + var sut = Field(new ArrayFieldProperties { UniqueFields = ImmutableList.Create("myString") }); + + await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "1")), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain items with duplicate 'myString' fields." }); + } + + private static IJsonValue CreateValue(params JsonObject[]? objects) + { + return objects == null ? JsonValue.Null : JsonValue.Array(objects); + } + + private static JsonObject Object() + { + return JsonValue.Object(); + } + + private static JsonObject Object(string key, object value) { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType().ToArray()); + return JsonValue.Object().Add(key, value); } private static RootField Field(ArrayFieldProperties properties) { - return Fields.Array(1, "my-array", Partitioning.Invariant, properties); + return Fields.Array(1, "myArray", Partitioning.Invariant, properties, null, Fields.String(2, "myString")); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 9e32b2d7b..3d4d297b1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties()); - Assert.Equal("my-assets", sut.Name); + Assert.Equal("myAssets", sut.Name); } [Fact] @@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(AssetsFieldProperties properties) { - return Fields.Assets(1, "my-assets", Partitioning.Invariant, properties); + return Fields.Assets(1, "myAssets", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs index e360a4892..4cdffcd3f 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new BooleanFieldProperties()); - Assert.Equal("my-boolean", sut.Name); + Assert.Equal("myBoolean", sut.Name); } [Fact] @@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(BooleanFieldProperties properties) { - return Fields.Boolean(1, "my-boolean", Partitioning.Invariant, properties); + return Fields.Boolean(1, "myBoolean", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs index ad71eace5..d20736286 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (_, sut, _) = Field(new ComponentFieldProperties()); - Assert.Equal("my-component", sut.Name); + Assert.Equal("myComponent", sut.Name); } [Fact] @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", 1), errors, components: components); + await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", 1), errors, components: components); Assert.Empty(errors); } @@ -68,10 +68,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); - await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", null), errors, components: components); + await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", null), errors, components: components); errors.Should().BeEquivalentTo( - new[] { "component-field: Field is required." }); + new[] { "componentField: Field is required." }); } [Fact] @@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - var value = CreateValue("my-component", "component-field", 1, "schemaName"); + var value = CreateValue("my-component", "componentField", 1, "schemaName"); await sut.ValidateAsync(value, errors, components: components); @@ -136,7 +136,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); - var value = CreateValue(null, "component-field", 1); + var value = CreateValue(null, "componentField", 1); await sut.ValidateAsync(value, errors, components: components); @@ -164,10 +164,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var schema = new Schema("my-component") - .AddNumber(1, "component-field", Partitioning.Invariant, + .AddNumber(1, "componentField", Partitioning.Invariant, new NumberFieldProperties { IsRequired = isRequired }); - var field = Fields.Component(1, "my-component", Partitioning.Invariant, properties); + var field = Fields.Component(1, "myComponent", Partitioning.Invariant, properties); var components = new ResolvedComponents(new Dictionary { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs index 93c8d56ff..962eee475 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (_, sut, _) = Field(new ComponentsFieldProperties()); - Assert.Equal("my-components", sut.Name); + Assert.Equal("myComponents", sut.Name); } [Fact] @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", 1), errors, components: components); + await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", 1), errors, components: components); Assert.Empty(errors); } @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); Assert.Empty(errors); } @@ -78,10 +78,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); - await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", null), errors, components: components); + await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", null), errors, components: components); errors.Should().BeEquivalentTo( - new[] { "[1].component-field: Field is required." }); + new[] { "[1].componentField: Field is required." }); } [Fact] @@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 3 }); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); errors.Should().BeEquivalentTo( new[] { "Must have at least 3 item(s)." }); @@ -155,18 +155,29 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components); + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); errors.Should().BeEquivalentTo( new[] { "Must not have more than 1 item(s)." }); } + [Fact] + public async Task Should_add_error_if_value_has_duplicates() + { + var (id, sut, components) = Field(new ComponentsFieldProperties { UniqueFields = ImmutableList.Create("componentField") }); + + await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain items with duplicate 'componentField' fields." }); + } + [Fact] public async Task Should_resolve_schema_id_from_name() { var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - var value = CreateValue(1, "my-component", "component-field", 1, "schemaName"); + var value = CreateValue(1, "my-component", "componentField", 1, "schemaName"); await sut.ValidateAsync(value, errors, components: components); @@ -179,7 +190,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); - var value = CreateValue(1, null, "component-field", 1); + var value = CreateValue(1, null, "componentField", 1); await sut.ValidateAsync(value, errors, components: components); @@ -214,10 +225,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var schema = new Schema("my-component") - .AddNumber(1, "component-field", Partitioning.Invariant, + .AddNumber(1, "componentField", Partitioning.Invariant, new NumberFieldProperties { IsRequired = isRequired }); - var field = Fields.Components(1, "my-components", Partitioning.Invariant, properties); + var field = Fields.Components(1, "myComponents", Partitioning.Invariant, properties); var components = new ResolvedComponents(new Dictionary { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index ff4a6ade5..57a3f023d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -42,12 +42,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent A.CallTo(() => validatorFactory.CreateValueValidators(A._, A._, A._)) .Returns(Enumerable.Repeat(validator, 1)); - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, new NumberFieldProperties()); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddInvariant(1000)); @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Validation failed with internal error.", "my-field.iv") + new ValidationError("Validation failed with internal error.", "myField.iv") }); } @@ -73,12 +73,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent A.CallTo(() => validatorFactory.CreateFieldValidators(A._, A._, A._)) .Returns(Enumerable.Repeat(validator, 1)); - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, new NumberFieldProperties()); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddInvariant(1000)); @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Validation failed with internal error.", "my-field") + new ValidationError("Validation failed with internal error.", "myField") }); } @@ -111,12 +111,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_validating_data_with_invalid_field() { - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 }); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddInvariant(1000)); @@ -125,18 +125,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Must be less or equal to 100.", "my-field.iv") + new ValidationError("Must be less or equal to 100.", "myField.iv") }); } [Fact] public async Task Should_add_error_if_non_localizable_data_field_contains_language() { - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant); + schema = schema.AddNumber(1, "myField", Partitioning.Invariant); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("es", 1) .AddLocalized("it", 1)); @@ -146,15 +146,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Not a known invariant value.", "my-field.es"), - new ValidationError("Not a known invariant value.", "my-field.it") + new ValidationError("Not a known invariant value.", "myField.es"), + new ValidationError("Not a known invariant value.", "myField.it") }); } [Fact] public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() { - schema = schema.AddNumber(1, "my-field", Partitioning.Language, + schema = schema.AddNumber(1, "myField", Partitioning.Language, new NumberFieldProperties { IsRequired = true }); var data = @@ -165,15 +165,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Field is required.", "my-field.de"), - new ValidationError("Field is required.", "my-field.en") + new ValidationError("Field is required.", "myField.de"), + new ValidationError("Field is required.", "myField.en") }); } [Fact] public async Task Should_add_error_if_required_data_field_is_not_in_bag() { - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, new NumberFieldProperties { IsRequired = true }); var data = @@ -184,14 +184,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Field is required.", "my-field.iv") + new ValidationError("Field is required.", "myField.iv") }); } [Fact] public async Task Should_add_error_if_required_data_string_field_is_not_in_bag() { - schema = schema.AddString(1, "my-field", Partitioning.Invariant, + schema = schema.AddString(1, "myField", Partitioning.Invariant, new StringFieldProperties { IsRequired = true }); var data = @@ -202,18 +202,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Field is required.", "my-field.iv") + new ValidationError("Field is required.", "myField.iv") }); } [Fact] public async Task Should_add_error_if_data_contains_invalid_language() { - schema = schema.AddNumber(1, "my-field", Partitioning.Language); + schema = schema.AddNumber(1, "myField", Partitioning.Language); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("de", 1) .AddLocalized("ru", 1)); @@ -223,7 +223,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Not a known language.", "my-field.ru") + new ValidationError("Not a known language.", "myField.ru") }); } @@ -236,12 +236,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .Set(Language.IT, true) .Remove(Language.EN); - schema = schema.AddString(1, "my-field", Partitioning.Language, + schema = schema.AddString(1, "myField", Partitioning.Language, new StringFieldProperties { IsRequired = true }); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("es", "value")); @@ -253,11 +253,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_data_contains_unsupported_language() { - schema = schema.AddNumber(1, "my-field", Partitioning.Language); + schema = schema.AddNumber(1, "myField", Partitioning.Language); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("es", 1) .AddLocalized("it", 1)); @@ -267,8 +267,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Not a known language.", "my-field.es"), - new ValidationError("Not a known language.", "my-field.it") + new ValidationError("Not a known language.", "myField.es"), + new ValidationError("Not a known language.", "myField.it") }); } @@ -292,12 +292,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_validating_partial_data_with_invalid_field() { - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 }); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddInvariant(1000)); @@ -306,18 +306,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Must be less or equal to 100.", "my-field.iv") + new ValidationError("Must be less or equal to 100.", "myField.iv") }); } [Fact] public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() { - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant); + schema = schema.AddNumber(1, "myField", Partitioning.Invariant); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("es", 1) .AddLocalized("it", 1)); @@ -327,15 +327,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Not a known invariant value.", "my-field.es"), - new ValidationError("Not a known invariant value.", "my-field.it") + new ValidationError("Not a known invariant value.", "myField.es"), + new ValidationError("Not a known invariant value.", "myField.it") }); } [Fact] public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() { - schema = schema.AddNumber(1, "my-field", Partitioning.Language, + schema = schema.AddNumber(1, "myField", Partitioning.Language, new NumberFieldProperties { IsRequired = true }); var data = @@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() { - schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, + schema = schema.AddNumber(1, "myField", Partitioning.Invariant, new NumberFieldProperties { IsRequired = true }); var data = @@ -363,11 +363,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_partial_data_contains_invalid_language() { - schema = schema.AddNumber(1, "my-field", Partitioning.Language); + schema = schema.AddNumber(1, "myField", Partitioning.Language); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("de", 1) .AddLocalized("ru", 1)); @@ -377,18 +377,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Not a known language.", "my-field.ru") + new ValidationError("Not a known language.", "myField.ru") }); } [Fact] public async Task Should_add_error_if_partial_data_contains_unsupported_language() { - schema = schema.AddNumber(1, "my-field", Partitioning.Language); + schema = schema.AddNumber(1, "myField", Partitioning.Language); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddLocalized("es", 1) .AddLocalized("it", 1)); @@ -398,25 +398,25 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Not a known language.", "my-field.es"), - new ValidationError("Not a known language.", "my-field.it") + new ValidationError("Not a known language.", "myField.es"), + new ValidationError("Not a known language.", "myField.it") }); } [Fact] public async Task Should_add_error_if_array_field_has_required_nested_field() { - schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. - AddNumber(2, "my-nested", new NumberFieldProperties { IsRequired = true })); + schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f. + AddNumber(2, "myNested", new NumberFieldProperties { IsRequired = true })); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddInvariant( JsonValue.Array( JsonValue.Object(), - JsonValue.Object().Add("my-nested", 1), + JsonValue.Object().Add("myNested", 1), JsonValue.Object()))); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); @@ -424,8 +424,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("Field is required.", "my-field.iv[1].my-nested"), - new ValidationError("Field is required.", "my-field.iv[3].my-nested") + new ValidationError("Field is required.", "myField.iv[1].myNested"), + new ValidationError("Field is required.", "myField.iv[3].myNested") }); } @@ -445,12 +445,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_nested_separator_not_defined() { - schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. - AddUI(2, "my-nested")); + schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f. + AddUI(2, "myNested")); var data = new ContentData() - .AddField("my-field", + .AddField("myField", new ContentFieldData() .AddInvariant( JsonValue.Array( diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 8af441b53..d2ff87f1d 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new DateTimeFieldProperties()); - Assert.Equal("my-datetime", sut.Name); + Assert.Equal("myDatetime", sut.Name); } [Fact] @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(DateTimeFieldProperties properties) { - return Fields.DateTime(1, "my-datetime", Partitioning.Invariant, properties); + return Fields.DateTime(1, "myDatetime", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs index 0088775ae..646b8649a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new GeolocationFieldProperties()); - Assert.Equal("my-geolocation", sut.Name); + Assert.Equal("myGeolocation", sut.Name); } [Fact] @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(GeolocationFieldProperties properties) { - return Fields.Geolocation(1, "my-geolocation", Partitioning.Invariant, properties); + return Fields.Geolocation(1, "myGeolocation", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs index c1ac3233b..79dcea030 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new JsonFieldProperties()); - Assert.Equal("my-json", sut.Name); + Assert.Equal("myJson", sut.Name); } [Fact] @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(JsonFieldProperties properties) { - return Fields.Json(1, "my-json", Partitioning.Invariant, properties); + return Fields.Json(1, "myJson", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index f31650b83..89934bde0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new NumberFieldProperties()); - Assert.Equal("my-number", sut.Name); + Assert.Equal("myNumber", sut.Name); } [Fact] @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(NumberFieldProperties properties) { - return Fields.Number(1, "my-number", Partitioning.Invariant, properties); + return Fields.Number(1, "myNumber", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 67d23809c..b7d8f73f8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - Assert.Equal("my-refs", sut.Name); + Assert.Equal("myRefs", sut.Name); } [Fact] @@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(ReferencesFieldProperties properties) { - return Fields.References(1, "my-refs", Partitioning.Invariant, properties); + return Fields.References(1, "myRefs", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index 8fde7b3c1..6c368d793 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new StringFieldProperties()); - Assert.Equal("my-string", sut.Name); + Assert.Equal("myString", sut.Name); } [Fact] @@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(StringFieldProperties properties) { - return Fields.String(1, "my-string", Partitioning.Invariant, properties); + return Fields.String(1, "myString", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs index 1bbf310c1..9b4746f2e 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new TagsFieldProperties()); - Assert.Equal("my-tags", sut.Name); + Assert.Equal("myTags", sut.Name); } [Fact] @@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private static RootField Field(TagsFieldProperties properties) { - return Fields.Tags(1, "my-tags", Partitioning.Invariant, properties); + return Fields.Tags(1, "myTags", Partitioning.Invariant, properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs index 8bf7b9011..0aed91150 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new UIFieldProperties()); - Assert.Equal("my-ui", sut.Name); + Assert.Equal("myUI", sut.Name); } [Fact] @@ -67,13 +67,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var schema = new Schema("my-schema") - .AddUI(1, "my-ui1", Partitioning.Invariant) - .AddUI(2, "my-ui2", Partitioning.Invariant); + .AddUI(1, "myUI1", Partitioning.Invariant) + .AddUI(2, "myUI2", Partitioning.Invariant); var data = new ContentData() - .AddField("my-ui1", new ContentFieldData()) - .AddField("my-ui2", new ContentFieldData() + .AddField("myUI1", new ContentFieldData()) + .AddField("myUI2", new ContentFieldData() .AddInvariant(null)); var dataErrors = new List(); @@ -83,8 +83,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent dataErrors.Should().BeEquivalentTo( new[] { - new ValidationError("Value must not be defined.", "my-ui1"), - new ValidationError("Value must not be defined.", "my-ui2") + new ValidationError("Value must not be defined.", "myUI1"), + new ValidationError("Value must not be defined.", "myUI2") }); } @@ -93,16 +93,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var schema = new Schema("my-schema") - .AddArray(1, "my-array", Partitioning.Invariant, array => array - .AddUI(101, "my-ui")); + .AddArray(1, "myArray", Partitioning.Invariant, array => array + .AddUI(101, "myUI")); var data = new ContentData() - .AddField("my-array", new ContentFieldData() + .AddField("myArray", new ContentFieldData() .AddInvariant( JsonValue.Array( JsonValue.Object() - .Add("my-ui", null)))); + .Add("myUI", null)))); var dataErrors = new List(); @@ -111,13 +111,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent dataErrors.Should().BeEquivalentTo( new[] { - new ValidationError("Value must not be defined.", "my-array.iv[1].my-ui") + new ValidationError("Value must not be defined.", "myArray.iv[1].myUI") }); } private static NestedField Field(UIFieldProperties properties) { - return new NestedField(1, "my-ui", properties); + return new NestedField(1, "myUI", properties); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs new file mode 100644 index 000000000..fa394f734 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +{ + public class UniqueObjectValuesValidatorTests : IClassFixture + { + private readonly List errors = new List(); + + [Fact] + public async Task Should_not_add_errors_if_value_is_invalid() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + + await sut.ValidateAsync(1, errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_errors_if_value_is_null() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + + await sut.ValidateAsync(null, errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_objects_contain_not_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + + await sut.ValidateAsync(new[] + { + new JsonObject() + .Add("myString", "1"), + new JsonObject() + .Add("myString", "2") + }, + errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_objects_contain_unchecked_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + + await sut.ValidateAsync(new[] + { + new JsonObject() + .Add("other", "1"), + new JsonObject() + .Add("other", "1") + }, + errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_objects_contain_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString" }); + + await sut.ValidateAsync(new[] + { + new JsonObject() + .Add("myString", "1"), + new JsonObject() + .Add("myString", "1") + }, + errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain items with duplicate 'myString' fields." }); + } + + [Fact] + public async Task Should_add_errors_if_objects_contain_multiple_duplicates() + { + var sut = new UniqueObjectValuesValidator(new[] { "myString", "myNumber" }); + + await sut.ValidateAsync(new[] + { + new JsonObject() + .Add("myString", "1") + .Add("myNumber", 1), + new JsonObject() + .Add("myString", "1") + .Add("myNumber", 1), + }, + errors); + + errors.Should().BeEquivalentTo( + new[] + { + "Must not contain items with duplicate 'myString' fields.", + "Must not contain items with duplicate 'myNumber' fields." + }); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs index 75c858a6c..7b9993ccc 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs @@ -21,36 +21,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { private readonly List errors = new List(); - [Fact] - public async Task Should_add_error_if_other_content_with_string_value_found() - { - var filter = string.Empty; - - var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); - - await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv")); - - errors.Should().BeEquivalentTo( - new[] { "property.iv: Another content with the same value exists." }); - - Assert.Equal("Data.property.iv == 'hi'", filter); - } - - [Fact] - public async Task Should_add_error_if_other_content_with_double_value_found() - { - var filter = string.Empty; - - var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); - - await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv")); - - errors.Should().BeEquivalentTo( - new[] { "property.iv: Another content with the same value exists." }); - - Assert.Equal("Data.property.iv == 12.5", filter); - } - [Fact] public async Task Should_not_check_uniqueness_if_localized_string() { @@ -99,6 +69,36 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators Assert.Empty(errors); } + [Fact] + public async Task Should_add_error_if_other_content_with_string_value_found() + { + var filter = string.Empty; + + var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); + + await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv")); + + errors.Should().BeEquivalentTo( + new[] { "property.iv: Another content with the same value exists." }); + + Assert.Equal("Data.property.iv == 'hi'", filter); + } + + [Fact] + public async Task Should_add_error_if_other_content_with_double_value_found() + { + var filter = string.Empty; + + var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f)); + + await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv")); + + errors.Should().BeEquivalentTo( + new[] { "property.iv: Another content with the same value exists." }); + + Assert.Equal("Data.property.iv == 12.5", filter); + } + private static CheckUniqueness FoundDuplicates(DomainId id, Action? filter = null) { return filterNode => diff --git a/frontend/app/features/content/shared/forms/field-editor.component.ts b/frontend/app/features/content/shared/forms/field-editor.component.ts index 2ec79ae3e..8032afa64 100644 --- a/frontend/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/app/features/content/shared/forms/field-editor.component.ts @@ -7,7 +7,7 @@ import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { AbstractControl, FormControl } from '@angular/forms'; -import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types } from '@app/shared'; +import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types, value$ } from '@app/shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -68,7 +68,7 @@ export class FieldEditorComponent implements OnChanges { previousControl.form['_clearChangeFns'](); } - this.isEmpty = this.formModel.form.valueChanges.pipe(map(x => Types.isUndefined(x) || Types.isNull(x))); + this.isEmpty = value$(this.formModel.form).pipe(map(x => Types.isUndefined(x) || Types.isNull(x))); } } diff --git a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html index e9240bebf..693c79721 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html @@ -16,6 +16,7 @@ [languages]="(languagesState.isoLanguages | async)!" [field]="field" [fieldForm]="editForm.form" + [schema]="schema" [isEditable]="true" [isLocalizable]="isLocalizable" [settings]="settings"> @@ -55,7 +56,7 @@ -
+
+ +
+ + +
+ +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/components-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/components-validation.component.html index d215b76c6..3ea794849 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/components-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/components-validation.component.html @@ -26,5 +26,13 @@ + +
+ + +
+ +
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts index 55ced02f6..6942e1695 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts @@ -7,10 +7,10 @@ import { Component, Input } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { FieldDto, LanguageDto, NumberFieldPropertiesDto, RootFieldDto, Types } from '@app/shared'; +import { FieldDto, LanguageDto, NumberFieldPropertiesDto, RootFieldDto, SchemaDto, Types } from '@app/shared'; @Component({ - selector: 'sqx-number-validation[field][fieldForm][properties]', + selector: 'sqx-number-validation[field][fieldForm][properties][schema]', styleUrls: ['number-validation.component.scss'], templateUrl: 'number-validation.component.html', }) @@ -21,6 +21,9 @@ export class NumberValidationComponent { @Input() public field: FieldDto; + @Input() + public schema: SchemaDto; + @Input() public properties: NumberFieldPropertiesDto; @@ -31,6 +34,6 @@ export class NumberValidationComponent { public isLocalizable?: boolean | null; public get showUnique() { - return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable; + return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; } } diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts index eb34d2b80..1fd322b3a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts @@ -7,11 +7,11 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { AppSettingsDto, fadeAnimation, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, StringFieldPropertiesDto, STRING_CONTENT_TYPES, Types, value$ } from '@app/shared'; +import { AppSettingsDto, fadeAnimation, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, SchemaDto, StringFieldPropertiesDto, STRING_CONTENT_TYPES, Types, value$ } from '@app/shared'; import { Observable } from 'rxjs'; @Component({ - selector: 'sqx-string-validation[field][fieldForm][properties]', + selector: 'sqx-string-validation[field][fieldForm][properties][schema]', styleUrls: ['string-validation.component.scss'], templateUrl: 'string-validation.component.html', animations: [ @@ -27,6 +27,9 @@ export class StringValidationComponent extends ResourceOwner implements OnChange @Input() public field: FieldDto; + @Input() + public schema: SchemaDto; + @Input() public properties: StringFieldPropertiesDto; @@ -46,7 +49,7 @@ export class StringValidationComponent extends ResourceOwner implements OnChange public patternsModal = new ModalModel(); public get showUnique() { - return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable; + return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component'; } public ngOnChanges(changes: SimpleChanges) { diff --git a/frontend/app/features/schemas/pages/schema/schema-page.component.html b/frontend/app/features/schemas/pages/schema/schema-page.component.html index 17c1b5a6a..f4bca16a8 100644 --- a/frontend/app/features/schemas/pages/schema/schema-page.component.html +++ b/frontend/app/features/schemas/pages/schema/schema-page.component.html @@ -8,12 +8,12 @@ {{ 'schemas.tabFields' | sqxTranslate }} -
  • +
  • {{ 'schemas.tabUI' | sqxTranslate }}
  • -
  • +
  • {{ 'schemas.tabScripts' | sqxTranslate }} @@ -92,9 +92,17 @@
    - - - + + + + + + + +
    diff --git a/frontend/app/framework/angular/forms/validators.spec.ts b/frontend/app/framework/angular/forms/validators.spec.ts index 51da5a5d1..0dc9d7d4b 100644 --- a/frontend/app/framework/angular/forms/validators.spec.ts +++ b/frontend/app/framework/angular/forms/validators.spec.ts @@ -9,398 +9,458 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { DateTime } from '@app/framework/internal'; import { ValidatorsEx } from './validators'; -describe('ValidatorsEx.between', () => { - it('should return null validator if no min value or max value', () => { - const validator = ValidatorsEx.between(undefined, undefined); +describe('ValidatorsEx', () => { + describe('between', () => { + it('should return null validator if no min value or max value', () => { + const validator = ValidatorsEx.between(undefined, undefined); - expect(validator).toBe(Validators.nullValidator); - }); + expect(validator).toBe(Validators.nullValidator); + }); - it('should return null if value is equal to min and max', () => { - const input = new FormControl(3); + it('should return null if value is equal to min and max', () => { + const input = new FormControl(3); - const error = ValidatorsEx.between(3, 3)(input); + const error = ValidatorsEx.between(3, 3)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is valid', () => { - const input = new FormControl(4); + it('should return null if value is valid', () => { + const input = new FormControl(4); - const error = ValidatorsEx.between(1, 5)(input); + const error = ValidatorsEx.between(1, 5)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is null', () => { - const input = new FormControl(null); + it('should return null if value is null', () => { + const input = new FormControl(null); - const error = ValidatorsEx.between(1, 5)(input); + const error = ValidatorsEx.between(1, 5)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is undefined', () => { - const input = new FormControl(undefined); + it('should return null if value is undefined', () => { + const input = new FormControl(undefined); - const error = ValidatorsEx.between(1, 5)(input); + const error = ValidatorsEx.between(1, 5)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return error if less than min', () => { - const input = new FormControl(0); + it('should return error if less than min', () => { + const input = new FormControl(0); - const error = ValidatorsEx.between(1, undefined)(input); + const error = ValidatorsEx.between(1, undefined)(input); - expect(error.min).toBeDefined(); - }); + expect(error.min).toBeDefined(); + }); - it('should return error if greater than max', () => { - const input = new FormControl(6); + it('should return error if greater than max', () => { + const input = new FormControl(6); - const error = ValidatorsEx.between(undefined, 5)(input); + const error = ValidatorsEx.between(undefined, 5)(input); - expect(error.max).toBeDefined(); - }); + expect(error.max).toBeDefined(); + }); - it('should return error if not in range', () => { - const input = new FormControl(1); + it('should return error if not in range', () => { + const input = new FormControl(1); - const error = ValidatorsEx.between(2, 4)(input); + const error = ValidatorsEx.between(2, 4)(input); - expect(error.between).toBeDefined(); - }); + expect(error.between).toBeDefined(); + }); - it('should return error if not equal to min and max', () => { - const input = new FormControl(2); + it('should return error if not equal to min and max', () => { + const input = new FormControl(2); - const error = ValidatorsEx.between(3, 3)(input); + const error = ValidatorsEx.between(3, 3)(input); - expect(error.exactly).toBeDefined(); + expect(error.exactly).toBeDefined(); + }); }); -}); -describe('ValidatorsEx.betweenLength', () => { - it('should return null validator if no min value or max value', () => { - const validator = ValidatorsEx.betweenLength(undefined, undefined); + describe('betweenLength', () => { + it('should return null validator if no min value or max value', () => { + const validator = ValidatorsEx.betweenLength(undefined, undefined); - expect(validator).toBe(Validators.nullValidator); - }); + expect(validator).toBe(Validators.nullValidator); + }); - it('should return null if value is equal to min and max', () => { - const input = new FormControl('xxx'); + it('should return null if value is equal to min and max', () => { + const input = new FormControl('xxx'); - const error = ValidatorsEx.betweenLength(3, 3)(input); + const error = ValidatorsEx.betweenLength(3, 3)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is valid', () => { - const input = new FormControl('xxxx'); + it('should return null if value is valid', () => { + const input = new FormControl('xxxx'); - const error = ValidatorsEx.betweenLength(1, 5)(input); + const error = ValidatorsEx.betweenLength(1, 5)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is null', () => { - const input = new FormControl(null); + it('should return null if value is null', () => { + const input = new FormControl(null); - const error = ValidatorsEx.betweenLength(1, 5)(input); + const error = ValidatorsEx.betweenLength(1, 5)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is undefined', () => { - const input = new FormControl(undefined); + it('should return null if value is undefined', () => { + const input = new FormControl(undefined); - const error = ValidatorsEx.betweenLength(1, 5)(input); + const error = ValidatorsEx.betweenLength(1, 5)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return error if less than min', () => { - const input = new FormControl('x'); + it('should return error if less than min', () => { + const input = new FormControl('x'); - const error = ValidatorsEx.betweenLength(2, undefined)(input); + const error = ValidatorsEx.betweenLength(2, undefined)(input); - expect(error.minlength).toBeDefined(); - }); + expect(error.minlength).toBeDefined(); + }); - it('should return error if greater than max', () => { - const input = new FormControl('xxxxxx'); + it('should return error if greater than max', () => { + const input = new FormControl('xxxxxx'); - const error = ValidatorsEx.betweenLength(undefined, 5)(input); + const error = ValidatorsEx.betweenLength(undefined, 5)(input); - expect(error.maxlength).toBeDefined(); - }); + expect(error.maxlength).toBeDefined(); + }); - it('should return error if not in range', () => { - const input = new FormControl('x'); + it('should return error if not in range', () => { + const input = new FormControl('x'); - const error = ValidatorsEx.betweenLength(2, 4)(input); + const error = ValidatorsEx.betweenLength(2, 4)(input); - expect(error.betweenlength).toBeDefined(); - }); + expect(error.betweenlength).toBeDefined(); + }); - it('should return error if not equal to min and max', () => { - const input = new FormControl('xx'); + it('should return error if not equal to min and max', () => { + const input = new FormControl('xx'); - const error = ValidatorsEx.betweenLength(3, 3)(input); + const error = ValidatorsEx.betweenLength(3, 3)(input); - expect(error.exactlylength).toBeDefined(); + expect(error.exactlylength).toBeDefined(); + }); }); -}); -describe('ValidatorsEx.validDateTime', () => { - it('should return null validator if valid is not defined', () => { - const input = new FormControl(null); + describe('validDateTime', () => { + it('should return null validator if valid is not defined', () => { + const input = new FormControl(null); - const error = ValidatorsEx.validDateTime()(input); + const error = ValidatorsEx.validDateTime()(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if date time is valid', () => { - const input = new FormControl(DateTime.now().toISOString()); + it('should return null if date time is valid', () => { + const input = new FormControl(DateTime.now().toISOString()); - const error = ValidatorsEx.validDateTime()(input); + const error = ValidatorsEx.validDateTime()(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return error if value is invalid date', () => { - const input = new FormControl('invalid'); + it('should return error if value is invalid date', () => { + const input = new FormControl('invalid'); - const error = ValidatorsEx.validDateTime()(input); + const error = ValidatorsEx.validDateTime()(input); - expect(error.validdatetime).toBeDefined(); + expect(error.validdatetime).toBeDefined(); + }); }); -}); -describe('ValidatorsEx.validValues', () => { - it('should return null validator if values not defined', () => { - const validator = ValidatorsEx.validValues(null!); + describe('validValues', () => { + it('should return null validator if values not defined', () => { + const validator = ValidatorsEx.validValues(null!); - expect(validator).toBe(Validators.nullValidator); - }); + expect(validator).toBe(Validators.nullValidator); + }); - it('should return null if value is in allowed values', () => { - const input = new FormControl(10); + it('should return null if value is in allowed values', () => { + const input = new FormControl(10); - const error = ValidatorsEx.validValues([10, 20, 30])(input); + const error = ValidatorsEx.validValues([10, 20, 30])(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return error if value is not in allowed values', () => { - const input = new FormControl(50); + it('should return error if value is not in allowed values', () => { + const input = new FormControl(50); - const error = ValidatorsEx.validValues([10, 20, 30])(input); + const error = ValidatorsEx.validValues([10, 20, 30])(input); - expect(error.validvalues).toBeDefined(); + expect(error.validvalues).toBeDefined(); + }); }); -}); -describe('ValidatorsEx.validArrayValues', () => { - it('should return null validator if values not defined', () => { - const validator = ValidatorsEx.validArrayValues(null!); + describe('validArrayValues', () => { + it('should return null validator if values not defined', () => { + const validator = ValidatorsEx.validArrayValues(null!); - expect(validator).toBe(Validators.nullValidator); - }); + expect(validator).toBe(Validators.nullValidator); + }); - it('should return null if value is in allowed values', () => { - const input = new FormControl([10, 20]); + it('should return null if value is in allowed values', () => { + const input = new FormControl([10, 20]); - const error = ValidatorsEx.validArrayValues([10, 20, 30])(input); + const error = ValidatorsEx.validArrayValues([10, 20, 30])(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return error if value is not in allowed values', () => { - const input = new FormControl([50]); + it('should return error if value is not in allowed values', () => { + const input = new FormControl([50]); - const error = ValidatorsEx.validArrayValues([10, 20, 30])(input); + const error = ValidatorsEx.validArrayValues([10, 20, 30])(input); - expect(error.validarrayvalues).toBeDefined(); + expect(error.validarrayvalues).toBeDefined(); + }); }); -}); -describe('ValidatorsEx.match', () => { - it('should revalidate if other control changes', () => { - const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + describe('match', () => { + it('should revalidate if other control changes', () => { + const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + + const form = new FormGroup({ + password: new FormControl('1'), + passwordConfirm: new FormControl('2', validator), + }); - const form = new FormGroup({ - password: new FormControl('1'), - passwordConfirm: new FormControl('2', validator), + form.controls['passwordConfirm'].setValue('1'); + + expect(form.valid).toBeTruthy(); + + form.controls['password'].setValue('2'); + + expect(form.controls['password'].valid).toBeTruthy(); + expect(form.controls['passwordConfirm'].valid).toBeFalsy(); }); - form.controls['passwordConfirm'].setValue('1'); + it('should return error if not the same value', () => { + const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); - expect(form.valid).toBeTruthy(); + const form = new FormGroup({ + password: new FormControl('1'), + passwordConfirm: new FormControl('2', validator), + }); - form.controls['password'].setValue('2'); + expect(validator(form.controls['passwordConfirm'])).toEqual({ match: { message: 'Passwords are not the same.' } }); + }); - expect(form.controls['password'].valid).toBeTruthy(); - expect(form.controls['passwordConfirm'].valid).toBeFalsy(); - }); + it('should return empty object if values are the same', () => { + const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); - it('should return error if not the same value', () => { - const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + const form = new FormGroup({ + password: new FormControl('1'), + passwordConfirm: new FormControl('1', validator), + }); - const form = new FormGroup({ - password: new FormControl('1'), - passwordConfirm: new FormControl('2', validator), + expect(validator(form.controls['passwordConfirm'])).toBeNull(); }); - expect(validator(form.controls['passwordConfirm'])).toEqual({ match: { message: 'Passwords are not the same.' } }); - }); + it('should throw error if other object is not found', () => { + const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); - it('should return empty object if values are the same', () => { - const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + const form = new FormGroup({ + passwordConfirm: new FormControl('2', validator), + }); - const form = new FormGroup({ - password: new FormControl('1'), - passwordConfirm: new FormControl('1', validator), + expect(() => validator(form.controls['passwordConfirm'])).toThrow(); }); - expect(validator(form.controls['passwordConfirm'])).toBeNull(); + it('should return empty object if control has no parent', () => { + const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + + const control = new FormControl('2', validator); + + expect(validator(control)).toBeNull(); + }); }); - it('should throw error if other object is not found', () => { - const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + describe('pattern', () => { + it('should return null validator if pattern not defined', () => { + const validator = ValidatorsEx.pattern(undefined!, undefined); - const form = new FormGroup({ - passwordConfirm: new FormControl('2', validator), + expect(validator).toBe(Validators.nullValidator); }); - expect(() => validator(form.controls['passwordConfirm'])).toThrow(); - }); + it('should return null if value is valid pattern', () => { + const input = new FormControl('1234'); - it('should return empty object if control has no parent', () => { - const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); - const control = new FormControl('2', validator); + expect(error).toBeNull(); + }); - expect(validator(control)).toBeNull(); - }); -}); + it('should return null if value is null string', () => { + const input = new FormControl(null); -describe('ValidatorsEx.pattern', () => { - it('should return null validator if pattern not defined', () => { - const validator = ValidatorsEx.pattern(undefined!, undefined); + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); - expect(validator).toBe(Validators.nullValidator); - }); + expect(error).toBeNull(); + }); - it('should return null if value is valid pattern', () => { - const input = new FormControl('1234'); + it('should return null if value is empty string', () => { + const input = new FormControl(''); - const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is null string', () => { - const input = new FormControl(null); + it('should return error with message if value does not match pattern string', () => { + const input = new FormControl('abc'); - const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + const error = ValidatorsEx.pattern('[0-9]{1,4}', 'My-Message')(input); + const expected: any = { + patternmessage: { + requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', message: 'My-Message', + }, + }; - expect(error).toBeNull(); - }); + expect(error).toEqual(expected); + }); - it('should return null if value is empty string', () => { - const input = new FormControl(''); + it('should return error with message if value does not match pattern', () => { + const input = new FormControl('abc'); - const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/, 'My-Message')(input); + const expected: any = { + patternmessage: { + requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', message: 'My-Message', + }, + }; - expect(error).toBeNull(); - }); + expect(error).toEqual(expected); + }); - it('should return error with message if value does not match pattern string', () => { - const input = new FormControl('abc'); + it('should return error without message if value does not match pattern string', () => { + const input = new FormControl('abc'); - const error = ValidatorsEx.pattern('[0-9]{1,4}', 'My-Message')(input); - const expected: any = { - patternmessage: { - requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', message: 'My-Message', - }, - }; + const error = ValidatorsEx.pattern('[0-9]{1,4}')(input); + const expected: any = { + pattern: { + requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', + }, + }; - expect(error).toEqual(expected); - }); + expect(error).toEqual(expected); + }); - it('should return error with message if value does not match pattern', () => { - const input = new FormControl('abc'); + it('should return error without message if value does not match pattern', () => { + const input = new FormControl('abc'); - const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/, 'My-Message')(input); - const expected: any = { - patternmessage: { - requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', message: 'My-Message', - }, - }; + const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); + const expected: any = { + pattern: { + requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', + }, + }; - expect(error).toEqual(expected); + expect(error).toEqual(expected); + }); }); - it('should return error without message if value does not match pattern string', () => { - const input = new FormControl('abc'); + describe('uniqueStrings', () => { + it('should return null if value is null', () => { + const input = new FormControl(null); - const error = ValidatorsEx.pattern('[0-9]{1,4}')(input); - const expected: any = { - pattern: { - requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', - }, - }; + const error = ValidatorsEx.uniqueStrings()(input); - expect(error).toEqual(expected); - }); + expect(error).toBeNull(); + }); - it('should return error without message if value does not match pattern', () => { - const input = new FormControl('abc'); + it('should return null if value is not a string array', () => { + const input = new FormControl([1, 2, 3]); - const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input); - const expected: any = { - pattern: { - requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', - }, - }; + const error = ValidatorsEx.uniqueStrings()(input); - expect(error).toEqual(expected); - }); -}); + expect(error).toBeNull(); + }); -describe('ValidatorsEx.uniqueStrings', () => { - it('should return null if value is null', () => { - const input = new FormControl(null); + it('should return null if values are unique', () => { + const input = new FormControl(['1', '2', '3']); - const error = ValidatorsEx.uniqueStrings()(input); + const error = ValidatorsEx.uniqueStrings()(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); - it('should return null if value is not a string array', () => { - const input = new FormControl([1, 2, 3]); + it('should return error if values are not unique', () => { + const input = new FormControl(['1', '2', '2', '3']); - const error = ValidatorsEx.uniqueStrings()(input); + const error = ValidatorsEx.uniqueStrings()(input); - expect(error).toBeNull(); + expect(error).toEqual({ uniquestrings: false }); + }); }); - it('should return null if values are unique', () => { - const input = new FormControl(['1', '2', '3']); + describe('uniqueObjectValues', () => { + it('should return null if value is null', () => { + const input = new FormControl(null); - const error = ValidatorsEx.uniqueStrings()(input); + const error = ValidatorsEx.uniqueObjectValues(['myString'])(input); - expect(error).toBeNull(); - }); + expect(error).toBeNull(); + }); + + it('should return null if value is not an object array', () => { + const input = new FormControl([1, 2, 3]); + + const error = ValidatorsEx.uniqueObjectValues(['myString'])(input); + + expect(error).toBeNull(); + }); + + it('should return null if values array has one item', () => { + const input = new FormControl([{}]); + + const error = ValidatorsEx.uniqueObjectValues(['myString'])(input); + + expect(error).toBeNull(); + }); - it('should return error if values are not unique', () => { - const input = new FormControl(['1', '2', '2', '3']); + it('should return null if values array has no duplicate', () => { + const input = new FormControl([{ myString: '1' }, { myString: '2' }]); - const error = ValidatorsEx.uniqueStrings()(input); + const error = ValidatorsEx.uniqueObjectValues(['myString'])(input); - expect(error).toEqual({ uniquestrings: false }); + expect(error).toBeNull(); + }); + + it('should return null if values array has unchecked duplicate', () => { + const input = new FormControl([{ other: '1' }, { other: '1' }]); + + const error = ValidatorsEx.uniqueObjectValues(['myString'])(input); + + expect(error).toBeNull(); + }); + + it('should return error if values array has duplicate', () => { + const input = new FormControl([{ myString: '1' }, { myString: '1' }]); + + const error = ValidatorsEx.uniqueObjectValues(['myString'])(input); + + expect(error).toEqual({ uniqueobjectvalues: { fields: 'myString' } }); + }); + + it('should return error if values array has multiple duplicates', () => { + const input = new FormControl([{ myString: '1', myNumber: 2 }, { myString: '1', myNumber: 2 }]); + + const error = ValidatorsEx.uniqueObjectValues(['myString', 'myNumber'])(input); + + expect(error).toEqual({ uniqueobjectvalues: { fields: 'myString, myNumber' } }); + }); }); }); diff --git a/frontend/app/framework/angular/forms/validators.ts b/frontend/app/framework/angular/forms/validators.ts index fa6154564..0f25b1a5f 100644 --- a/frontend/app/framework/angular/forms/validators.ts +++ b/frontend/app/framework/angular/forms/validators.ts @@ -9,7 +9,7 @@ import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'; import { DateTime, Types } from '@app/framework/internal'; function isEmptyInputValue(value: any): boolean { - return value == null || value.length === 0; + return value == null || value === undefined || value.length === 0; } export module ValidatorsEx { @@ -191,4 +191,43 @@ export module ValidatorsEx { return null; }; } + + export function uniqueObjectValues(fields: ReadonlyArray): ValidatorFn { + return (control: AbstractControl) => { + if (isEmptyInputValue(control.value) || !Types.isArrayOfObject(control.value)) { + return null; + } + + const items: any[] = control.value; + + const duplicateKeys: object = {}; + + for (const field of fields) { + const values: any[] = []; + + for (const item of items) { + if (item.hasOwnProperty(field)) { + const fieldValue = item[field]; + + for (const other of values) { + if (Types.equals(other, fieldValue)) { + duplicateKeys[field] = true; + break; + } + } + + values.push(fieldValue); + } + } + } + + const keys = Object.keys(duplicateKeys); + + if (keys.length > 0) { + return { uniqueobjectvalues: { fields: keys.join(', ') } }; + } + + return null; + }; + } } diff --git a/frontend/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts index e5509458b..31169e824 100644 --- a/frontend/app/shared/services/schemas.types.ts +++ b/frontend/app/shared/services/schemas.types.ts @@ -181,6 +181,7 @@ export class ArrayFieldPropertiesDto extends FieldPropertiesDto { public readonly maxItems?: number; public readonly minItems?: number; + public readonly uniqueFields?: ReadonlyArray; public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitArray(this); @@ -270,6 +271,7 @@ export class ComponentsFieldPropertiesDto extends FieldPropertiesDto { public readonly schemaIds?: ReadonlyArray; public readonly maxItems?: number; public readonly minItems?: number; + public readonly uniqueFields?: ReadonlyArray; public get isComplexUI() { return true; diff --git a/frontend/app/shared/state/contents.forms.visitors.ts b/frontend/app/shared/state/contents.forms.visitors.ts index 025d6271d..8560d5c03 100644 --- a/frontend/app/shared/state/contents.forms.visitors.ts +++ b/frontend/app/shared/state/contents.forms.visitors.ts @@ -246,6 +246,10 @@ export class FieldsValidators implements FieldPropertiesVisitor 0) { + validators.push(ValidatorsEx.uniqueObjectValues(properties.uniqueFields)); + } + return validators; } @@ -274,6 +278,10 @@ export class FieldsValidators implements FieldPropertiesVisitor 0) { + validators.push(ValidatorsEx.uniqueObjectValues(properties.uniqueFields)); + } + return validators; } diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts index 724fe4430..9cc5ee09b 100644 --- a/frontend/app/shared/state/schemas.forms.ts +++ b/frontend/app/shared/state/schemas.forms.ts @@ -256,6 +256,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor { public visitArray() { this.config['maxItems'] = undefined; this.config['minItems'] = undefined; + this.config['uniqueFields'] = undefined; } public visitAssets() { @@ -294,6 +295,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor { this.config['schemaIds'] = undefined; this.config['maxItems'] = undefined; this.config['minItems'] = undefined; + this.config['uniqueFields'] = undefined; } public visitDateTime() {