From 7adcb6f7ca320d77d69f51d0bdfa21c6b6ffa020 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 3 Nov 2020 18:49:09 +0100 Subject: [PATCH] New validation logic for references and assets. --- backend/i18n/frontend_en.json | 1 + backend/i18n/frontend_it.json | 1 + backend/i18n/frontend_nl.json | 1 + backend/i18n/source/backend_en.json | 4 +- backend/i18n/source/backend_it.json | 4 +- backend/i18n/source/backend_nl.json | 4 +- backend/i18n/source/frontend_en.json | 1 + .../Schemas/ReferencesFieldProperties.cs | 2 + .../DefaultFieldValueValidatorsFactory.cs | 28 +-- .../Validators/AssetsValidator.cs | 156 ++++++++++------ .../Validators/CollectionValidator.cs | 53 +++++- .../Validators/CollectionValidatorBase.cs | 86 --------- .../Validators/ReferencesValidator.cs | 70 ++++++- .../Validators/UniqueValidator.cs | 3 +- .../Contents/Fields.cs | 8 + .../Contents/MongoContentCollectionAll.cs | 5 +- .../MongoContentCollectionPublished.cs | 3 +- .../Contents/MongoContentRepository.cs | 5 +- .../Contents/Operations/QueryIdsAsync.cs | 28 +-- .../Repositories/IContentRepository.cs | 5 +- .../Validation/DependencyValidatorsFactory.cs | 18 +- .../MongoDb/MongoExtensions.cs | 8 + backend/src/Squidex.Shared/Texts.it.resx | 12 +- backend/src/Squidex.Shared/Texts.nl.resx | 12 +- backend/src/Squidex.Shared/Texts.resx | 12 +- .../Fields/ReferencesFieldPropertiesDto.cs | 5 + .../ValidateContent/AssetsFieldTests.cs | 88 +-------- .../ValidateContent/ReferencesFieldTests.cs | 99 +--------- .../ValidationTestExtensions.cs | 20 +- .../Validators/AssetsValidatorTests.cs | 76 +++++++- .../Validators/ReferencesValidatorTests.cs | 175 ++++++++++++++++-- .../Validators/UniqueValidatorTests.cs | 3 +- .../list/content-list-cell.directive.ts | 2 +- .../content-selector-item.component.html | 6 +- .../content-selector.component.html | 7 +- .../references/reference-item.component.html | 4 + .../references-validation.component.html | 14 +- .../types/references-validation.component.ts | 3 + frontend/app/shared/services/schemas.types.ts | 1 + 39 files changed, 598 insertions(+), 435 deletions(-) delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidatorBase.cs diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 92356969a..c162c1dda 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -731,6 +731,7 @@ "schemas.fieldTypes.references.countMax": "Max Items", "schemas.fieldTypes.references.countMin": "Min Items", "schemas.fieldTypes.references.description": "Links to other content items.", + "schemas.fieldTypes.references.mustBePublished": "References must be published", "schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.", "schemas.fieldTypes.string.characters": "Characters", "schemas.fieldTypes.string.charactersMax": "Max Characters", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 0ff1310d0..fffe715cd 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -731,6 +731,7 @@ "schemas.fieldTypes.references.countMax": "Numero Max Elementi", "schemas.fieldTypes.references.countMin": "Numero Min Elementi", "schemas.fieldTypes.references.description": "Link ad altri elementi del contenuto.", + "schemas.fieldTypes.references.mustBePublished": "References must be published", "schemas.fieldTypes.references.resolveHint": "Mostra il nome dell'elemento collegato (reference) nella lista dei contenuti quando il numero massimo di elementi è impostato a 1.", "schemas.fieldTypes.string.characters": "Caratteri", "schemas.fieldTypes.string.charactersMax": "Max numero di Caratteri", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index e0bef07f8..7218dc962 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -731,6 +731,7 @@ "schemas.fieldTypes.references.countMax": "Max. aantal items", "schemas.fieldTypes.references.countMin": "Min. items", "schemas.fieldTypes.references.description": "Links naar andere inhoudsitems.", + "schemas.fieldTypes.references.mustBePublished": "References must be published", "schemas.fieldTypes.references.resolveHint": "Toon de naam van het item waarnaar wordt verwezen in de inhoudslijst wanneer MaxItems is ingesteld op 1.", "schemas.fieldTypes.string.characters": "Karakters", "schemas.fieldTypes.string.charactersMax": "Max. karakters", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 96c6a62a4..2b43774e0 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -106,8 +106,6 @@ "common.properties": "Properties", "common.property": "Property", "common.readonlyMode": "Application is in readonly mode at the moment.", - "common.referenceNotFound": "Contains invalid reference '{id}'.", - "common.referenceToInvalidSchema": "Contains reference '{id}' to invalid schema.", "common.remove": "Remove", "common.resultTooLarge": "Result set is too large to be retrieved. Use $take parameter to reduce the number of items.", "common.role": "Role", @@ -172,6 +170,8 @@ "contents.validation.normalCharactersBetween": "Must have between {min} and {max} text character(s).", "contents.validation.notAllowed": "Not an allowed value.", "contents.validation.pattern": "Must follow the pattern.", + "contents.validation.referenceNotFound": "Reference '{id}' not found.", + "contents.validation.referenceToInvalidSchema": "Reference '{id}' has invalid schema.", "contents.validation.regexTooSlow": "Regex is too slow.", "contents.validation.required": "Field is required.", "contents.validation.unique": "Another content with the same value exists.", diff --git a/backend/i18n/source/backend_it.json b/backend/i18n/source/backend_it.json index 6e376973d..6b14e7944 100644 --- a/backend/i18n/source/backend_it.json +++ b/backend/i18n/source/backend_it.json @@ -105,8 +105,6 @@ "common.properties": "Proprietà", "common.property": "Proprietà", "common.readonlyMode": "Al momento l'applicazione è in sola lettura.", - "common.referenceNotFound": "Contiene un collegamento '{id}' non valido.", - "common.referenceToInvalidSchema": "Contiene dei collegamenti '{id}' ad uno schema errato.", "common.remove": "Rimuovi", "common.resultTooLarge": "Il numero di risultati è troppo grande per essere recuperato. Utilizza il parametro $take per ridurre il numero di elementi.", "common.role": "Ruolo", @@ -169,6 +167,8 @@ "contents.validation.normalCharactersBetween": "Deve essere un testo tra {min} e {max} carattere(i).", "contents.validation.notAllowed": "Non è un valore consentito.", "contents.validation.pattern": "Deve seguire il pattern.", + "contents.validation.referenceNotFound": "Contiene un collegamento '{id}' non valido.", + "contents.validation.referenceToInvalidSchema": "Contiene dei collegamenti '{id}' ad uno schema errato.", "contents.validation.regexTooSlow": "La Regex è troppo lenta.", "contents.validation.required": "Il campo è obbligatorio.", "contents.validation.unique": "Esiste un altro contenuto con lo stesso valore.", diff --git a/backend/i18n/source/backend_nl.json b/backend/i18n/source/backend_nl.json index fcb5095df..09103ae63 100644 --- a/backend/i18n/source/backend_nl.json +++ b/backend/i18n/source/backend_nl.json @@ -106,8 +106,6 @@ "common.properties": "Eigenschappen", "common.property": "Eigenschap", "common.readonlyMode": "Applicatie is momenteel in de alleen-lezen modus.", - "common.referenceNotFound": "Bevat ongeldige referentie '{id}'.", - "common.referenceToInvalidSchema": "Bevat verwijzing '{id}' naar ongeldig schema.", "common.remove": "Verwijderen", "common.resultTooLarge": "Resultaatset is te groot om opgehaald te worden. Gebruik $ take parameter om het aantal items te verminderen.", "common.role": "Rol", @@ -172,6 +170,8 @@ "contents.validation.normalCharactersBetween": "Moet tussen {min} en {max} tekstteken (s) bevatten.", "contents.validation.notAllowed": "Geen toegestane waarde.", "contents.validation.pattern": "Moet het patroon volgen.", + "contents.validation.referenceNotFound": "Bevat ongeldige referentie '{id}'.", + "contents.validation.referenceToInvalidSchema": "Bevat verwijzing '{id}' naar ongeldig schema.", "contents.validation.regexTooSlow": "Regex is te traag.", "contents.validation.required": "Veld is verplicht.", "contents.validation.unique": "Er bestaat een andere inhoud met dezelfde waarde.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 92356969a..c162c1dda 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -731,6 +731,7 @@ "schemas.fieldTypes.references.countMax": "Max Items", "schemas.fieldTypes.references.countMin": "Min Items", "schemas.fieldTypes.references.description": "Links to other content items.", + "schemas.fieldTypes.references.mustBePublished": "References must be published", "schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.", "schemas.fieldTypes.string.characters": "Characters", "schemas.fieldTypes.string.charactersMax": "Max Characters", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index e28d89a13..7f36badf0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public bool AllowDuplicates { get; set; } + public bool MustBePublished { get; set; } + public string[]? DefaultValue { get; set; } public ReferencesFieldEditor Editor { get; set; } 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 b019fcfb8..e261e1945 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -59,19 +59,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - var properties = field.Properties; - - var isRequired = IsRequired(properties); - - if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) - { - yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } - - if (!properties.AllowDuplicates) - { - yield return new UniqueValuesValidator(); - } + yield break; } public IEnumerable Visit(IField field) @@ -151,19 +139,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - var properties = field.Properties; - - var isRequired = IsRequired(properties); - - if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) - { - yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); - } - - if (!properties.AllowDuplicates) - { - yield return new UniqueValuesValidator(); - } + yield break; } public IEnumerable Visit(IField field) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index 8a63b958f..9e99cf2b5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Assets; @@ -21,20 +22,34 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public sealed class AssetsValidator : IValidator { private readonly AssetsFieldProperties properties; + private readonly CollectionValidator? collectionValidator; + private readonly UniqueValuesValidator? uniqueValidator; private readonly CheckAssets checkAssets; - public AssetsValidator(AssetsFieldProperties properties, CheckAssets checkAssets) + public AssetsValidator(bool isRequired, AssetsFieldProperties properties, CheckAssets checkAssets) { Guard.NotNull(properties, nameof(properties)); Guard.NotNull(checkAssets, nameof(checkAssets)); this.properties = properties; + if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); + } + + if (!properties.AllowDuplicates) + { + uniqueValidator = new UniqueValuesValidator(); + } + this.checkAssets = checkAssets; } public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { + var foundIds = new List(); + if (value is ICollection assetIds && assetIds.Count > 0) { var assets = await checkAssets(assetIds); @@ -50,80 +65,111 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (asset == null) { - addError(path, T.Get("contents.validation.assetNotFound", new { id = assetId })); + if (context.Action == ValidationAction.Upsert) + { + addError(path, T.Get("contents.validation.assetNotFound", new { id = assetId })); + } + continue; } - if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) - { - var min = properties.MinSize.Value.ToReadableSize(); + foundIds.Add(asset.AssetId); - addError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); - } + ValidateCommon(asset, path, addError); - if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) + if (asset.Type != AssetType.Image) { - var max = properties.MaxSize.Value.ToReadableSize(); - - addError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); + ValidateNonImage(path, addError); } - - if (properties.AllowedExtensions != null && - properties.AllowedExtensions.Count > 0 && - !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) + else { - addError(path, T.Get("contents.validation.extension")); + ValidateImage(asset, path, addError); } + } + } - if (asset.Type != AssetType.Image) - { - if (properties.MustBeImage) - { - addError(path, T.Get("contents.validation.image")); - } + if (collectionValidator != null) + { + await collectionValidator.ValidateAsync(foundIds, context, addError); + } - continue; - } + if (uniqueValidator != null) + { + await uniqueValidator.ValidateAsync(foundIds, context, addError); + } + } - var pixelWidth = asset.Metadata.GetPixelWidth(); - var pixelHeight = asset.Metadata.GetPixelHeight(); + private void ValidateCommon(IAssetInfo asset, ImmutableQueue path, AddError addError) + { + if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) + { + var min = properties.MinSize.Value.ToReadableSize(); - if (pixelWidth.HasValue && pixelHeight.HasValue) - { - var w = pixelWidth.Value; - var h = pixelHeight.Value; + addError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); + } - var actualRatio = (double)w / h; + if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) + { + var max = properties.MaxSize.Value.ToReadableSize(); - if (properties.MinWidth.HasValue && w < properties.MinWidth) - { - addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); - } + addError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); + } - if (properties.MaxWidth.HasValue && w > properties.MaxWidth) - { - addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); - } + if (properties.AllowedExtensions != null && + properties.AllowedExtensions.Count > 0 && + !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) + { + addError(path, T.Get("contents.validation.extension")); + } + } - if (properties.MinHeight.HasValue && h < properties.MinHeight) - { - addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); - } + private void ValidateNonImage(ImmutableQueue path, AddError addError) + { + if (properties.MustBeImage) + { + addError(path, T.Get("contents.validation.image")); + } + } - if (properties.MaxHeight.HasValue && h > properties.MaxHeight) - { - addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); - } + private void ValidateImage(IAssetInfo asset, ImmutableQueue path, AddError addError) + { + var pixelWidth = asset.Metadata.GetPixelWidth(); + var pixelHeight = asset.Metadata.GetPixelHeight(); - if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) - { - var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; + if (pixelWidth.HasValue && pixelHeight.HasValue) + { + var w = pixelWidth.Value; + var h = pixelHeight.Value; - if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) - { - addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); - } - } + var actualRatio = (double)w / h; + + if (properties.MinWidth.HasValue && w < properties.MinWidth) + { + addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); + } + + if (properties.MaxWidth.HasValue && w > properties.MaxWidth) + { + addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); + } + + if (properties.MinHeight.HasValue && h < properties.MinHeight) + { + addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); + } + + if (properties.MaxHeight.HasValue && h > properties.MaxHeight) + { + addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); + } + + if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue) + { + var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; + + if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) + { + addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs index 02f32a8e6..94979e7a3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -5,21 +5,66 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections; using System.Threading.Tasks; +using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { - public sealed class CollectionValidator : CollectionValidatorBase, IValidator + public sealed class CollectionValidator : IValidator { + private readonly bool isRequired; + private readonly int? minItems; + private readonly int? maxItems; + public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) - : base(isRequired, minItems, maxItems) { + if (minItems.HasValue && maxItems.HasValue && minItems > maxItems) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); + } + + this.isRequired = isRequired; + this.minItems = minItems; + this.maxItems = maxItems; } public Task ValidateAsync(object? value, ValidationContext context, AddError addError) { - ValidateRequired(value, context, addError); - ValidateSize(value, context, addError); + if (!(value is ICollection items) || items.Count == 0) + { + if (isRequired && !context.IsOptional) + { + addError(context.Path, T.Get("contents.validation.required")); + } + + return Task.CompletedTask; + } + + if (minItems.HasValue && maxItems.HasValue) + { + if (minItems == maxItems && minItems != items.Count) + { + addError(context.Path, T.Get("contents.validation.itemCount", new { count = minItems })); + } + else if (items.Count < minItems || items.Count > maxItems) + { + addError(context.Path, T.Get("contents.validation.itemCountBetween", new { min = minItems, max = maxItems })); + } + } + else + { + if (minItems.HasValue && items.Count < minItems) + { + addError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); + } + + if (maxItems.HasValue && items.Count > maxItems) + { + addError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); + } + } return Task.CompletedTask; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidatorBase.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidatorBase.cs deleted file mode 100644 index c2d531fd8..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidatorBase.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections; -using Squidex.Infrastructure.Translations; - -namespace Squidex.Domain.Apps.Core.ValidateContent.Validators -{ - public abstract class CollectionValidatorBase - { - private readonly bool isRequired; - private readonly int? minItems; - private readonly int? maxItems; - - protected CollectionValidatorBase(bool isRequired, int? minItems = null, int? maxItems = null) - { - if (minItems.HasValue && maxItems.HasValue && minItems > maxItems) - { - throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); - } - - this.isRequired = isRequired; - this.minItems = minItems; - this.maxItems = maxItems; - } - - protected void ValidateRequired(object? value, ValidationContext context, AddError addError) - { - var size = 0; - - if (value is ICollection items) - { - size = items.Count; - } - - if (size == 0 && isRequired && !context.IsOptional) - { - addError(context.Path, T.Get("contents.validation.required")); - } - } - - protected void ValidateSize(object? value, ValidationContext context, AddError addError) - { - var size = 0; - - if (value is ICollection items) - { - size = items.Count; - } - - if (size == 0) - { - return; - } - - if (minItems.HasValue && maxItems.HasValue) - { - if (minItems == maxItems && minItems != size) - { - addError(context.Path, T.Get("contents.validation.itemCount", new { count = minItems })); - } - else if (size < minItems || size > maxItems) - { - addError(context.Path, T.Get("contents.validation.itemCountBetween", new { min = minItems, max = maxItems })); - } - } - else - { - if (minItems.HasValue && size < minItems) - { - addError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); - } - - if (maxItems.HasValue && size > maxItems) - { - addError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); - } - } - } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index dcb3d73a3..226bfbedf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -8,47 +8,99 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { - public delegate Task> CheckContentsByIds(HashSet ids); + public delegate Task> CheckContentsByIds(HashSet ids); public sealed class ReferencesValidator : IValidator { - private readonly IEnumerable? schemaIds; + private readonly ReferencesFieldProperties properties; + private readonly CollectionValidator? collectionValidator; + private readonly UniqueValuesValidator? uniqueValidator; private readonly CheckContentsByIds checkReferences; - public ReferencesValidator(IEnumerable? schemaIds, CheckContentsByIds checkReferences) + public ReferencesValidator(bool isRequired, ReferencesFieldProperties properties, CheckContentsByIds checkReferences) { + Guard.NotNull(properties, nameof(properties)); Guard.NotNull(checkReferences, nameof(checkReferences)); - this.schemaIds = schemaIds; + this.properties = properties; + + if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + collectionValidator = new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); + } + + if (!properties.AllowDuplicates) + { + uniqueValidator = new UniqueValuesValidator(); + } this.checkReferences = checkReferences; } public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { + var foundIds = new List(); + if (value is ICollection contentIds && contentIds.Count > 0) { - var foundIds = await checkReferences(contentIds.ToHashSet()); + var references = await checkReferences(contentIds.ToHashSet()); + var index = 0; foreach (var id in contentIds) { - var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id); + index++; + + var path = context.Path.Enqueue($"[{index}]"); + + var (schemaId, _, status) = references.FirstOrDefault(x => x.Id == id); if (schemaId == DomainId.Empty) { - addError(context.Path, T.Get("common.referenceNotFound", new { id })); + if (context.Action == ValidationAction.Upsert) + { + addError(path, T.Get("contents.validation.referenceNotFound", new { id })); + } + + continue; } - else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId)) + + var isValid = true; + + if (properties.SchemaIds?.Any() == true && !properties.SchemaIds.Contains(schemaId)) { - addError(context.Path, T.Get("common.referenceToInvalidSchema", new { id })); + if (context.Action == ValidationAction.Upsert) + { + addError(path, T.Get("contents.validation.referenceToInvalidSchema", new { id })); + } + + isValid = false; + } + + isValid &= (!properties.MustBePublished || status == Status.Published); + + if (isValid) + { + foundIds.Add(id); } } } + + if (collectionValidator != null) + { + await collectionValidator.ValidateAsync(foundIds, context, addError); + } + + if (uniqueValidator != null) + { + await uniqueValidator.ValidateAsync(foundIds, context, addError); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index 1540409de..d60f222dc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -8,13 +8,14 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { - public delegate Task> CheckUniqueness(FilterNode filter); + public delegate Task> CheckUniqueness(FilterNode filter); public sealed class UniqueValidator : IValidator { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs index 7e7e988c8..4bae382b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs @@ -14,11 +14,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { private static readonly Lazy IdField = new Lazy(GetIdField); private static readonly Lazy SchemaIdField = new Lazy(GetSchemaIdField); + private static readonly Lazy StatusField = new Lazy(GetStatusField); public static string Id => IdField.Value; public static string SchemaId => SchemaIdField.Value; + public static string Status => StatusField.Value; + private static string GetIdField() { return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Id)).ElementName; @@ -28,5 +31,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.IndexedSchemaId)).ElementName; } + + private static string GetStatusField() + { + return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Status)).ElementName; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs index cd817bf01..fb5354c96 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using NodaTime; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Text; @@ -112,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task> QueryIdsAsync(DomainId appId, HashSet ids) + public async Task> QueryIdsAsync(DomainId appId, HashSet ids) { using (Profiler.TraceMethod()) { @@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) + public async Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) { using (Profiler.TraceMethod()) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs index b8bcfa644..bad197b2a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Text; @@ -105,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task> QueryIdsAsync(DomainId appId, HashSet ids) + public async Task> QueryIdsAsync(DomainId appId, HashSet ids) { using (Profiler.TraceMethod()) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 1f0a2fce3..3faf06966 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using NodaTime; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Repositories; @@ -102,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope) + public Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope) { if (scope == SearchScope.All) { @@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return collectionAll.QueryScheduledWithoutDataAsync(now, callback); } - public Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) + public Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) { return collectionAll.QueryIdsAsync(appId, schemaId, filterNode); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs index 8c23e55bd..c6fb8ea85 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb.Queries; @@ -19,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { internal sealed class QueryIdsAsync : OperationBase { - private static readonly List<(DomainId SchemaId, DomainId Id)> EmptyIds = new List<(DomainId SchemaId, DomainId Id)>(); + private static readonly List<(DomainId SchemaId, DomainId Id, Status Status)> EmptyIds = new List<(DomainId SchemaId, DomainId Id, Status Status)>(); private readonly IAppProvider appProvider; public QueryIdsAsync(IAppProvider appProvider) @@ -38,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); } - public async Task> DoAsync(DomainId appId, HashSet ids) + public async Task> DoAsync(DomainId appId, HashSet ids) { var documentIds = ids.Select(x => DomainId.Combine(appId, x)); @@ -47,14 +48,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations Filter.In(x => x.DocumentId, documentIds), Filter.Ne(x => x.IsDeleted, true)); - var contentEntities = - await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) - .ToListAsync(); - - return contentEntities.Select(x => (DomainId.Create(x[Fields.SchemaId].AsString), DomainId.Create(x[Fields.Id].AsString))).ToList(); + return await SearchAsync(filter); } - public async Task> DoAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) + public async Task> DoAsync(DomainId appId, DomainId schemaId, FilterNode filterNode) { var schema = await appProvider.GetSchemaAsync(appId, schemaId, false); @@ -65,14 +62,23 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations var filter = BuildFilter(filterNode.AdjustToModel(schema.SchemaDef), appId, schemaId); + return await SearchAsync(filter); + } + + private async Task> SearchAsync(FilterDefinition filter) + { var contentEntities = - await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) + await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId, x => x.Status) .ToListAsync(); - return contentEntities.Select(x => (DomainId.Create(x[Fields.SchemaId].AsString), DomainId.Create(x[Fields.Id].AsString))).ToList(); + return contentEntities.Select(x => ( + DomainId.Create(x[Fields.SchemaId].AsString), + DomainId.Create(x[Fields.Id].AsString), + new Status(x[Fields.Status].AsString) + )).ToList(); } - public static FilterDefinition BuildFilter(FilterNode? filterNode, DomainId appId, DomainId schemaId) + private static FilterDefinition BuildFilter(FilterNode? filterNode, DomainId appId, DomainId schemaId) { var filters = new List> { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index bef1b4053..399858954 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using NodaTime; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -24,9 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, DomainId? referenced, SearchScope scope); - Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode); + Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode); - Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope); + Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs index bc5f83f0a..2a70b4d71 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs @@ -36,6 +36,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation yield break; } + var isRequired = IsRequired(context, field.RawProperties); + if (field is IField assetsField) { var checkAssets = new CheckAssets(async ids => @@ -43,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation return await assetRepository.QueryAsync(context.AppId.Id, new HashSet(ids)); }); - yield return new AssetsValidator(assetsField.Properties, checkAssets); + yield return new AssetsValidator(isRequired, assetsField.Properties, checkAssets); } if (field is IField referencesField) @@ -53,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation return await contentRepository.QueryIdsAsync(context.AppId.Id, ids, SearchScope.All); }); - yield return new ReferencesValidator(referencesField.Properties.SchemaIds, checkReferences); + yield return new ReferencesValidator(isRequired, referencesField.Properties, checkReferences); } if (field is IField numberField && numberField.Properties.IsUnique) @@ -76,5 +78,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation yield return new UniqueValidator(checkUniqueness); } } + + private static bool IsRequired(ValidatorContext context, FieldProperties properties) + { + var isRequired = properties.IsRequired; + + if (context.Action == ValidationAction.Publish) + { + isRequired = isRequired || properties.IsRequiredOnPublish; + } + + return isRequired; + } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index 0dfb21bbf..0287d8870 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -75,6 +75,14 @@ namespace Squidex.Infrastructure.MongoDb return find.Project(Builders.Projection.Include(include1).Include(include2)); } + public static IFindFluent Only(this IFindFluent find, + Expression> include1, + Expression> include2, + Expression> include3) + { + return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); + } + public static IFindFluent Not(this IFindFluent find, Expression> exclude) { diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index a8486a55a..e9783eebf 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -403,12 +403,6 @@ Al momento l'applicazione è in sola lettura. - - Contiene un collegamento '{id}' non valido. - - - Contiene dei collegamenti '{id}' ad uno schema errato. - Rimuovi @@ -601,6 +595,12 @@ Deve seguire il pattern. + + Contiene un collegamento '{id}' non valido. + + + Contiene dei collegamenti '{id}' ad uno schema errato. + La Regex è troppo lenta. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 4327aa279..d1b2c183b 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -403,12 +403,6 @@ Applicatie is momenteel in de alleen-lezen modus. - - Bevat ongeldige referentie '{id}'. - - - Bevat verwijzing '{id}' naar ongeldig schema. - Verwijderen @@ -601,6 +595,12 @@ Moet het patroon volgen. + + Bevat ongeldige referentie '{id}'. + + + Bevat verwijzing '{id}' naar ongeldig schema. + Regex is te traag. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index f4b63d137..65dc55e89 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -403,12 +403,6 @@ Application is in readonly mode at the moment. - - Contains invalid reference '{id}'. - - - Contains reference '{id}' to invalid schema. - Remove @@ -601,6 +595,12 @@ Must follow the pattern. + + Reference '{id}' not found. + + + Reference '{id}' has invalid schema. + Regex is too slow. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index 807b645ca..ff547b651 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -39,6 +39,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public bool ResolveReference { get; set; } + /// + /// True when all references must be published. + /// + public bool MustBePublished { get; set; } + /// /// The editor that is used to manage this field. /// 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 b710c76c4..4b1609c2b 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 @@ -5,14 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using FluentAssertions; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; -using Squidex.Infrastructure.Json.Objects; using Xunit; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent @@ -34,93 +30,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(null, errors); Assert.Empty(errors); } - [Fact] - public async Task Should_not_add_error_if_number_of_assets_is_equal_to_min_and_max_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_ignored() - { - var sut = Field(new AssetsFieldProperties { AllowDuplicates = true }); - - await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_add_error_if_assets_are_required_and_null() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_assets_are_required_and_empty() - { - var sut = Field(new AssetsFieldProperties { IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new AssetsFieldProperties { MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new AssetsFieldProperties { MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(Guid.NewGuid(), Guid.NewGuid()), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_values_contains_duplicate() - { - var sut = Field(new AssetsFieldProperties()); - - var id = Guid.NewGuid(); - - await sut.ValidateAsync(CreateValue(id, id), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } - - private static IJsonValue CreateValue(params Guid[]? ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - private static RootField Field(AssetsFieldProperties properties) { return Fields.Assets(1, "my-assets", 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 63d8c4b4f..9a1d05542 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 @@ -6,13 +6,9 @@ // ========================================================================== 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; -using Squidex.Infrastructure.Json.Objects; using Xunit; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent @@ -20,9 +16,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public class ReferencesFieldTests : IClassFixture { private readonly List errors = new List(); - private readonly DomainId schemaId = DomainId.NewGuid(); - private readonly DomainId ref1 = DomainId.NewGuid(); - private readonly DomainId ref2 = DomainId.NewGuid(); [Fact] public void Should_instantiate_field() @@ -32,106 +25,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent Assert.Equal("my-refs", sut.Name); } - [Fact] - public async Task Should_not_add_error_if_references_are_valid() - { - var sut = Field(new ReferencesFieldProperties()); - - await sut.ValidateAsync(CreateValue(ref1), errors); - - Assert.Empty(errors); - } - [Fact] public async Task Should_not_add_error_if_references_are_null_and_valid() { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_number_of_references_is_equal_to_min_and_max_items() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors); - - Assert.Empty(errors); - } - - [Fact] - public async Task Should_not_add_error_if_duplicate_values_are_allowed() - { - var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); - - await sut.ValidateAsync(CreateValue(ref1, ref1), errors); + await sut.ValidateAsync(null, errors); Assert.Empty(errors); } - [Fact] - public async Task Should_add_error_if_references_are_required_and_null() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - - await sut.ValidateAsync(CreateValue(null), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_references_are_required_and_empty() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - - await sut.ValidateAsync(CreateValue(), errors); - - errors.Should().BeEquivalentTo( - new[] { "Field is required." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_not_enough_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must have at least 3 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_value_has_too_much_items() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); - - await sut.ValidateAsync(CreateValue(ref1, ref2), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not have more than 1 item(s)." }); - } - - [Fact] - public async Task Should_add_error_if_reference_contains_duplicate_values() - { - var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - - await sut.ValidateAsync(CreateValue(ref1, ref1), errors); - - errors.Should().BeEquivalentTo( - new[] { "Must not contain duplicate values." }); - } - - private static IJsonValue CreateValue(params DomainId[]? ids) - { - return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); - } - private static RootField Field(ReferencesFieldProperties properties) { return Fields.References(1, "my-refs", Partitioning.Invariant, properties); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index f9a712cd6..ac56d96f2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -31,9 +31,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public static Task ValidateAsync(this IValidator validator, object? value, IList errors, Schema? schema = null, ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null) + ValidationUpdater? updater = null, + ValidationAction action = ValidationAction.Upsert) { - var context = CreateContext(schema, mode, updater); + var context = CreateContext(schema, mode, updater, action); return validator.ValidateAsync(value, context, CreateFormatter(errors)); } @@ -42,9 +43,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent Schema? schema = null, ValidationMode mode = ValidationMode.Default, ValidationUpdater? updater = null, - IValidatorsFactory? factory = null) + IValidatorsFactory? factory = null, + ValidationAction action = ValidationAction.Upsert) { - var context = CreateContext(schema, mode, updater); + var context = CreateContext(schema, mode, updater, action); var validators = Factories(factory).SelectMany(x => x.CreateValueValidators(context, field, null!)).ToArray(); @@ -56,9 +58,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent Schema? schema = null, ValidationMode mode = ValidationMode.Default, ValidationUpdater? updater = null, - IValidatorsFactory? factory = null) + IValidatorsFactory? factory = null, + ValidationAction action = ValidationAction.Upsert) { - var context = CreateContext(schema, mode, updater); + var context = CreateContext(schema, mode, updater, action); var validator = new ContentValidator(partitionResolver, context, Factories(factory), Log); @@ -74,9 +77,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent Schema? schema = null, ValidationMode mode = ValidationMode.Default, ValidationUpdater? updater = null, - IValidatorsFactory? factory = null) + IValidatorsFactory? factory = null, + ValidationAction action = ValidationAction.Upsert) { - var context = CreateContext(schema, mode, updater); + var context = CreateContext(schema, mode, updater, action); var validator = new ContentValidator(partitionResolver, context, Factories(factory), Log); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs index 15d83ac7e..823a3e805 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs @@ -89,6 +89,47 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators Assert.Empty(errors); } + [Fact] + public async Task Should_not_add_error_if_assets_are_null_but_not_required() + { + var sut = Validator(new AssetsFieldProperties()); + + await sut.ValidateAsync(null, errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_assets_are_empty_but_not_required() + { + var sut = Validator(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_duplicates_are_allowed() + { + var sut = Validator(new AssetsFieldProperties { AllowDuplicates = true }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_references_are_required() + { + var sut = Validator(new AssetsFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + [Fact] public async Task Should_add_error_if_asset_are_not_valid() { @@ -190,6 +231,39 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators new[] { "[2]: Must have aspect ratio 1:1." }); } + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var sut = Validator(new AssetsFieldProperties { MinItems = 2 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var sut = Validator(new AssetsFieldProperties { MaxItems = 1 }); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var sut = Validator(new AssetsFieldProperties()); + + await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); + } + [Fact] public async Task Should_add_error_if_image_has_invalid_extension() { @@ -212,7 +286,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators private IValidator Validator(AssetsFieldProperties properties) { - return new AssetsValidator(properties, ids => + return new AssetsValidator(properties.IsRequired, properties, ids => { return Task.FromResult>(new List { document, image1, image2 }); }); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs index 2518716eb..9d83e82e4 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs @@ -9,7 +9,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Infrastructure; using Xunit; @@ -19,40 +22,185 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators public class ReferencesValidatorTests : IClassFixture { private readonly List errors = new List(); - private readonly DomainId schemaId = DomainId.NewGuid(); + private readonly DomainId schemaId1 = DomainId.NewGuid(); + private readonly DomainId schemaId2 = DomainId.NewGuid(); private readonly DomainId ref1 = DomainId.NewGuid(); private readonly DomainId ref2 = DomainId.NewGuid(); [Fact] - public async Task Should_add_error_if_references_are_not_valid() + public async Task Should_not_add_error_if_reference_invalid_but_publishing() { - var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences()); + var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; - await sut.ValidateAsync(CreateValue(ref1), errors); + var sut = Validator(properties, FoundReferences((schemaId2, ref2, Status.Published))); - errors.Should().BeEquivalentTo( - new[] { $"Contains invalid reference '{ref1}'." }); + await sut.ValidateAsync(CreateValue(ref2), errors, action: ValidationAction.Publish); + + Assert.Empty(errors); } [Fact] public async Task Should_not_add_error_if_schemas_not_defined() { - var sut = new ReferencesValidator(null, FoundReferences((DomainId.NewGuid(), ref2))); + var properties = new ReferencesFieldProperties(); + + var sut = Validator(properties, FoundReferences((schemaId2, ref2, Status.Published))); + + await sut.ValidateAsync(CreateValue(ref2), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schema_is_valid() + { + var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; + + var sut = Validator(properties, FoundReferences((schemaId1, ref2, Status.Published))); await sut.ValidateAsync(CreateValue(ref2), errors); Assert.Empty(errors); } + [Fact] + public async Task Should_not_add_error_if_references_are_null_but_not_required() + { + var properties = new ReferencesFieldProperties(); + + var sut = Validator(properties, FoundReferences()); + + await sut.ValidateAsync(null, errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_references_are_empty_but_not_required() + { + var properties = new ReferencesFieldProperties(); + + var sut = Validator(properties, FoundReferences()); + + await sut.ValidateAsync(CreateValue(), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_duplicates_are_allowed() + { + var properties = new ReferencesFieldProperties { AllowDuplicates = true }; + + var sut = Validator(properties, FoundReferences((schemaId1, ref1, Status.Published))); + + await sut.ValidateAsync(CreateValue(ref1, ref1), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_references_are_required() + { + var properties = new ReferencesFieldProperties { IsRequired = true }; + + var sut = Validator(properties, FoundReferences()); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_references_are_published_required() + { + var properties = new ReferencesFieldProperties { MustBePublished = true, IsRequired = true }; + + var sut = Validator(properties, FoundReferences((schemaId1, ref1, Status.Published))); + + await sut.ValidateAsync(CreateValue(), errors); + + errors.Should().BeEquivalentTo( + new[] { "Field is required." }); + } + + [Fact] + public async Task Should_add_error_if_references_are_not_valid() + { + var properties = new ReferencesFieldProperties(); + + var sut = Validator(properties, FoundReferences()); + + await sut.ValidateAsync(CreateValue(ref1), errors); + + errors.Should().BeEquivalentTo( + new[] { $"[1]: Reference '{ref1}' not found." }); + } + [Fact] public async Task Should_add_error_if_reference_schema_is_not_valid() { - var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences((DomainId.NewGuid(), ref2))); + var properties = new ReferencesFieldProperties { SchemaId = schemaId1 }; + + var sut = Validator(properties, FoundReferences((schemaId2, ref2, Status.Draft))); await sut.ValidateAsync(CreateValue(ref2), errors); errors.Should().BeEquivalentTo( - new[] { $"Contains reference '{ref2}' to invalid schema." }); + new[] { $"[1]: Reference '{ref2}' has invalid schema." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_items() + { + var properties = new ReferencesFieldProperties { MinItems = 2 }; + + var sut = Validator(properties, FoundReferences((schemaId2, ref2, Status.Draft))); + + await sut.ValidateAsync(CreateValue(ref2), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_enough_published_items() + { + var properties = new ReferencesFieldProperties { MinItems = 2, MustBePublished = true }; + + var sut = Validator(properties, FoundReferences((schemaId1, ref1, Status.Published), (schemaId1, ref2, Status.Draft))); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_has_too_much_items() + { + var properties = new ReferencesFieldProperties { MaxItems = 1 }; + + var sut = Validator(properties, FoundReferences((schemaId1, ref1, Status.Published), (schemaId1, ref2, Status.Draft))); + + await sut.ValidateAsync(CreateValue(ref1, ref2), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1 item(s)." }); + } + + [Fact] + public async Task Should_add_error_if_reference_contains_duplicate_values() + { + var properties = new ReferencesFieldProperties(); + + var sut = Validator(properties, FoundReferences((schemaId1, ref1, Status.Published))); + + await sut.ValidateAsync(CreateValue(ref1, ref1), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not contain duplicate values." }); } private static List CreateValue(params DomainId[] ids) @@ -60,9 +208,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators return ids.ToList(); } - private static CheckContentsByIds FoundReferences(params (DomainId SchemaId, DomainId Id)[] references) + private static CheckContentsByIds FoundReferences(params (DomainId SchemaId, DomainId Id, Status Status)[] references) + { + return x => Task.FromResult>(references.ToList()); + } + + private IValidator Validator(ReferencesFieldProperties properties, CheckContentsByIds found) { - return x => Task.FromResult>(references.ToList()); + return new ReferencesValidator(properties.IsRequired, properties, found); } } } 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 d1d99d56a..76b0cade2 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 @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Infrastructure; @@ -91,7 +92,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { filter?.Invoke(filterNode.ToString()); - return Task.FromResult>(new List<(DomainId, DomainId)> { (schemaId, id) }); + return Task.FromResult>(new List<(DomainId, DomainId, Status)> { (schemaId, id, Status.Published) }); }; } } diff --git a/frontend/app/features/content/shared/list/content-list-cell.directive.ts b/frontend/app/features/content/shared/list/content-list-cell.directive.ts index c3cb45572..d8a296bdf 100644 --- a/frontend/app/features/content/shared/list/content-list-cell.directive.ts +++ b/frontend/app/features/content/shared/list/content-list-cell.directive.ts @@ -42,7 +42,7 @@ export function getCellWidth(field: TableField) { case MetaFields.statusNext: return 240; case MetaFields.statusColor: - return 80; + return 50; case MetaFields.version: return 80; } diff --git a/frontend/app/features/content/shared/references/content-selector-item.component.html b/frontend/app/features/content/shared/references/content-selector-item.component.html index 47d34cc93..0e85df3fd 100644 --- a/frontend/app/features/content/shared/references/content-selector-item.component.html +++ b/frontend/app/features/content/shared/references/content-selector-item.component.html @@ -8,11 +8,15 @@ - + + + + + \ No newline at end of file diff --git a/frontend/app/features/content/shared/references/content-selector.component.html b/frontend/app/features/content/shared/references/content-selector.component.html index 594fe0851..7f60705da 100644 --- a/frontend/app/features/content/shared/references/content-selector.component.html +++ b/frontend/app/features/content/shared/references/content-selector.component.html @@ -44,7 +44,9 @@ - + @@ -57,6 +59,9 @@ [language]="language"> + + + diff --git a/frontend/app/features/content/shared/references/reference-item.component.html b/frontend/app/features/content/shared/references/reference-item.component.html index 03e68b105..10318c83d 100644 --- a/frontend/app/features/content/shared/references/reference-item.component.html +++ b/frontend/app/features/content/shared/references/reference-item.component.html @@ -11,6 +11,10 @@ + + + + {{content.schemaDisplayName}} diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html index f1b6674d6..23d938f16 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.html @@ -3,7 +3,8 @@
- +
@@ -32,6 +33,17 @@ +
+
+
+ + +
+
+
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts index cae1275b5..f68d13e59 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-validation.component.ts @@ -44,5 +44,8 @@ export class ReferencesValidationComponent implements OnInit { this.fieldForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + + this.fieldForm.setControl('mustBePublished', + new FormControl(this.properties.mustBePublished)); } } \ No newline at end of file diff --git a/frontend/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts index 89be0f843..cd749d86d 100644 --- a/frontend/app/shared/services/schemas.types.ts +++ b/frontend/app/shared/services/schemas.types.ts @@ -330,6 +330,7 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { public readonly editor: ReferencesFieldEditor = 'List'; public readonly maxItems?: number; public readonly minItems?: number; + public readonly mustBePublished?: boolean; public readonly resolveReference?: boolean; public readonly schemaIds?: ReadonlyArray;