Browse Source

Validation fixes and improvements.

pull/596/head
Sebastian 5 years ago
parent
commit
bbe951c677
  1. 2
      backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs
  2. 3
      backend/i18n/frontend_en.json
  3. 3
      backend/i18n/frontend_it.json
  4. 3
      backend/i18n/frontend_nl.json
  5. 1
      backend/i18n/source/backend_en.json
  6. 1
      backend/i18n/source/backend_it.json
  7. 1
      backend/i18n/source/backend_nl.json
  8. 3
      backend/i18n/source/frontend_en.json
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  12. 20
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  13. 75
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  14. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultValidatorsFactory.cs
  15. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs
  16. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationAction.cs
  17. 76
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  18. 36
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorContext.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AggregateValidator.cs
  20. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  21. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs
  22. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  23. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs
  24. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  25. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  27. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs
  28. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
  29. 56
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  30. 13
      backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  31. 75
      backend/src/Squidex.Domain.Apps.Entities/Contents/Operations/ContentOperationContext.cs
  32. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/Validation/DependencyValidatorsFactory.cs
  33. 18
      backend/src/Squidex.Domain.Apps.Entities/Q.cs
  34. 3
      backend/src/Squidex.Shared/Texts.it.resx
  35. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  36. 3
      backend/src/Squidex.Shared/Texts.resx
  37. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs
  38. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs
  39. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs
  40. 1
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  41. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs
  42. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs
  43. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/ReferencesValidatorTests.cs
  44. 13
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  45. 40
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
  46. 8
      backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs
  47. 11
      frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  48. 11
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html
  49. 9
      frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html
  50. 2
      frontend/app/shared/services/schemas.service.spec.ts
  51. 3
      frontend/app/shared/services/schemas.service.ts
  52. 1
      frontend/app/shared/services/schemas.types.ts
  53. 2
      frontend/app/shared/state/schemas.forms.ts
  54. 2
      frontend/app/theme/_forms.scss

2
backend/extensions/Squidex.Extensions/Validation/CompositeUniqueValidatorFactory.cs

@ -25,7 +25,7 @@ namespace Squidex.Extensions.Validation
this.contentRepository = contentRepository;
}
public IEnumerable<IValidator> CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator)
public IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, FieldValidatorFactory createFieldValidator)
{
foreach (var validatorTag in ValidatorTags(context.Schema.Properties.Tags))
{

3
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",

3
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",

3
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",

1
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.",

1
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.",

1
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.",

3
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",

2
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; }

6
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; }
}
}

2
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 });
}

20
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<string> path, string message)
@ -82,28 +84,28 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private IValidator CreateSchemaValidator(bool isPartial)
{
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(context.Schema.Fields.Count);
var fieldValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(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<ContentFieldData>(fieldsValidators, isPartial, "field");
return new ObjectValidator<ContentFieldData>(fieldValidators, isPartial, "field");
}
private IValidator CreateFieldValidator(IRootField field, bool isPartial)
{
var partitioning = partitionResolver(field.Partitioning);
var fieldValidator = CreateFieldValidator(field);
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
var partitioning = partitionResolver(field.Partitioning);
var partitioningValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
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<IJsonValue>(fieldsValidators, isPartial, typeName), 1)), log);
new ObjectValidator<IJsonValue>(partitioningValidators, isPartial, typeName), 1)), log);
}
private IValidator CreateFieldValidator(IField field)

75
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<IEnumerable<IValidator>>
{
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<IValidator> CreateValidators(IField field, FieldValidatorFactory createFieldValidator)
public static IEnumerable<IValidator> 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<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count);
var nestedValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(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<IJsonValue>(nestedSchema, false, "field"));
yield return new CollectionItemValidator(new ObjectValidator<IJsonValue>(nestedValidators, false, "field"));
}
public IEnumerable<IValidator> Visit(IField<AssetsFieldProperties> 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;
}
}
}

6
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<IValidator> CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
public IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{
if (field is IField<UIFieldProperties>)
{
@ -21,9 +21,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
}
}
public IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
public IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{
return DefaultFieldValueValidatorsFactory.CreateValidators(field, createFieldValidator);
return DefaultFieldValueValidatorsFactory.CreateValidators(context, field, createFieldValidator);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IValidatorsFactory.cs

@ -14,17 +14,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public interface IValidatorsFactory
{
IEnumerable<IValidator> CreateFieldValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
IEnumerable<IValidator> CreateFieldValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{
yield break;
}
IEnumerable<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{
yield break;
}
IEnumerable<IValidator> CreateContentValidators(ValidationContext context, FieldValidatorFactory createFieldValidator)
IEnumerable<IValidator> CreateContentValidators(ValidatorContext context, FieldValidatorFactory createFieldValidator)
{
yield break;
}

15
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
}
}

