From bbe951c677be76b3e06901f1d2f983a5de227696 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 31 Oct 2020 22:32:15 +0100 Subject: [PATCH] Validation fixes and improvements. --- .../CompositeUniqueValidatorFactory.cs | 2 +- backend/i18n/frontend_en.json | 3 + backend/i18n/frontend_it.json | 3 + backend/i18n/frontend_nl.json | 3 + backend/i18n/source/backend_en.json | 1 - backend/i18n/source/backend_it.json | 1 - backend/i18n/source/backend_nl.json | 1 - backend/i18n/source/frontend_en.json | 3 + .../Schemas/FieldProperties.cs | 2 + .../Schemas/SchemaProperties.cs | 6 +- .../SchemaSynchronizer.cs | 2 +- .../ValidateContent/ContentValidator.cs | 20 ++--- .../DefaultFieldValueValidatorsFactory.cs | 75 +++++++++++++----- .../DefaultValidatorsFactory.cs | 6 +- .../ValidateContent/IValidatorsFactory.cs | 6 +- .../ValidateContent/ValidationAction.cs | 15 ++++ .../ValidateContent/ValidationContext.cs | 76 +++++++++---------- .../ValidateContent/ValidatorContext.cs | 36 +++++++++ .../Validators/AggregateValidator.cs | 2 +- .../Validators/AssetsValidator.cs | 13 ++-- .../Validators/CollectionItemValidator.cs | 1 - .../Validators/FieldValidator.cs | 7 +- .../Validators/ObjectValidator.cs | 10 +-- .../Validators/ReferencesValidator.cs | 5 -- .../Validators/UniqueValidator.cs | 5 -- .../Contents/MongoContentEntity.cs | 2 +- .../Operations/QueryReferrersAsync.cs | 6 +- .../Contents/Commands/ChangeContentStatus.cs | 2 + .../Contents/ContentDomainObject.cs | 56 ++++---------- .../Contents/Guards/GuardContent.cs | 13 +++- .../ContentOperationContext.cs | 75 +++++++++++++++--- .../Validation/DependencyValidatorsFactory.cs | 7 +- backend/src/Squidex.Domain.Apps.Entities/Q.cs | 18 ++--- backend/src/Squidex.Shared/Texts.it.resx | 3 - backend/src/Squidex.Shared/Texts.nl.resx | 3 - backend/src/Squidex.Shared/Texts.resx | 3 - .../Schemas/Models/FieldPropertiesDto.cs | 5 ++ .../Schemas/Models/SchemaPropertiesDto.cs | 5 ++ .../Schemas/Models/UpdateSchemaDto.cs | 5 ++ .../Squidex/Config/Domain/ContentsServices.cs | 1 + .../ValidationTestExtensions.cs | 8 +- .../Validators/AssetsValidatorTests.cs | 12 --- .../Validators/ReferencesValidatorTests.cs | 10 --- .../Contents/ContentDomainObjectTests.cs | 13 +++- .../Contents/Guard/GuardContentTests.cs | 40 +++++++--- .../DefaultXmlRepositoryTests.cs | 8 +- .../common/schema-edit-form.component.html | 11 ++- .../field-form-validation.component.html | 11 +++ .../types/string-validation.component.html | 9 ++- .../shared/services/schemas.service.spec.ts | 2 + .../app/shared/services/schemas.service.ts | 3 + frontend/app/shared/services/schemas.types.ts | 1 + frontend/app/shared/state/schemas.forms.ts | 2 + frontend/app/theme/_forms.scss | 2 +- 54 files changed, 406 insertions(+), 234 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/{ => Operations}/ContentOperationContext.cs (65%) diff --git a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs index 276f40947..944ad88fc 100644 --- a/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs +++ b/backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs @@ -25,7 +25,7 @@ namespace Squidex.Extensions.Validation this.contentRepository = contentRepository; } - public IEnumerable CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator) + public IEnumerable CreateContentValidators(ValidatorContext context, FieldValidatorFactory createFieldValidator) { foreach (var validatorTag in ValidatorTags(context.Schema.Properties.Tags)) { diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 6423c9df4..183e05c56 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -686,6 +686,7 @@ "schemas.field.placeholder": "Placeholder", "schemas.field.placeholderHint": "Define the placeholder for the input control.", "schemas.field.required": "Required", + "schemas.field.requiredOnPublish": "Required when publishing", "schemas.field.show": "Show in API", "schemas.field.tabCommon": "Common", "schemas.field.tabEditing": "Editing", @@ -741,6 +742,7 @@ "schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.suggestions": "Suggestions", + "schemas.fieldTypes.string.wordHint": "Word count and character count are calculated on the plain text. The plain text is calculated based on the defined content type, which can either be Markdown or HTML.", "schemas.fieldTypes.string.words": "Words", "schemas.fieldTypes.string.wordsMax": "Max Words", "schemas.fieldTypes.string.wordsMin": "Min Words", @@ -805,6 +807,7 @@ "schemas.updateRulesFailed": "Failed to update schema rules. Please reload.", "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", + "schemas.validateOnPublish": "Validate when publishing", "search.addFilter": "Add Filter", "search.addGroup": "Add Group", "search.addSorting": "Add Sorting", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index f01614d4b..6c19502d0 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -686,6 +686,7 @@ "schemas.field.placeholder": "Segnaposto", "schemas.field.placeholderHint": "Definisci il segnaposto per la verifica dell'input.", "schemas.field.required": "Obbligatorio", + "schemas.field.requiredOnPublish": "Required when publishing", "schemas.field.show": "Mostra nelle API", "schemas.field.tabCommon": "Comune", "schemas.field.tabEditing": "Modifica", @@ -741,6 +742,7 @@ "schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.patternMessage": "Messaggio del Pattern", "schemas.fieldTypes.string.suggestions": "Suggerimenti", + "schemas.fieldTypes.string.wordHint": "Word count and character count are calculated on the plain text. The plain text is calculated based on the defined content type, which can either be Markdown or HTML.", "schemas.fieldTypes.string.words": "Parole", "schemas.fieldTypes.string.wordsMax": "Numero max di Parole", "schemas.fieldTypes.string.wordsMin": "Numero min di Parole", @@ -805,6 +807,7 @@ "schemas.updateRulesFailed": "Non è stato possibile aggiornare le regole dello schema. Per favore ricarica.", "schemas.updateScriptsFailed": "Non è stato possibile aggiornare gli script dello schema. Per favore ricarica.", "schemas.updateUIFieldsFailed": "Non è stato possibile aggiornare i campi della UI. Per favore ricarica.", + "schemas.validateOnPublish": "Validate when publishing", "search.addFilter": "Aggiungi un Filtro", "search.addGroup": "Aggiungi un Gruppo", "search.addSorting": "Aggiungi ordinamento", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index a08982012..b8245f6e4 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -686,6 +686,7 @@ "schemas.field.placeholder": "Placeholder", "schemas.field.placeholderHint": "Definieer de tijdelijke aanduiding voor het invoerbesturingselement.", "schemas.field.required": "Vereist", + "schemas.field.requiredOnPublish": "Required when publishing", "schemas.field.show": "Weergeven in API", "schemas.field.tabCommon": "Algemeen", "schemas.field.tabEditing": "Bewerken", @@ -741,6 +742,7 @@ "schemas.fieldTypes.string.pattern": "Regex-patroon", "schemas.fieldTypes.string.patternMessage": "Patroonbericht", "schemas.fieldTypes.string.suggestions": "Suggesties", + "schemas.fieldTypes.string.wordHint": "Word count and character count are calculated on the plain text. The plain text is calculated based on the defined content type, which can either be Markdown or HTML.", "schemas.fieldTypes.string.words": "Woorden", "schemas.fieldTypes.string.wordsMax": "Max. Woorden", "schemas.fieldTypes.string.wordsMin": "Min. Woorden", @@ -805,6 +807,7 @@ "schemas.updateRulesFailed": "Updaten van schemaregels is mislukt. Laad opnieuw.", "schemas.updateScriptsFailed": "Updaten van schemascripts is mislukt. Laad opnieuw.", "schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.", + "schemas.validateOnPublish": "Validate when publishing", "search.addFilter": "Filter toevoegen", "search.addGroup": "Groep toevoegen", "search.addSorting": "Sortering toevoegen", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 4b4c6a32f..96c6a62a4 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -119,7 +119,6 @@ "common.workflow": "Workflow", "common.workflowStep": "Step", "common.workflowTransition": "Transition", - "contents.alreadyDeleted": "Content has already been deleted.", "contents.bulkInsertQueryNotUnique": "More than one content matches to the query.", "contents.draftNotCreateForUnpublished": "You can only create a new version when the content is published.", "contents.draftToDeleteNotFound": "There is nothing to delete.", diff --git a/backend/i18n/source/backend_it.json b/backend/i18n/source/backend_it.json index c8ac6806c..6e376973d 100644 --- a/backend/i18n/source/backend_it.json +++ b/backend/i18n/source/backend_it.json @@ -118,7 +118,6 @@ "common.workflow": "Workflow", "common.workflowStep": "Step", "common.workflowTransition": "Transizione", - "contents.alreadyDeleted": "Il contento è stato già eliminato.", "contents.bulkInsertQueryNotUnique": "Ci sono più contenuti che corrispondono alla query.", "contents.draftNotCreateForUnpublished": "Puoi creare versioni del contenuto solo se questo è pubblicato.", "contents.draftToDeleteNotFound": "Non c'è niente da eliminare.", diff --git a/backend/i18n/source/backend_nl.json b/backend/i18n/source/backend_nl.json index ed07d292f..fcb5095df 100644 --- a/backend/i18n/source/backend_nl.json +++ b/backend/i18n/source/backend_nl.json @@ -119,7 +119,6 @@ "common.workflow": "Workflow", "common.workflowStep": "Stap", "common.workflowTransition": "Overgang", - "contents.alreadyDeleted": "Content is al verwijderd.", "contents.bulkInsertQueryNotUnique": "Meer dan één inhoud komt overeen met de zoekopdracht.", "contents.draftNotCreateForUnpublished": "Je kunt alleen een nieuwe versie maken wanneer de inhoud is gepubliceerd.", "contents.draftToDeleteNotFound": "Er is niets te verwijderen.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 6423c9df4..183e05c56 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -686,6 +686,7 @@ "schemas.field.placeholder": "Placeholder", "schemas.field.placeholderHint": "Define the placeholder for the input control.", "schemas.field.required": "Required", + "schemas.field.requiredOnPublish": "Required when publishing", "schemas.field.show": "Show in API", "schemas.field.tabCommon": "Common", "schemas.field.tabEditing": "Editing", @@ -741,6 +742,7 @@ "schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.suggestions": "Suggestions", + "schemas.fieldTypes.string.wordHint": "Word count and character count are calculated on the plain text. The plain text is calculated based on the defined content type, which can either be Markdown or HTML.", "schemas.fieldTypes.string.words": "Words", "schemas.fieldTypes.string.wordsMax": "Max Words", "schemas.fieldTypes.string.wordsMin": "Min Words", @@ -805,6 +807,7 @@ "schemas.updateRulesFailed": "Failed to update schema rules. Please reload.", "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", + "schemas.validateOnPublish": "Validate when publishing", "search.addFilter": "Add Filter", "search.addGroup": "Add Group", "search.addSorting": "Add Sorting", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index 152688f2f..41ccb8aaa 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas { public bool IsRequired { get; set; } + public bool IsRequiredOnPublish { get; set; } + public bool IsHalfWidth { get; set; } public string? Placeholder { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs index fa06a7f61..12e7d25e8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Collections.ObjectModel; -using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Core.Schemas { @@ -19,9 +18,6 @@ namespace Squidex.Domain.Apps.Core.Schemas public string? ContentSidebarUrl { get; set; } - public bool DeepEquals(SchemaProperties properties) - { - return SimpleEquals.IsEquals(this, properties); - } + public bool ValidateOnPublish { get; set; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index f8ba0ef96..0af929a76 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization return @event; } - if (!source.Properties.DeepEquals(target.Properties)) + if (!source.Properties.Equals(target.Properties)) { yield return E(new SchemaUpdated { Properties = target.Properties }); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index 955de873e..c6decb238 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -39,11 +39,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent Guard.NotNull(context, nameof(context)); Guard.NotNull(factories, nameof(factories)); Guard.NotNull(partitionResolver, nameof(partitionResolver)); + Guard.NotNull(log, nameof(log)); this.context = context; this.factories = factories; - this.log = log; this.partitionResolver = partitionResolver; + + this.log = log; } private void AddError(IEnumerable path, string message) @@ -82,28 +84,28 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private IValidator CreateSchemaValidator(bool isPartial) { - var fieldsValidators = new Dictionary(context.Schema.Fields.Count); + var fieldValidators = new Dictionary(context.Schema.Fields.Count); foreach (var field in context.Schema.Fields) { - fieldsValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); + fieldValidators[field.Name] = (!field.RawProperties.IsRequired, CreateFieldValidator(field, isPartial)); } - return new ObjectValidator(fieldsValidators, isPartial, "field"); + return new ObjectValidator(fieldValidators, isPartial, "field"); } private IValidator CreateFieldValidator(IRootField field, bool isPartial) { - var partitioning = partitionResolver(field.Partitioning); - var fieldValidator = CreateFieldValidator(field); - var fieldsValidators = new Dictionary(); + + var partitioning = partitionResolver(field.Partitioning); + var partitioningValidators = new Dictionary(); foreach (var partitionKey in partitioning.AllKeys) { var optional = partitioning.IsOptional(partitionKey); - fieldsValidators[partitionKey] = (optional, fieldValidator); + partitioningValidators[partitionKey] = (optional, fieldValidator); } var typeName = partitioning.ToString()!; @@ -111,7 +113,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return new AggregateValidator( CreateFieldValidators(field) .Union(Enumerable.Repeat( - new ObjectValidator(fieldsValidators, isPartial, typeName), 1)), log); + new ObjectValidator(partitioningValidators, isPartial, typeName), 1)), log); } private IValidator CreateFieldValidator(IField field) 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 75f229e8a..b019fcfb8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -17,18 +17,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor> { + private readonly ValidatorContext context; private readonly FieldValidatorFactory createFieldValidator; - private DefaultFieldValueValidatorsFactory(FieldValidatorFactory createFieldValidator) + private DefaultFieldValueValidatorsFactory(ValidatorContext context, FieldValidatorFactory createFieldValidator) { + this.context = context; this.createFieldValidator = createFieldValidator; } - public static IEnumerable CreateValidators(IField field, FieldValidatorFactory createFieldValidator) + public static IEnumerable CreateValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator) { + Guard.NotNull(context, nameof(context)); Guard.NotNull(field, nameof(field)); - var visitor = new DefaultFieldValueValidatorsFactory(createFieldValidator); + var visitor = new DefaultFieldValueValidatorsFactory(context, createFieldValidator); return field.Accept(visitor); } @@ -37,28 +40,32 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + var isRequired = IsRequired(properties); + + if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } - var nestedSchema = new Dictionary(field.Fields.Count); + var nestedValidators = new Dictionary(field.Fields.Count); foreach (var nestedField in field.Fields) { - nestedSchema[nestedField.Name] = (false, createFieldValidator(nestedField)); + nestedValidators[nestedField.Name] = (false, createFieldValidator(nestedField)); } - yield return new CollectionItemValidator(new ObjectValidator(nestedSchema, false, "field")); + yield return new CollectionItemValidator(new ObjectValidator(nestedValidators, false, "field")); } public IEnumerable Visit(IField field) { var properties = field.Properties; - if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + var isRequired = IsRequired(properties); + + if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } if (!properties.AllowDuplicates) @@ -71,7 +78,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired) + var isRequired = IsRequired(properties); + + if (isRequired) { yield return new RequiredValidator(); } @@ -81,7 +90,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired) + var isRequired = IsRequired(properties); + + if (isRequired) { yield return new RequiredValidator(); } @@ -96,7 +107,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired) + var isRequired = IsRequired(properties); + + if (isRequired) { yield return new RequiredValidator(); } @@ -106,7 +119,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired) + var isRequired = IsRequired(properties); + + if (isRequired) { yield return new RequiredValidator(); } @@ -116,7 +131,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired) + var isRequired = IsRequired(properties); + + if (isRequired) { yield return new RequiredValidator(); } @@ -136,9 +153,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + var isRequired = IsRequired(properties); + + if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } if (!properties.AllowDuplicates) @@ -151,7 +170,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired) + var isRequired = IsRequired(properties); + + if (isRequired) { yield return new RequiredStringValidator(true); } @@ -200,9 +221,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var properties = field.Properties; - if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + var isRequired = IsRequired(properties); + + if (isRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems); } if (properties.AllowedValues != null) @@ -220,5 +243,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return NoValueValidator.Instance; } } + + private bool IsRequired(FieldProperties properties) + { + var isRequired = properties.IsRequired; + + if (context.Action == ValidationAction.Publish) + { + isRequired = isRequired || properties.IsRequiredOnPublish; + } + + return isRequired; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs index ec75360a2..df8bd5d56 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { public sealed class DefaultValidatorsFactory : IValidatorsFactory { - public IEnumerable CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + public IEnumerable CreateFieldValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator) { if (field is IField) { @@ -21,9 +21,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } } - public IEnumerable CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + public IEnumerable CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator) { - return DefaultFieldValueValidatorsFactory.CreateValidators(field, createFieldValidator); + return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, createFieldValidator); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs index 15e36d2f4..e0b982325 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs @@ -14,17 +14,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public interface IValidatorsFactory { - IEnumerable CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + IEnumerable CreateFieldValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator) { yield break; } - IEnumerable CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + IEnumerable CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator) { yield break; } - IEnumerable CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator) + IEnumerable CreateContentValidators(ValidatorContext context, FieldValidatorFactory createFieldValidator) { yield break; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs new file mode 100644 index 000000000..6172bf391 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public enum ValidationAction + { + Upsert, + Publish + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index de04a71b3..7a611e19e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -5,91 +5,83 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Immutable; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.ValidateContent { - public sealed class ValidationContext + public sealed class ValidationContext : ValidatorContext { - public ImmutableQueue Path { get; } - - public NamedId AppId { get; } - - public NamedId SchemaId { get; } - - public Schema Schema { get; } + public ImmutableQueue Path { get; private set; } = ImmutableQueue.Empty; public DomainId ContentId { get; } - public bool IsOptional { get; } - - public ValidationMode Mode { get; } + public bool IsOptional { get; private set; } public ValidationContext( NamedId appId, NamedId schemaId, Schema schema, - DomainId contentId, - ValidationMode mode = ValidationMode.Default) - : this(appId, schemaId, schema, contentId, ImmutableQueue.Empty, false, mode) + DomainId contentId) + : base(appId, schemaId, schema) { + ContentId = contentId; } - private ValidationContext( - NamedId appId, - NamedId schemaId, - Schema schema, - DomainId contentId, - ImmutableQueue path, - bool isOptional, - ValidationMode mode = ValidationMode.Default) + public ValidationContext Optimized(bool optimized = true) { - AppId = appId; - - ContentId = contentId; - - Mode = mode; + return WithMode(optimized ? ValidationMode.Optimized : ValidationMode.Default); + } - Schema = schema; - SchemaId = schemaId; + public ValidationContext AsPublishing(bool publish = true) + { + return WithAction(publish ? ValidationAction.Publish : ValidationAction.Upsert); + } - IsOptional = isOptional; + public ValidationContext Optional(bool isOptional = true) + { + if (IsOptional == isOptional) + { + return this; + } - Path = path; + return Clone(clone => clone.IsOptional = isOptional); } - public ValidationContext Optimized(bool isOptimized = true) + public ValidationContext WithAction(ValidationAction action) { - var mode = isOptimized ? ValidationMode.Optimized : ValidationMode.Default; - - if (Mode == mode) + if (Action == action) { return this; } - return Clone(Path, IsOptional, mode); + return Clone(clone => clone.Action = action); } - public ValidationContext Optional(bool fieldIsOptional) + public ValidationContext WithMode(ValidationMode mode) { - if (IsOptional == fieldIsOptional) + if (Mode == mode) { return this; } - return Clone(Path, fieldIsOptional, Mode); + return Clone(clone => clone.Mode = mode); } public ValidationContext Nested(string property) { - return Clone(Path.Enqueue(property), IsOptional, Mode); + return Clone(clone => clone.Path = clone.Path.Enqueue(property)); } - private ValidationContext Clone(ImmutableQueue path, bool isOptional, ValidationMode mode) + private ValidationContext Clone(Action updater) { - return new ValidationContext(AppId, SchemaId, Schema, ContentId, path, isOptional, mode); + var clone = (ValidationContext)MemberwiseClone(); + + updater(clone); + + return clone; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs new file mode 100644 index 000000000..4bac3539e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public abstract class ValidatorContext + { + public NamedId AppId { get; } + + public NamedId SchemaId { get; } + + public Schema Schema { get; } + + public ValidationMode Mode { get; protected set; } + + public ValidationAction Action { get; protected set; } + + protected ValidatorContext( + NamedId appId, + NamedId schemaId, + Schema schema) + { + AppId = appId; + + Schema = schema; + SchemaId = schemaId; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs index d4d31a22e..258348e11 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { - public sealed class AggregateValidator : IValidator + internal sealed class AggregateValidator : IValidator { private readonly IValidator[]? validators; private readonly ISemanticLog log; 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 5f247c5d3..8a63b958f 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 @@ -35,11 +35,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { - if (context.Mode == ValidationMode.Optimized) - { - return; - } - if (value is ICollection assetIds && assetIds.Count > 0) { var assets = await checkAssets(assetIds); @@ -61,12 +56,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) { - addError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min = properties.MinSize.Value.ToReadableSize() })); + var min = properties.MinSize.Value.ToReadableSize(); + + addError(path, T.Get("contents.validation.minimumSize", new { size = asset.FileSize.ToReadableSize(), min })); } if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize) { - addError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max = properties.MaxSize.Value.ToReadableSize() })); + var max = properties.MaxSize.Value.ToReadableSize(); + + addError(path, T.Get("contents.validation.maximumSize", new { size = asset.FileSize.ToReadableSize(), max })); } if (properties.AllowedExtensions != null && diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs index ec44f68e9..42d4485da 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -18,7 +18,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public CollectionItemValidator(params IValidator[] itemValidators) { - Guard.NotNull(itemValidators, nameof(itemValidators)); Guard.NotEmpty(itemValidators, nameof(itemValidators)); this.itemValidators = itemValidators; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index a9f7fd8c9..79911b17d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -17,14 +18,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public sealed class FieldValidator : IValidator { - private readonly IValidator[]? validators; + private readonly IValidator[] validators; private readonly IField field; public FieldValidator(IEnumerable? validators, IField field) { Guard.NotNull(field, nameof(field)); - this.validators = validators?.ToArray(); + this.validators = validators?.ToArray() ?? Array.Empty(); this.field = field; } @@ -62,7 +63,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators return; } - if (validators?.Length > 0) + if (validators.Length > 0) { var tasks = new List(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs index b201d2047..8250fc828 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs @@ -14,13 +14,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public sealed class ObjectValidator : IValidator { private static readonly IReadOnlyDictionary DefaultValue = new Dictionary(); - private readonly IDictionary schema; + private readonly IDictionary fields; private readonly bool isPartial; private readonly string fieldType; - public ObjectValidator(IDictionary schema, bool isPartial, string fieldType) + public ObjectValidator(IDictionary fields, bool isPartial, string fieldType) { - this.schema = schema; + this.fields = fields; this.fieldType = fieldType; this.isPartial = isPartial; } @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { var name = fieldData.Key; - if (!schema.ContainsKey(name)) + if (!fields.ContainsKey(name)) { addError(context.Path.Enqueue(name), T.Get("contents.validation.unknownField", new { fieldType })); } @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators var tasks = new List(); - foreach (var (fieldName, fieldConfig) in schema) + foreach (var (fieldName, fieldConfig) in fields) { var (isOptional, validator) = fieldConfig; 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 2d02c4e74..95b5aafd1 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 @@ -31,11 +31,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { - if (context.Mode == ValidationMode.Optimized) - { - return; - } - if (value is ICollection contentIds) { var foundIds = await checkReferences(contentIds.ToHashSet()); 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 f7f9bb67a..1540409de 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 @@ -27,11 +27,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public async Task ValidateAsync(object? value, ValidationContext context, AddError addError) { - if (context.Mode == ValidationMode.Optimized) - { - return; - } - var count = context.Path.Count(); if (value != null && (count == 0 || (count == 2 && context.Path.Last() == InvariantPartitioning.Key))) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 354784803..3e3203d67 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public void LoadData(NamedContentData data, Schema schema, DataConverter converter) { - ReferencedIds = data.GetReferencedIds(schema).Select(x => DomainId.Combine(AppId, x)).ToHashSet(); + ReferencedIds = data.GetReferencedIds(schema).ToHashSet(); DataByIds = converter.ToMongoModel(data, schema); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs index 861db95cc..163b49a85 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs @@ -28,14 +28,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations public async Task DoAsync(DomainId appId, DomainId contentId) { - var currentId = DomainId.Combine(appId, contentId); - var filter = Filter.And( - Filter.AnyEq(x => x.ReferencedIds, appId), + Filter.AnyEq(x => x.ReferencedIds, contentId), Filter.Eq(x => x.IndexedAppId, appId), Filter.Ne(x => x.IsDeleted, true), - Filter.Ne(x => x.Id, currentId)); + Filter.Ne(x => x.Id, contentId)); var hasReferrerAsync = await Collection.Find(filter).Only(x => x.Id) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs index 69b8b0636..733ad09ee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs @@ -18,5 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public Instant? DueTime { get; set; } public DomainId? StatusJobId { get; set; } + + public bool DoNotValidate { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 354c2c2c5..0bee3f063 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Guards; -using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Operations; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Contents; @@ -22,28 +22,19 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Entities.Contents { public class ContentDomainObject : LogSnapshotDomainObject { - private readonly IContentWorkflow contentWorkflow; - private readonly IContentRepository contentRepository; private readonly ContentOperationContext context; public ContentDomainObject(IStore store, ISemanticLog log, - IContentWorkflow contentWorkflow, - IContentRepository contentRepository, ContentOperationContext context) : base(store, log) { - Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); Guard.NotNull(context, nameof(context)); - this.contentWorkflow = contentWorkflow; - this.contentRepository = contentRepository; this.context = context; } @@ -67,8 +58,6 @@ namespace Squidex.Domain.Apps.Entities.Contents public override Task ExecuteAsync(IAggregateCommand command) { - VerifyNotDeleted(); - switch (command) { case UpsertContent uspertContent: @@ -92,13 +81,13 @@ namespace Squidex.Domain.Apps.Entities.Contents { await LoadContext(c.AppId, c.SchemaId, c, c.OptimizeValidation); - await GuardContent.CanCreate(context.Schema, contentWorkflow, c); + await GuardContent.CanCreate(context.Schema, context.Workflow, c); - var status = await contentWorkflow.GetInitialStatusAsync(context.Schema); + var status = await context.GetInitialStatusAsync(); if (!c.DoNotValidate) { - await context.ValidateInputAsync(c.Data); + await context.ValidateInputAsync(c.Data, createContent.Publish); } if (!c.DoNotScript) @@ -144,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents GuardContent.CanCreateDraft(c, Snapshot); - var status = await contentWorkflow.GetInitialStatusAsync(context.Schema); + var status = await context.GetInitialStatusAsync(); CreateDraft(c, status); @@ -166,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents case UpdateContent updateContent: return UpdateReturnAsync(updateContent, async c => { - await GuardContent.CanUpdate(Snapshot, contentWorkflow, c); + await GuardContent.CanUpdate(Snapshot, context.Workflow, c); return await UpdateAsync(c, x => c.Data, false); }); @@ -174,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents case PatchContent patchContent: return UpdateReturnAsync(patchContent, async c => { - await GuardContent.CanPatch(Snapshot, contentWorkflow, c); + await GuardContent.CanPatch(Snapshot, context.Workflow, c); return await UpdateAsync(c, c.Data.MergeInto, true); }); @@ -186,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); - await GuardContent.CanChangeStatus(context.Schema, Snapshot, contentWorkflow, c); + await GuardContent.CanChangeStatus(context.Schema, Snapshot, context.Workflow, c); if (c.DueTime.HasValue) { @@ -196,7 +185,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var change = GetChange(c); - if (!c.DoNotScript) + if (!c.DoNotScript && context.HasScript(c => c.Change)) { var data = Snapshot.Data.Clone(); @@ -217,6 +206,11 @@ namespace Squidex.Domain.Apps.Entities.Contents } } + if (!c.DoNotValidate && change == StatusChange.Published) + { + await context.ValidateOnPublishAsync(Snapshot.Data); + } + ChangeStatus(c, change); } } @@ -240,7 +234,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { await LoadContext(Snapshot.AppId, Snapshot.SchemaId, c); - GuardContent.CanDelete(context.Schema, c); + await GuardContent.CanDelete(context.Schema, Snapshot, context.Repository, c); if (!c.DoNotScript) { @@ -254,16 +248,6 @@ namespace Squidex.Domain.Apps.Entities.Contents }); } - if (c.CheckReferrers) - { - var hasReferrer = await contentRepository.HasReferrersAsync(Snapshot.AppId.Id, c.AggregateId); - - if (hasReferrer) - { - throw new DomainException(T.Get("contents.referenced")); - } - } - Delete(c); }); @@ -290,7 +274,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - await context.ValidateInputAsync(command.Data); + await context.ValidateInputAsync(command.Data, false); } } @@ -387,14 +371,6 @@ namespace Squidex.Domain.Apps.Entities.Contents return change; } - private void VerifyNotDeleted() - { - if (Snapshot.IsDeleted) - { - throw new DomainException(T.Get("contents.alreadyDeleted")); - } - } - private Task LoadContext(NamedId appId, NamedId schemaId, ContentCommand command, bool optimized = false) { return context.LoadAsync(appId, schemaId, command, optimized); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 1fb0ed895..c1d30b0a8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -112,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards }); } - public static void CanDelete(ISchemaEntity schema, DeleteContent command) + public static async Task CanDelete(ISchemaEntity schema, ContentState content, IContentRepository contentRepository, DeleteContent command) { Guard.NotNull(command, nameof(command)); @@ -120,6 +121,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { throw new DomainException(T.Get("contents.singletonNotDeletable")); } + + if (command.CheckReferrers) + { + var hasReferrer = await contentRepository.HasReferrersAsync(content.AppId.Id, command.ContentId); + + if (hasReferrer) + { + throw new DomainException(T.Get("contents.referenced")); + } + } } private static void ValidateData(ContentDataCommand command, AddValidation e) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs similarity index 65% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs index 097aba0d8..e56db09ce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.DefaultValues; using Squidex.Domain.Apps.Core.Schemas; @@ -16,6 +17,7 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; @@ -23,7 +25,7 @@ using Squidex.Infrastructure.Validation; #pragma warning disable IDE0016 // Use 'throw' expression -namespace Squidex.Domain.Apps.Entities.Contents +namespace Squidex.Domain.Apps.Entities.Contents.Operations { public sealed class ContentOperationContext { @@ -37,20 +39,37 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly IScriptEngine scriptEngine; private readonly ISemanticLog log; private readonly IAppProvider appProvider; - private readonly IEnumerable factories; + private readonly IEnumerable validators; + private readonly IContentWorkflow contentWorkflow; + private readonly IContentRepository contentRepository; private ISchemaEntity schema; private IAppEntity app; private ContentCommand command; private ValidationContext validationContext; + public IContentWorkflow Workflow => contentWorkflow; + + public IContentRepository Repository => contentRepository; + public ContentOperationContext( IAppProvider appProvider, - IEnumerable factories, + IEnumerable validators, + IContentWorkflow contentWorkflow, + IContentRepository contentRepository, IScriptEngine scriptEngine, ISemanticLog log) { + Guard.NotDefault(appProvider, nameof(appProvider)); + Guard.NotDefault(validators, nameof(validators)); + Guard.NotDefault(contentWorkflow, nameof(contentWorkflow)); + Guard.NotDefault(contentRepository, nameof(contentRepository)); + Guard.NotDefault(scriptEngine, nameof(scriptEngine)); + Guard.NotDefault(log, nameof(log)); + this.appProvider = appProvider; - this.factories = factories; + this.validators = validators; + this.contentWorkflow = contentWorkflow; + this.contentRepository = contentRepository; this.scriptEngine = scriptEngine; this.log = log; @@ -84,16 +103,23 @@ namespace Squidex.Domain.Apps.Entities.Contents validationContext = new ValidationContext(appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized); } + public Task GetInitialStatusAsync() + { + return contentWorkflow.GetInitialStatusAsync(schema); + } + public Task GenerateDefaultValuesAsync(NamedContentData data) { - data.GenerateDefaultValues(schema.SchemaDef, app.PartitionResolver()); + data.GenerateDefaultValues(schema.SchemaDef, Partition()); return Task.CompletedTask; } - public async Task ValidateInputAsync(NamedContentData data) + public async Task ValidateInputAsync(NamedContentData data, bool publish) { - var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories, log); + var validator = + new ContentValidator(Partition(), + validationContext.AsPublishing(publish), validators, log); await validator.ValidateInputAsync(data); @@ -102,7 +128,9 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task ValidateInputPartialAsync(NamedContentData data) { - var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories, log); + var validator = + new ContentValidator(Partition(), + validationContext, validators, log); await validator.ValidateInputPartialAsync(data); @@ -111,8 +139,27 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task ValidateContentAsync(NamedContentData data) { - var validator = new ContentValidator(app.PartitionResolver(), validationContext, factories, log); + var validator = + new ContentValidator(Partition(), + validationContext, validators, log); + + await validator.ValidateContentAsync(data); + + CheckErrors(validator); + } + + public async Task ValidateOnPublishAsync(NamedContentData data) + { + if (!schema.SchemaDef.Properties.ValidateOnPublish) + { + return; + } + + var validator = + new ContentValidator(Partition(), + validationContext.AsPublishing(), validators, log); + await validator.ValidateInputAsync(data); await validator.ValidateContentAsync(data); CheckErrors(validator); @@ -126,6 +173,11 @@ namespace Squidex.Domain.Apps.Entities.Contents } } + public bool HasScript(Func script) + { + return !string.IsNullOrWhiteSpace(GetScript(script)); + } + public async Task ExecuteScriptAndTransformAsync(Func script, ScriptVars context) { Enrich(context); @@ -154,6 +206,11 @@ namespace Squidex.Domain.Apps.Entities.Contents await scriptEngine.ExecuteAsync(context, GetScript(script), ScriptOptions); } + private PartitionResolver Partition() + { + return app.PartitionResolver(); + } + private void Enrich(ScriptVars context) { context.ContentId = command.ContentId; 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 adadffb66..bc5f83f0a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs @@ -29,8 +29,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Validation this.contentRepository = contentRepository; } - public IEnumerable CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator) + public IEnumerable CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator) { + if (context.Mode == ValidationMode.Optimized) + { + yield break; + } + if (field is IField assetsField) { var checkAssets = new CheckAssets(async ids => diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs index f0010a92f..ac7738894 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -31,44 +31,44 @@ namespace Squidex.Domain.Apps.Entities public Q WithQuery(ClrQuery? query) { - return Clone(c => c.Query = query); + return Clone(clone => clone.Query = query); } public Q WithODataQuery(string? odataQuery) { - return Clone(c => c.ODataQuery = odataQuery); + return Clone(clone => clone.ODataQuery = odataQuery); } public Q WithJsonQuery(string? jsonQuery) { - return Clone(c => c.JsonQuery = jsonQuery); + return Clone(clone => clone.JsonQuery = jsonQuery); } public Q WithJsonQuery(Query? jsonQuery) { - return Clone(c => c.ParsedJsonQuery = jsonQuery); + return Clone(clone => clone.ParsedJsonQuery = jsonQuery); } public Q WithIds(params DomainId[] ids) { - return Clone(c => c.Ids = ids.ToList()); + return Clone(clone => clone.Ids = ids.ToList()); } public Q WithReference(DomainId? reference) { - return Clone(c => c.Reference = reference); + return Clone(clone => clone.Reference = reference); } public Q WithIds(IEnumerable ids) { - return Clone(c => c.Ids = ids.ToList()); + return Clone(clone => clone.Ids = ids.ToList()); } public Q WithIds(string? ids) { if (!string.IsNullOrEmpty(ids)) { - return Clone(c => + return Clone(clone => { var idsList = new List(); @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities idsList.Add(DomainId.Create(id)); } - c.Ids = idsList; + clone.Ids = idsList; }); } diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index fba3d0a5d..a8486a55a 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -442,9 +442,6 @@ Transizione - - Il contento è stato già eliminato. - Ci sono più contenuti che corrispondono alla query. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index ae161559d..4327aa279 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -442,9 +442,6 @@ Overgang - - Content is al verwijderd. - Meer dan één inhoud komt overeen met de zoekopdracht. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index df10f50cf..f4b63d137 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -442,9 +442,6 @@ Transition - - Content has already been deleted. - More than one content matches to the query. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs index be00a4f25..c0f1de0b9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs @@ -43,6 +43,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public bool IsRequired { get; set; } + /// + /// Indicates if the field is required when publishing. + /// + public bool IsRequiredOnPublish { get; set; } + /// /// Indicates if the field should be rendered with half width only. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs index c8630b2df..e7d7288bd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs @@ -34,6 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public string? ContentSidebarUrl { get; set; } + /// + /// True to validate the content items on publish. + /// + public bool ValidateOnPublish { get; set; } + /// /// Tags for automation processes. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs index fec58f16c..9cbadf8ec 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs @@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public string? ContentSidebarUrl { get; set; } + /// + /// True to validate the content items on publish. + /// + public bool ValidateOnPublish { get; set; } + /// /// Tags for automation processes. /// diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 122c98e7c..c1e32be9f 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Operations; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Text; 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 f707ebb71..f9a712cd6 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 @@ -118,14 +118,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public static ValidationContext CreateContext( Schema? schema = null, ValidationMode mode = ValidationMode.Default, - ValidationUpdater? updater = null) + ValidationUpdater? updater = null, + ValidationAction action = ValidationAction.Upsert) { var context = new ValidationContext( AppId, SchemaId, schema ?? new Schema(SchemaId.Name), - DomainId.NewGuid(), - mode); + DomainId.NewGuid()); + + context = context.WithMode(mode).WithAction(action); if (updater != null) { 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 476cb53f9..15d83ac7e 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 @@ -102,18 +102,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators new[] { $"[1]: Id {assetId} not found." }); } - [Fact] - public async Task Should_not_add_error_if_asset_are_not_valid_but_in_optimized_mode() - { - var assetId = DomainId.NewGuid(); - - var sut = Validator(new AssetsFieldProperties()); - - await sut.ValidateAsync(CreateValue(assetId), errors, updater: c => c.Optimized()); - - Assert.Empty(errors); - } - [Fact] public async Task Should_add_error_if_document_is_too_small() { 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 3b0fd1bcd..2518716eb 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 @@ -34,16 +34,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators new[] { $"Contains invalid reference '{ref1}'." }); } - [Fact] - public async Task Should_not_add_error_if_reference_are_not_valid_but_in_optimized_mode() - { - var sut = new ReferencesValidator(Enumerable.Repeat(schemaId, 1), FoundReferences()); - - await sut.ValidateAsync(CreateValue(ref1), errors, updater: c => c.Optimized()); - - Assert.Empty(errors); - } - [Fact] public async Task Should_not_add_error_if_schemas_not_defined() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 7ed9a6b09..eb541a6dc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Operations; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Schemas; @@ -108,9 +109,13 @@ namespace Squidex.Domain.Apps.Entities.Contents var validators = Enumerable.Repeat(new DefaultValidatorsFactory(), 1); - var context = new ContentOperationContext(appProvider, validators, scriptEngine, A.Fake()); + var context = new ContentOperationContext(appProvider, + validators, + contentWorkflow, + contentRepository, + scriptEngine, A.Fake()); - sut = new ContentDomainObject(Store, A.Dummy(), contentWorkflow, contentRepository, context); + sut = new ContentDomainObject(Store, A.Dummy(), context); sut.Setup(Id); } @@ -588,7 +593,7 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id)) + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId)) .Returns(true); await Assert.ThrowsAsync(() => PublishAsync(command)); @@ -601,7 +606,7 @@ namespace Squidex.Domain.Apps.Entities.Contents await ExecuteCreateAsync(); - A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id)) + A.CallTo(() => contentRepository.HasReferrersAsync(AppId, contentId)) .Returns(true); await PublishAsync(command); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index dc7b4f197..b2c9c0fe5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard public class GuardContentTests : IClassFixture { private readonly IContentWorkflow contentWorkflow = A.Fake(); + private readonly IContentRepository contentRepository = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly ClaimsPrincipal user = Mocks.FrontendUser(); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); @@ -254,7 +256,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard { CreateSchema(false); - var content = new ContentState(); + var content = CreateContent(Status.Published); var command = new DeleteContentDraft(); Assert.Throws(() => GuardContent.CanDeleteDraft(command, content)); @@ -270,23 +272,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard } [Fact] - public void CanDelete_should_throw_exception_if_singleton() + public async Task CanDelete_should_throw_exception_if_singleton() + { + var schema = CreateSchema(true); + + var content = CreateContent(Status.Published); + var command = new DeleteContent(); + + await Assert.ThrowsAsync(() => GuardContent.CanDelete(schema, content, contentRepository, command)); + } + + [Fact] + public async Task CanDelete_should_throw_exception_if_referenced() { var schema = CreateSchema(true); + var content = CreateContent(Status.Published); var command = new DeleteContent(); - Assert.Throws(() => GuardContent.CanDelete(schema, command)); + A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, content.Id)) + .Returns(true); + + await Assert.ThrowsAsync(() => GuardContent.CanDelete(schema, content, contentRepository, command)); } [Fact] - public void CanDelete_should_not_throw_exception() + public async Task CanDelete_should_not_throw_exception() { var schema = CreateSchema(false); + var content = CreateContent(Status.Published); var command = new DeleteContent(); - GuardContent.CanDelete(schema, command); + await GuardContent.CanDelete(schema, content, contentRepository, command); } private void SetupCanUpdate(bool canUpdate) @@ -306,19 +324,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard return Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "my-schema"), new Schema("schema", isSingleton: isSingleton)); } - private static ContentState CreateDraftContent(Status status) + private ContentState CreateDraftContent(Status status) { return new ContentState { - NewVersion = new ContentVersion(status, new NamedContentData()) + Id = DomainId.NewGuid(), + NewVersion = new ContentVersion(status, new NamedContentData()), + AppId = appId }; } - private static ContentState CreateContent(Status status) + private ContentState CreateContent(Status status) { return new ContentState { - CurrentVersion = new ContentVersion(status, new NamedContentData()) + Id = DomainId.NewGuid(), + CurrentVersion = new ContentVersion(status, new NamedContentData()), + AppId = appId }; } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs index 9881c5ca5..1fc1fa3fe 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs @@ -28,17 +28,17 @@ namespace Squidex.Domain.Users [Fact] public void Should_read_from_store() { - A.CallTo(() => store.ReadAllAsync(A< Func>._, A._)) + A.CallTo(() => store.ReadAllAsync(A>._, A._)) .Invokes((Func callback, CancellationToken _) => { callback(new DefaultXmlRepository.State { - Xml = new XElement("a").ToString() + Xml = new XElement("xml").ToString() }, 0); callback(new DefaultXmlRepository.State { - Xml = new XElement("b").ToString() + Xml = new XElement("xml").ToString() }, 0); }); @@ -50,7 +50,7 @@ namespace Squidex.Domain.Users [Fact] public void Should_write_to_store() { - var xml = new XElement("x"); + var xml = new XElement("xml"); sut.StoreElement(xml, "name"); diff --git a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html index 67074d7cd..7e8e63fae 100644 --- a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html +++ b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html @@ -9,7 +9,7 @@ - +
@@ -59,6 +59,15 @@ {{ 'schemas.schemaTagsHint' | sqxTranslate }}
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html index ae4bc8db4..c960df2dc 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html @@ -62,8 +62,13 @@ -
- +
+
+ + {{ 'schemas.fieldTypes.string.wordHint' | sqxTranslate}} + +
+
diff --git a/frontend/app/shared/services/schemas.service.spec.ts b/frontend/app/shared/services/schemas.service.spec.ts index b2084aef5..cd62f6f03 100644 --- a/frontend/app/shared/services/schemas.service.spec.ts +++ b/frontend/app/shared/services/schemas.service.spec.ts @@ -643,6 +643,7 @@ describe('SchemasService', () => { label: `label${id}${suffix}`, contentsSidebarUrl: `url/to/contents/${id}${suffix}`, contentSidebarUrl: `url/to/content/${id}${suffix}`, + validateOnPublish: id % 2 === 1, tags: [ `tags${id}${suffix}` ], @@ -823,6 +824,7 @@ function createSchemaProperties(id: number, suffix = '') { `hints${id}${suffix}`, `url/to/contents/${id}${suffix}`, `url/to/content/${id}${suffix}`, + id % 2 === 0, [ `tags${id}${suffix}` ] diff --git a/frontend/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts index 1b39a7fa1..35402bc56 100644 --- a/frontend/app/shared/services/schemas.service.ts +++ b/frontend/app/shared/services/schemas.service.ts @@ -325,6 +325,7 @@ export class SchemaPropertiesDto { public readonly hints?: string, public readonly contentsSidebarUrl?: string, public readonly contentSidebarUrl?: string, + public readonly validateOnPublish?: boolean, public readonly tags?: ReadonlyArray ) { } @@ -366,6 +367,7 @@ export interface UpdateSchemaDto { readonly hints?: string; readonly contentsSidebarUrl?: string; readonly contentSidebarUrl?: string; + readonly validateOnPublish?: boolean; readonly tags?: ReadonlyArray; } @@ -741,6 +743,7 @@ function parseProperties(response: any) { response.hints, response.contentsSidebarUrl, response.contentSidebarUrl, + response.validateOnPublish, response.tags); } diff --git a/frontend/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts index 4fd44e9d2..34543b51f 100644 --- a/frontend/app/shared/services/schemas.types.ts +++ b/frontend/app/shared/services/schemas.types.ts @@ -135,6 +135,7 @@ export abstract class FieldPropertiesDto { public readonly editorUrl?: string; public readonly hints?: string; public readonly isRequired: boolean = false; + public readonly isRequiredOnPublish: boolean = false; public readonly isHalfWidth: boolean = false; public readonly label?: string; public readonly placeholder?: string; diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts index 975583702..6f83b106c 100644 --- a/frontend/app/shared/state/schemas.forms.ts +++ b/frontend/app/shared/state/schemas.forms.ts @@ -214,6 +214,7 @@ export class EditFieldForm extends Form { ], editorUrl: null, isRequired: false, + isRequiredOnPublish: false, isHalfWidth: false, tags: [] })); @@ -235,6 +236,7 @@ export class EditSchemaForm extends Form