76
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<string> Path { get; }
public NamedId<DomainId> AppId { get; }
public NamedId<DomainId> SchemaId { get; }
public Schema Schema { get; }
public ImmutableQueue<string> Path { get; private set; } = ImmutableQueue<string>.Empty;
public DomainId ContentId { get; }
public bool IsOptional { get; }
public ValidationMode Mode { get; }
public bool IsOptional { get; private set; }
public ValidationContext(
NamedId<DomainId> appId,
NamedId<DomainId> schemaId,
Schema schema,
DomainId contentId,
ValidationMode mode = ValidationMode.Default)
: this(appId, schemaId, schema, contentId, ImmutableQueue<string>.Empty, false, mode)
DomainId contentId)
: base(appId, schemaId, schema)
{
ContentId = contentId;
}
private ValidationContext(
NamedId<DomainId> appId,
NamedId<DomainId> schemaId,
Schema schema,
DomainId contentId,
ImmutableQueue<string> 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<string> path, bool isOptional, ValidationMode mode)
private ValidationContext Clone(Action<ValidationContext> updater)
{
return new ValidationContext(AppId, SchemaId, Schema, ContentId, path, isOptional, mode);
var clone = (ValidationContext)MemberwiseClone();
updater(clone);
return clone;
}
}
}

36
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<DomainId> AppId { get; }
public NamedId<DomainId> SchemaId { get; }
public Schema Schema { get; }
public ValidationMode Mode { get; protected set; }
public ValidationAction Action { get; protected set; }
protected ValidatorContext(
NamedId<DomainId> appId,
NamedId<DomainId> schemaId,
Schema schema)
{
AppId = appId;
Schema = schema;
SchemaId = schemaId;
}
}
}

2
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;

13
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<DomainId> 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 &&

1
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;

7
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<IValidator>? validators, IField field)
{
Guard.NotNull(field, nameof(field));
this.validators = validators?.ToArray();
this.validators = validators?.ToArray() ?? Array.Empty<IValidator>();
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<Task>();

10
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<TValue> : IValidator
{
private static readonly IReadOnlyDictionary<string, TValue> DefaultValue = new Dictionary<string, TValue>();
private readonly IDictionary<string, (bool IsOptional, IValidator Validator)> schema;
private readonly IDictionary<string, (bool IsOptional, IValidator Validator)> fields;
private readonly bool isPartial;
private readonly string fieldType;
public ObjectValidator(IDictionary<string, (bool IsOptional, IValidator Validator)> schema, bool isPartial, string fieldType)
public ObjectValidator(IDictionary<string, (bool IsOptional, IValidator Validator)> 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<Task>();
foreach (var (fieldName, fieldConfig) in schema)
foreach (var (fieldName, fieldConfig) in fields)
{
var (isOptional, validator) = fieldConfig;

5
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<DomainId> contentIds)
{
var foundIds = await checkReferences(contentIds.ToHashSet());

5
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)))

2
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);
}

6
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<bool> 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)

2
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; }
}
}

56
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<ContentState>
{
private readonly IContentWorkflow contentWorkflow;
private readonly IContentRepository contentRepository;
private readonly ContentOperationContext context;
public ContentDomainObject(IStore<DomainId> 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<object?> 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<DomainId> appId, NamedId<DomainId> schemaId, ContentCommand command, bool optimized = false)
{
return context.LoadAsync(appId, schemaId, command, optimized);

13
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)

75
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs → 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<IValidatorsFactory> factories;
private readonly IEnumerable<IValidatorsFactory> 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<IValidatorsFactory> factories,
IEnumerable<IValidatorsFactory> 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<Status> 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<SchemaScripts, string> script)
{
return !string.IsNullOrWhiteSpace(GetScript(script));
}
public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> 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;

7
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<IValidator> CreateValueValidators(ValidationContext context, IField field, FieldValidatorFactory createFieldValidator)
public IEnumerable<IValidator> CreateValueValidators(ValidatorContext context, IField field, FieldValidatorFactory createFieldValidator)
{
if (context.Mode == ValidationMode.Optimized)
{
yield break;
}
if (field is IField<AssetsFieldProperties> assetsField)
{
var checkAssets = new CheckAssets(async ids =>

18
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<IJsonValue>? 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<DomainId> 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<DomainId>();
@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities
idsList.Add(DomainId.Create(id));
}
c.Ids = idsList;
clone.Ids = idsList;
});
}

3
backend/src/Squidex.Shared/Texts.it.resx

@ -442,9 +442,6 @@
<data name="common.workflowTransition" xml:space="preserve">
<value>Transizione</value>
</data>
<data name="contents.alreadyDeleted" xml:space="preserve">
<value>Il contento è stato già eliminato.</value>
</data>
<data name="contents.bulkInsertQueryNotUnique" xml:space="preserve">
<value>Ci sono più contenuti che corrispondono alla query.</value>
</data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -442,9 +442,6 @@
<data name="common.workflowTransition" xml:space="preserve">
<value>Overgang</value>
</data>
<data name="contents.alreadyDeleted" xml:space="preserve">
<value>Content is al verwijderd.</value>
</data>
<data name="contents.bulkInsertQueryNotUnique" xml:space="preserve">
<value>Meer dan één inhoud komt overeen met de zoekopdracht.</value>
</data>

3
backend/src/Squidex.Shared/Texts.resx

@ -442,9 +442,6 @@
<data name="common.workflowTransition" xml:space="preserve">
<value>Transition</value>
</data>
<data name="contents.alreadyDeleted" xml:space="preserve">
<value>Content has already been deleted.</value>
</data>
<data name="contents.bulkInsertQueryNotUnique" xml:space="preserve">
<value>More than one content matches to the query.</value>
</data>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs

@ -43,6 +43,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Indicates if the field is required when publishing.
/// </summary>
public bool IsRequiredOnPublish { get; set; }
/// <summary>
/// Indicates if the field should be rendered with half width only.
/// </summary>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs

@ -34,6 +34,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// True to validate the content items on publish.
/// </summary>
public bool ValidateOnPublish { get; set; }
/// <summary>
/// Tags for automation processes.
/// </summary>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs

@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary>
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// True to validate the content items on publish.
/// </summary>
public bool ValidateOnPublish { get; set; }
/// <summary>
/// Tags for automation processes.
/// </summary>

1
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;

8
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)
{

12
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()
{

10
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()
{

13
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<ISemanticLog>());
var context = new ContentOperationContext(appProvider,
validators,
contentWorkflow,
contentRepository,
scriptEngine, A.Fake<ISemanticLog>());
sut = new ContentDomainObject(Store, A.Dummy<ISemanticLog>(), contentWorkflow, contentRepository, context);
sut = new ContentDomainObject(Store, A.Dummy<ISemanticLog>(), 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<DomainException>(() => 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);

40
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<TranslationsFixture>
{
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly NamedId<DomainId> 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<DomainException>(() => 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<DomainException>(() => 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<DomainException>(() => GuardContent.CanDelete(schema, command));
A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, content.Id))
.Returns(true);
await Assert.ThrowsAsync<DomainException>(() => 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
};
}
}

8
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<DefaultXmlRepository.State, long, Task>>._, A<CancellationToken>._))
A.CallTo(() => store.ReadAllAsync(A<Func<DefaultXmlRepository.State, long, Task>>._, A<CancellationToken>._))
.Invokes((Func<DefaultXmlRepository.State, long, Task> 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");

11
frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html

@ -9,7 +9,7 @@
<input type="text" class="form-control" id="name" readonly [ngModel]="schema.name" [ngModelOptions]="standalone">
</div>
<div class="form-group">
<label for="label">{{ 'common.label' | sqxTranslate }}</label>
@ -59,6 +59,15 @@
<sqx-form-hint>{{ 'schemas.schemaTagsHint' | sqxTranslate }}</sqx-form-hint>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" id="validateOnPublish" formControlName="validateOnPublish">
<label class="custom-control-label" for="validateOnPublish">
{{ 'schemas.validateOnPublish' | sqxTranslate }}
</label>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="float-right btn btn-primary" *ngIf="isEditable">

11
frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html

@ -9,6 +9,17 @@
</div>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-9 offset-3">
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" id="{{field.fieldId}}_fieldRequiredOnPublish" formControlName="isRequiredOnPublish">
<label class="custom-control-label" for="{{field.fieldId}}_fieldRequiredOnPublish">
{{ 'schemas.field.requiredOnPublish' | sqxTranslate }}
</label>
</div>
</div>
</div>
</div>
<ng-container [ngSwitch]="field.rawProperties.fieldType">

9
frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html

@ -62,8 +62,13 @@
</div>
</div>
<hr />
<div class="form-group row">
<div class="col-7 offset-3">
<sqx-form-hint>
{{ 'schemas.fieldTypes.string.wordHint' | sqxTranslate}}
</sqx-form-hint>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.contentType' | sqxTranslate }}</label>

2
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}`
]

3
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<string>
) {
}
@ -366,6 +367,7 @@ export interface UpdateSchemaDto {
readonly hints?: string;
readonly contentsSidebarUrl?: string;
readonly contentSidebarUrl?: string;
readonly validateOnPublish?: boolean;
readonly tags?: ReadonlyArray<string>;
}
@ -741,6 +743,7 @@ function parseProperties(response: any) {
response.hints,
response.contentsSidebarUrl,
response.contentSidebarUrl,
response.validateOnPublish,
response.tags);
}

1
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;

2
frontend/app/shared/state/schemas.forms.ts

@ -214,6 +214,7 @@ export class EditFieldForm extends Form<FormGroup, {}, FieldPropertiesDto> {
],
editorUrl: null,
isRequired: false,
isRequiredOnPublish: false,
isHalfWidth: false,
tags: []
}));
@ -235,6 +236,7 @@ export class EditSchemaForm extends Form<FormGroup, UpdateSchemaDto, SchemaPrope
],
contentsSidebarUrl: '',
contentSidebarUrl: '',
validateOnPublish: false,
tags: []
}));
}

2
frontend/app/theme/_forms.scss

@ -111,7 +111,7 @@
.form-alert {
@include absolute(.25rem, 0, auto, auto);
font-size: .9rem;
font-size: 1rem;
font-weight: normal;
line-height: 1.75;
max-width: 600px;

Loading…
Cancel
Save